mirror of
https://github.com/thooge/esp32-nmea2000-obp60.git
synced 2025-12-28 13:13:07 +01:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b477331de | |||
| 9b9bf76e4d |
@@ -137,7 +137,6 @@ class GwApi{
|
||||
* thread safe methods - can directly be called from a user task
|
||||
*/
|
||||
virtual GwRequestQueue *getQueue()=0;
|
||||
virtual QueueHandle_t getKbQueue()=0;
|
||||
virtual void sendN2kMessage(const tN2kMsg &msg, bool convert=true)=0;
|
||||
/**
|
||||
* deprecated - sourceId will be ignored
|
||||
|
||||
@@ -71,7 +71,6 @@ class GwConverterConfig{
|
||||
int rmcInterval=1000;
|
||||
int rmcCheckTime=4000;
|
||||
int winst312=256;
|
||||
int swBankInstance=0;
|
||||
bool unmappedXdr=false;
|
||||
unsigned long xdrTimeout=60000;
|
||||
std::vector<WindMapping> windMappings;
|
||||
@@ -98,7 +97,6 @@ class GwConverterConfig{
|
||||
windMappings.push_back(mapping);
|
||||
}
|
||||
}
|
||||
swBankInstance=config->getInt(GwConfigDefinitions::swBankInstance,0);
|
||||
}
|
||||
const WindMapping findWindMapping(const tN2kWindReference &n2k) const{
|
||||
for (const auto & it:windMappings){
|
||||
@@ -115,4 +113,4 @@ class GwConverterConfig{
|
||||
|
||||
|
||||
};
|
||||
#endif
|
||||
#endif
|
||||
@@ -1572,36 +1572,6 @@ private:
|
||||
finalizeXdr();
|
||||
}
|
||||
|
||||
void Handle127502(const tN2kMsg &msg){
|
||||
// switch bank control / receive remote key strokes
|
||||
unsigned char instance=-1;
|
||||
tN2kBinaryStatus bankstatus;
|
||||
LOG_DEBUG(GwLog::LOG,"received switch bank control");
|
||||
// check if we are addressed and our configured instance is used
|
||||
if (! ParseN2kPGN127502(msg,instance,bankstatus)) {
|
||||
LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN);
|
||||
return;
|
||||
}
|
||||
if (! (instance == config.swBankInstance)) {
|
||||
LOG_DEBUG(GwLog::DEBUG,"switch bank instance #%d ignored",instance);
|
||||
return;
|
||||
}
|
||||
// TODO (?) multiple keys together
|
||||
|
||||
// only process configured key count (default 6)
|
||||
for (uint8_t i=1; i<=6; i++) {
|
||||
tN2kOnOff keystatus = N2kGetStatusOnBinaryStatus(bankstatus, i);
|
||||
if (keystatus == 1) {
|
||||
// key pressed: send key to queue
|
||||
xQueueSend(keyboardQueue, &i, 0);
|
||||
} else if (keystatus == 2) {
|
||||
// long key pressed: send long key to queue
|
||||
xQueueSend(keyboardQueue, &i, 0);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void registerConverters()
|
||||
{
|
||||
//register all converter functions
|
||||
@@ -1637,7 +1607,6 @@ private:
|
||||
converters.registerConverter(127488UL, &N2kToNMEA0183Functions::Handle127488);
|
||||
converters.registerConverter(130316UL, &N2kToNMEA0183Functions::Handle130316);
|
||||
converters.registerConverter(127257UL, &N2kToNMEA0183Functions::Handle127257);
|
||||
converters.registerConverter(127502UL, &N2kToNMEA0183Functions::Handle127502);
|
||||
#define HANDLE_AIS
|
||||
#ifdef HANDLE_AIS
|
||||
converters.registerConverter(129038UL, &N2kToNMEA0183Functions::HandleAISClassAPosReport); // AIS Class A Position Report, Message Type 1
|
||||
@@ -1651,15 +1620,13 @@ private:
|
||||
public:
|
||||
N2kToNMEA0183Functions(GwLog *logger, GwBoatData *boatData,
|
||||
SendNMEA0183MessageCallback callback,
|
||||
String talkerId, GwXDRMappings *xdrMappings, const GwConverterConfig &cfg,
|
||||
QueueHandle_t kbQueue)
|
||||
String talkerId, GwXDRMappings *xdrMappings, const GwConverterConfig &cfg)
|
||||
: N2kDataToNMEA0183(logger, boatData, callback,talkerId)
|
||||
{
|
||||
this->logger = logger;
|
||||
this->boatData = boatData;
|
||||
this->xdrMappings=xdrMappings;
|
||||
this->config=cfg;
|
||||
this->keyboardQueue=kbQueue;
|
||||
registerConverters();
|
||||
}
|
||||
virtual void loop(unsigned long lastExtRmc) override
|
||||
@@ -1675,8 +1642,8 @@ private:
|
||||
|
||||
N2kDataToNMEA0183* N2kDataToNMEA0183::create(GwLog *logger, GwBoatData *boatData,
|
||||
SendNMEA0183MessageCallback callback, String talkerId, GwXDRMappings *xdrMappings,
|
||||
const GwConverterConfig &cfg, QueueHandle_t kbQueue){
|
||||
LOG_DEBUG(GwLog::LOG,"creating N2kToNMEA0183");
|
||||
return new N2kToNMEA0183Functions(logger,boatData,callback, talkerId,xdrMappings,cfg,kbQueue);
|
||||
const GwConverterConfig &cfg){
|
||||
LOG_DEBUG(GwLog::LOG,"creating N2kToNMEA0183");
|
||||
return new N2kToNMEA0183Functions(logger,boatData,callback, talkerId,xdrMappings,cfg);
|
||||
}
|
||||
//*****************************************************************************
|
||||
|
||||
@@ -43,7 +43,6 @@ protected:
|
||||
GwBoatData *boatData;
|
||||
int sourceId=0;
|
||||
char talkerId[3];
|
||||
QueueHandle_t keyboardQueue;
|
||||
SendNMEA0183MessageCallback sendNMEA0183MessageCallback;
|
||||
void SendMessage(const tNMEA0183Msg &NMEA0183Msg);
|
||||
N2kDataToNMEA0183(GwLog *logger, GwBoatData *boatData,
|
||||
@@ -51,8 +50,7 @@ protected:
|
||||
|
||||
public:
|
||||
static N2kDataToNMEA0183* create(GwLog *logger, GwBoatData *boatData, SendNMEA0183MessageCallback callback,
|
||||
String talkerId, GwXDRMappings *xdrMappings,const GwConverterConfig &cfg,
|
||||
QueueHandle_t kbQueue);
|
||||
String talkerId, GwXDRMappings *xdrMappings,const GwConverterConfig &cfg);
|
||||
virtual void HandleMsg(const tN2kMsg &N2kMsg, int sourceId) = 0;
|
||||
virtual void loop(unsigned long lastRmc);
|
||||
virtual ~N2kDataToNMEA0183(){}
|
||||
@@ -61,4 +59,4 @@ public:
|
||||
virtual void toJson(GwJsonDocument *json)=0;
|
||||
virtual String handledKeys()=0;
|
||||
};
|
||||
#endif
|
||||
#endif
|
||||
443
lib/obp60task/PageAnchor.cpp
Normal file
443
lib/obp60task/PageAnchor.cpp
Normal file
@@ -0,0 +1,443 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
#if defined BOARD_OBP60S3 || defined BOARD_OBP40S3
|
||||
|
||||
/*
|
||||
This page is in experimental stage so be warned!
|
||||
North is up.
|
||||
Anchor page with background map from mapservice
|
||||
|
||||
Boatdata used
|
||||
DBS - Water depth
|
||||
HDT - Boat heading
|
||||
AWS - Wind strength; Boat not moving so we assume AWS=TWS and AWD=TWD
|
||||
AWD - Wind direction
|
||||
LAT/LON - Boat position, current
|
||||
HDOP - Position error
|
||||
|
||||
Drop / raise function in device OBP40 has to be done inside
|
||||
config mode because of limited number of buttons.
|
||||
|
||||
TODO
|
||||
gzip for data transfer,
|
||||
manually inflating with tinflate from ROM
|
||||
Save position in FRAM
|
||||
Alarm: gps fix lost
|
||||
switch unit feet/meter
|
||||
force map update if new position is different from old position by
|
||||
a certain level (e.g. 10m)
|
||||
|
||||
*/
|
||||
|
||||
#include <WiFi.h>
|
||||
#include <HTTPClient.h>
|
||||
#include "Pagedata.h"
|
||||
#include "OBP60Extensions.h"
|
||||
|
||||
#define anchor_width 16
|
||||
#define anchor_height 16
|
||||
static unsigned char anchor_bits[] PROGMEM = {
|
||||
0x80, 0x01, 0x40, 0x02, 0x40, 0x02, 0x80, 0x01, 0xf0, 0x0f, 0x80, 0x01,
|
||||
0x80, 0x01, 0x88, 0x11, 0x8c, 0x31, 0x8e, 0x71, 0x84, 0x21, 0x86, 0x61,
|
||||
0x86, 0x61, 0xfc, 0x3f, 0xf8, 0x1f, 0x80, 0x01 };
|
||||
|
||||
class PageAnchor : public Page
|
||||
{
|
||||
private:
|
||||
char mode = 'N'; // (N)ormal, (C)onfig
|
||||
int8_t editmode = -1; // marker for menu/edit/set function
|
||||
|
||||
//uint8_t *mapbuf = new uint8_t[10000]; // 8450 Byte without header
|
||||
//int mapbuf_size = 10000;
|
||||
//uint8_t *mapbuf = (uint8_t*) heap_caps_malloc(mapbuf_size, MALLOC_CAP_SPIRAM);
|
||||
GFXcanvas1 *canvas;
|
||||
const uint16_t map_width = 264;
|
||||
const uint16_t map_height = 260;
|
||||
bool map_valid = false;
|
||||
double map_lat = 0; // current center of valid map
|
||||
double map_lon = 0;
|
||||
String server_name; // server with map service
|
||||
String tile_path;
|
||||
|
||||
String lengthformat;
|
||||
|
||||
double scale = 50; // Radius of display circle in meter, depends on lat
|
||||
uint8_t zoom = 15; // map zoom level
|
||||
|
||||
bool alarm = false;
|
||||
bool alarm_enabled = false;
|
||||
uint8_t alarm_range;
|
||||
|
||||
uint8_t chain_length;
|
||||
uint8_t chain = 0;
|
||||
|
||||
bool anchor_set = false;
|
||||
double anchor_lat;
|
||||
double anchor_lon;
|
||||
double anchor_depth;
|
||||
int anchor_ts; // time stamp anchor dropped
|
||||
|
||||
void displayModeNormal(PageData &pageData) {
|
||||
|
||||
// Boatvalues: DBS, HDT, AWS, AWD, LAT, LON, HDOP
|
||||
GwApi::BoatValue *bv_dbs = pageData.values[0]; // DBS
|
||||
String sval_dbs = formatValue(bv_dbs, *commonData).svalue;
|
||||
String sunit_dbs = formatValue(bv_dbs, *commonData).unit;
|
||||
GwApi::BoatValue *bv_hdt = pageData.values[1]; // HDT
|
||||
String sval_hdt = formatValue(bv_hdt, *commonData).svalue;
|
||||
GwApi::BoatValue *bv_aws = pageData.values[2]; // AWS
|
||||
String sval_aws = formatValue(bv_aws, *commonData).svalue;
|
||||
String sunit_aws = formatValue(bv_aws, *commonData).unit;
|
||||
GwApi::BoatValue *bv_awd = pageData.values[3]; // AWD
|
||||
String sval_awd = formatValue(bv_awd, *commonData).svalue;
|
||||
GwApi::BoatValue *bv_lat = pageData.values[4]; // LAT
|
||||
String sval_lat = formatValue(bv_lat, *commonData).svalue;
|
||||
GwApi::BoatValue *bv_lon = pageData.values[5]; // LON
|
||||
String sval_lon = formatValue(bv_lon, *commonData).svalue;
|
||||
GwApi::BoatValue *bv_hdop = pageData.values[6]; // HDOP
|
||||
String sval_hdop = formatValue(bv_hdop, *commonData).svalue;
|
||||
String sunit_hdop = formatValue(bv_hdop, *commonData).unit;
|
||||
|
||||
commonData->logger->logDebug(GwLog::DEBUG, "Drawing at PageAnchor; DBS=%f, HDT=%f, AWS=%f", bv_dbs->value, bv_hdt->value, bv_aws->value);
|
||||
|
||||
// Draw canvas with background map
|
||||
// rhumb(map_lat, map_lon, bv_lat->value, bv_lon->value)
|
||||
int posdiff = 0;
|
||||
if (map_valid) {
|
||||
if (bv_lat->valid and bv_lon->valid) {
|
||||
// calculate movement since last map refresh
|
||||
posdiff = rhumb(map_lat, map_lon, bv_lat->value, bv_lon->value);
|
||||
if (posdiff > 25) {
|
||||
map_lat = bv_lat->value;
|
||||
map_lon = bv_lon->value;
|
||||
getBackgroundMap(map_lat, map_lon, zoom);
|
||||
if (map_valid) {
|
||||
// prepare visible space for anchor-symbol or boat
|
||||
canvas->fillCircle(132, 130, 12, commonData->fgcolor);
|
||||
}
|
||||
}
|
||||
}
|
||||
getdisplay().drawBitmap(68, 20, canvas->getBuffer(), map_width, map_height, commonData->fgcolor);
|
||||
}
|
||||
|
||||
Point c = {200, 150}; // center = anchor position
|
||||
uint16_t r = 125;
|
||||
|
||||
// Circle as map border
|
||||
getdisplay().drawCircle(c.x, c.y, r, commonData->fgcolor);
|
||||
getdisplay().drawCircle(c.x, c.y, r + 1, commonData->fgcolor);
|
||||
|
||||
Point b = {200, 180}; // boat position while dropping anchor
|
||||
|
||||
const std::vector<Point> pts_boat = { // polygon lines
|
||||
{b.x - 5, b.y},
|
||||
{b.x - 5, b.y - 10},
|
||||
{b.x, b.y - 16},
|
||||
{b.x + 5, b.y - 10},
|
||||
{b.x + 5, b.y}
|
||||
};
|
||||
//rotatePoints und dann Linien zeichnen
|
||||
// TODO rotate boat according to current heading
|
||||
if (bv_hdt->valid) {
|
||||
if (map_valid) {
|
||||
Point b1 = rotatePoint(c, {b.x, b.y - 8}, RadToDeg(bv_hdt->value));
|
||||
getdisplay().fillCircle(b1.x, b1.y, 10, commonData->bgcolor);
|
||||
}
|
||||
drawPoly(rotatePoints(c, pts_boat, RadToDeg(bv_hdt->value)), commonData->fgcolor);
|
||||
} else {
|
||||
// no heading available draw north oriented
|
||||
if (map_valid) {
|
||||
getdisplay().fillCircle(b.x, b.y - 8, 10, commonData->bgcolor);
|
||||
}
|
||||
drawPoly(pts_boat, commonData->fgcolor);
|
||||
}
|
||||
|
||||
// Draw wind arrow
|
||||
const std::vector<Point> pts_wind = {
|
||||
{c.x, c.y - r + 25},
|
||||
{c.x - 12, c.y - r - 4},
|
||||
{c.x, c.y - r + 6},
|
||||
{c.x + 12, c.y - r - 4}
|
||||
};
|
||||
if (bv_awd->valid) {
|
||||
fillPoly4(rotatePoints(c, pts_wind, bv_awd->value), commonData->fgcolor);
|
||||
}
|
||||
|
||||
// Title and corner value headings
|
||||
getdisplay().setTextColor(commonData->fgcolor);
|
||||
getdisplay().setFont(&Ubuntu_Bold10pt8b);
|
||||
// Left
|
||||
getdisplay().setCursor(8, 36);
|
||||
getdisplay().print("Anchor");
|
||||
getdisplay().setCursor(8, 210);
|
||||
getdisplay().print("Depth");
|
||||
// Right
|
||||
drawTextRalign(392, 80, "Chain");
|
||||
drawTextRalign(392, 210, "Wind");
|
||||
|
||||
// Units
|
||||
getdisplay().setCursor(8, 272);
|
||||
getdisplay().print(sunit_dbs);
|
||||
drawTextRalign(392, 272, sunit_aws);
|
||||
// drawTextRalign(392, 100, lengthformat); // chain unit not implemented
|
||||
|
||||
// Corner values
|
||||
getdisplay().setFont(&Ubuntu_Bold8pt8b);
|
||||
getdisplay().setCursor(8, 54);
|
||||
getdisplay().print(anchor_set ? "Dropped" : "Ready"); // Anchor state
|
||||
getdisplay().setCursor(8, 72);
|
||||
getdisplay().print("Alarm: "); // Alarm state
|
||||
getdisplay().print(alarm_enabled ? "on" : "off");
|
||||
|
||||
getdisplay().setCursor(8, 120);
|
||||
getdisplay().print("Zoom");
|
||||
getdisplay().setCursor(8, 136);
|
||||
getdisplay().print(zoom);
|
||||
|
||||
getdisplay().setCursor(8, 160);
|
||||
getdisplay().print("diff");
|
||||
getdisplay().setCursor(8, 176);
|
||||
if (map_valid and bv_lat->valid and bv_lon->valid) {
|
||||
getdisplay().print(String(posdiff));
|
||||
} else {
|
||||
getdisplay().print("n/a");
|
||||
}
|
||||
|
||||
// Chain out TODO lengthformat ft/m
|
||||
drawTextRalign(392, 96, String(chain) + " m");
|
||||
drawTextRalign(392, 96+16, "of " + String(chain_length) + " m");
|
||||
|
||||
getdisplay().setFont(&DSEG7Classic_BoldItalic16pt7b);
|
||||
|
||||
// Depth
|
||||
getdisplay().setCursor(8, 250);
|
||||
getdisplay().print(sval_dbs);
|
||||
|
||||
// Wind
|
||||
getdisplay().setCursor(320, 250);
|
||||
getdisplay().print(sval_aws);
|
||||
|
||||
// Position of boat in center of map
|
||||
getdisplay().setFont(&IBM8x8px);
|
||||
drawTextRalign(392, 34, sval_lat);
|
||||
drawTextRalign(392, 44, sval_lon);
|
||||
// quality
|
||||
String hdop = "HDOP: ";
|
||||
if (bv_hdop->valid) {
|
||||
hdop += String(round(bv_hdop->value));
|
||||
} else {
|
||||
hdop += " n/a";
|
||||
}
|
||||
drawTextRalign(392, 54, hdop);
|
||||
|
||||
// zoom scale
|
||||
getdisplay().drawLine(c.x + 10, c.y, c.x + r - 4, c.y, commonData->fgcolor);
|
||||
// arrow left
|
||||
getdisplay().drawLine(c.x + 10, c.y, c.x + 16, c.y - 4, commonData->fgcolor);
|
||||
getdisplay().drawLine(c.x + 10, c.y, c.x + 16, c.y + 4, commonData->fgcolor);
|
||||
// arrow right
|
||||
getdisplay().drawLine(c.x + r - 4, c.y, c.x + r - 10, c.y - 4, commonData->fgcolor);
|
||||
getdisplay().drawLine(c.x + r - 4, c.y, c.x + r - 10, c.y + 4, commonData->fgcolor);
|
||||
getdisplay().setFont(&Ubuntu_Bold8pt8b);
|
||||
drawTextCenter(c.x + r / 2, c.y + 8, String(scale, 0) + "m");
|
||||
|
||||
// draw anchor symbol (as bitmap)
|
||||
getdisplay().drawXBitmap(c.x - anchor_width / 2, c.y - anchor_height / 2,
|
||||
anchor_bits, anchor_width, anchor_height, commonData->fgcolor);
|
||||
|
||||
}
|
||||
|
||||
void displayModeConfig() {
|
||||
|
||||
getdisplay().setTextColor(commonData->fgcolor);
|
||||
getdisplay().setFont(&Ubuntu_Bold12pt8b);
|
||||
getdisplay().setCursor(8, 48);
|
||||
getdisplay().print("Anchor configuration");
|
||||
|
||||
}
|
||||
|
||||
public:
|
||||
PageAnchor(CommonData &common)
|
||||
{
|
||||
commonData = &common;
|
||||
common.logger->logDebug(GwLog::LOG,"Instantiate PageAnchor");
|
||||
|
||||
server_name = common.config->getString(common.config->mapServer);
|
||||
tile_path = common.config->getString(common.config->mapTilePath);
|
||||
|
||||
lengthformat = common.config->getString(common.config->lengthFormat);
|
||||
chain_length = common.config->getInt(common.config->chainLength);
|
||||
|
||||
canvas = new GFXcanvas1(264, 260); // Byte aligned, no padding!
|
||||
}
|
||||
|
||||
void setupKeys(){
|
||||
Page::setupKeys();
|
||||
commonData->keydata[0].label = "MODE";
|
||||
#ifdef BOARD_OBP40S3
|
||||
commonData->keydata[1].label = "DROP";
|
||||
#endif
|
||||
#ifdef BOARD_OBP60S3
|
||||
commonData->keydata[4].label = "DROP";
|
||||
#endif
|
||||
}
|
||||
|
||||
// TODO OBP40 / OBP60 different handling
|
||||
int handleKey(int key) {
|
||||
if (key == 1) { // Switch between normal and config mode
|
||||
if (mode == 'N') {
|
||||
mode = 'C';
|
||||
commonData->keydata[1].label = "EDIT";
|
||||
} else {
|
||||
mode = 'N';
|
||||
#ifdef BOARD_OBP40S3
|
||||
commonData->keydata[1].label = anchor_set ? "RAISE": "DROP";
|
||||
#endif
|
||||
#ifdef BOARD_OBP60S3
|
||||
commonData->keydata[4].label = anchor_set ? "RAISE": "DROP";
|
||||
#endif
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (key == 2) {
|
||||
anchor_set = !anchor_set;
|
||||
commonData->keydata[1].label = anchor_set ? "RAISE": "DROP";
|
||||
return 0;
|
||||
}
|
||||
// Code for keylock
|
||||
if (key == 11){
|
||||
commonData->keylock = !commonData->keylock;
|
||||
return 0;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
int rhumb(double lat1, double lon1, double lat2, double lon2) {
|
||||
// calc distance in m between two geo points
|
||||
static const double degToRad = M_PI / 180.0;
|
||||
lat1 = degToRad * lat1;
|
||||
lon1 = degToRad * lon1;
|
||||
lat2 = degToRad * lat2;
|
||||
lon2 = degToRad * lon2;
|
||||
double dlon = lon2 - lon1;
|
||||
double dlat = lat2 - lat1;
|
||||
double mlat = (lat1 + lat2) / 2;
|
||||
return (int) (6371000 * sqrt(pow(dlat, 2) + pow(cos(mlat) * dlon, 2)));
|
||||
}
|
||||
|
||||
bool getBackgroundMap(double lat, double lon, uint8_t zoom) {
|
||||
// HTTP-Request for map
|
||||
// TODO über pagedata -> status abfragen?
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
return false;
|
||||
}
|
||||
bool valid = false;
|
||||
HTTPClient http;
|
||||
String url = "http://" + server_name + "/" + tile_path;
|
||||
String parameter = "?lat=" + String(lat, 6) + "&lon=" + String(lon, 6)+ "&zoom=" + String(zoom)
|
||||
+ "&width=" + String(map_width) + "&height=" + String(map_height);
|
||||
commonData->logger->logDebug(GwLog::LOG, "HTTP query: %s", String(url + parameter).c_str());
|
||||
http.begin(url + parameter);
|
||||
// http.SetAcceptEncoding("gzip");
|
||||
// TODO miniz.c from ROM
|
||||
int httpCode = http.GET();
|
||||
if (httpCode > 0) {
|
||||
if (httpCode == HTTP_CODE_OK) {
|
||||
WiFiClient* stream = http.getStreamPtr();
|
||||
int size = http.getSize();
|
||||
commonData->logger->logDebug(GwLog::LOG, "HTTP get size: %d", size);
|
||||
// header: P4<LF><width> <height><LF> (e.g. 11 byte)
|
||||
uint8_t header[14]; // max: P4<LF>wwww wwww<LF>
|
||||
bool header_read = false;
|
||||
int header_size = 0;
|
||||
uint8_t* buf = canvas->getBuffer();
|
||||
int n = 0;
|
||||
int ix = 0;
|
||||
while (stream->available()) {
|
||||
uint8_t b = stream->read();
|
||||
n += 1;
|
||||
if ((! header_read) and (n < 13) ) {
|
||||
header[n-1] = b;
|
||||
if ((n > 3) and (b == 0x0a)) {
|
||||
header_read = true;
|
||||
header_size = n;
|
||||
header[n] = 0;
|
||||
}
|
||||
} else {
|
||||
// write image data to canvas buffer
|
||||
buf[ix++] = b;
|
||||
}
|
||||
}
|
||||
if (n == size) {
|
||||
valid = true;
|
||||
}
|
||||
commonData->logger->logDebug(GwLog::LOG, "HTTP: final bytesRead=%d, header-size=%d", n, header_size);
|
||||
} else {
|
||||
commonData->logger->logDebug(GwLog::LOG, "HTTP result #%d", httpCode);
|
||||
}
|
||||
} else {
|
||||
commonData->logger->logDebug(GwLog::ERROR, "HTTP error #%d", httpCode);
|
||||
}
|
||||
http.end();
|
||||
return valid;
|
||||
}
|
||||
|
||||
void displayNew(PageData &pageData){
|
||||
|
||||
GwApi::BoatValue *bv_lat = pageData.values[4]; // LAT
|
||||
GwApi::BoatValue *bv_lon = pageData.values[5]; // LON
|
||||
|
||||
// check if valid data available
|
||||
if (!bv_lat->valid or !bv_lon->valid) {
|
||||
map_valid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
map_lat = bv_lat->value; // save for later comparison
|
||||
map_lon = bv_lon->value;
|
||||
map_valid = getBackgroundMap(map_lat, map_lon, zoom);
|
||||
|
||||
if (map_valid) {
|
||||
// prepare visible space for anchor-symbol or boat
|
||||
canvas->fillCircle(132, 130, 10, commonData->fgcolor);
|
||||
}
|
||||
};
|
||||
|
||||
int displayPage(PageData &pageData) {
|
||||
GwLog *logger = commonData->logger;
|
||||
|
||||
// Logging boat values
|
||||
logger->logDebug(GwLog::LOG, "Drawing at PageAnchor; Mode=%c", mode);
|
||||
|
||||
// Set display in partial refresh mode
|
||||
getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update
|
||||
|
||||
if (mode == 'N') {
|
||||
displayModeNormal(pageData);
|
||||
} else if (mode == 'C') {
|
||||
displayModeConfig();
|
||||
}
|
||||
|
||||
return PAGE_UPDATE;
|
||||
};
|
||||
};
|
||||
|
||||
static Page *createPage(CommonData &common){
|
||||
return new PageAnchor(common);
|
||||
}
|
||||
|
||||
/**
|
||||
* with the code below we make this page known to the PageTask
|
||||
* we give it a type (name) that can be selected in the config
|
||||
* we define which function is to be called
|
||||
* and we provide the number of user parameters we expect
|
||||
* this will be number of BoatValue pointers in pageData.values
|
||||
*/
|
||||
PageDescription registerPageAnchor(
|
||||
"Anchor", // Page name
|
||||
createPage, // Action
|
||||
0, // Number of bus values depends on selection in Web configuration
|
||||
{"DBS", "HDT", "AWS", "AWD", "LAT", "LON", "HDOP"}, // Names of bus values undepends on selection in Web configuration (refer GwBoatData.h)
|
||||
true // Show display header on/off
|
||||
);
|
||||
|
||||
#endif
|
||||
@@ -19,6 +19,28 @@
|
||||
"obp60": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "mapServer",
|
||||
"label": "map server",
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Server for converting map tiles. Use only one hostname or IP address",
|
||||
"category": "wifi client",
|
||||
"capabilities": {
|
||||
"obp40": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "mapTilePath",
|
||||
"label": "map tile path",
|
||||
"type": "string",
|
||||
"default": "map.php",
|
||||
"description": "Path to converter access e.g. index.php or map.php",
|
||||
"category": "wifi client",
|
||||
"capabilities": {
|
||||
"obp40": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "timeZone",
|
||||
"label": "Time Zone",
|
||||
@@ -75,6 +97,20 @@
|
||||
"obp60":"true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "chainLength",
|
||||
"label": "Anchor Chain Length [m]",
|
||||
"type": "number",
|
||||
"default": "0",
|
||||
"check": "checkMinMax",
|
||||
"min": 0,
|
||||
"max": 255,
|
||||
"description": "The length of the anchor chain [0...255m]",
|
||||
"category": "OBP60 Settings",
|
||||
"capabilities": {
|
||||
"obp60":"true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "fuelTank",
|
||||
"label": "Fuel Tank [l]",
|
||||
@@ -1212,7 +1248,6 @@
|
||||
"obp60":"true"
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"name": "page1type",
|
||||
"label": "Type",
|
||||
@@ -1220,6 +1255,7 @@
|
||||
"default": "Voltage",
|
||||
"description": "Type of page for page 1",
|
||||
"list": [
|
||||
"Anchor",
|
||||
"BME280",
|
||||
"Battery",
|
||||
"Battery2",
|
||||
@@ -1518,6 +1554,7 @@
|
||||
"default": "WindRose",
|
||||
"description": "Type of page for page 2",
|
||||
"list": [
|
||||
"Anchor",
|
||||
"BME280",
|
||||
"Battery",
|
||||
"Battery2",
|
||||
@@ -1808,6 +1845,7 @@
|
||||
"default": "OneValue",
|
||||
"description": "Type of page for page 3",
|
||||
"list": [
|
||||
"Anchor",
|
||||
"BME280",
|
||||
"Battery",
|
||||
"Battery2",
|
||||
@@ -2090,6 +2128,7 @@
|
||||
"default": "TwoValues",
|
||||
"description": "Type of page for page 4",
|
||||
"list": [
|
||||
"Anchor",
|
||||
"BME280",
|
||||
"Battery",
|
||||
"Battery2",
|
||||
@@ -2364,6 +2403,7 @@
|
||||
"default": "ThreeValues",
|
||||
"description": "Type of page for page 5",
|
||||
"list": [
|
||||
"Anchor",
|
||||
"BME280",
|
||||
"Battery",
|
||||
"Battery2",
|
||||
@@ -2630,6 +2670,7 @@
|
||||
"default": "FourValues",
|
||||
"description": "Type of page for page 6",
|
||||
"list": [
|
||||
"Anchor",
|
||||
"BME280",
|
||||
"Battery",
|
||||
"Battery2",
|
||||
@@ -2888,6 +2929,7 @@
|
||||
"default": "FourValues2",
|
||||
"description": "Type of page for page 7",
|
||||
"list": [
|
||||
"Anchor",
|
||||
"BME280",
|
||||
"Battery",
|
||||
"Battery2",
|
||||
@@ -3138,6 +3180,7 @@
|
||||
"default": "Clock",
|
||||
"description": "Type of page for page 8",
|
||||
"list": [
|
||||
"Anchor",
|
||||
"BME280",
|
||||
"Battery",
|
||||
"Battery2",
|
||||
@@ -3380,6 +3423,7 @@
|
||||
"default": "RollPitch",
|
||||
"description": "Type of page for page 9",
|
||||
"list": [
|
||||
"Anchor",
|
||||
"BME280",
|
||||
"Battery",
|
||||
"Battery2",
|
||||
@@ -3614,6 +3658,7 @@
|
||||
"default": "Battery2",
|
||||
"description": "Type of page for page 10",
|
||||
"list": [
|
||||
"Anchor",
|
||||
"BME280",
|
||||
"Battery",
|
||||
"Battery2",
|
||||
|
||||
@@ -19,6 +19,28 @@
|
||||
"obp40": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "mapServer",
|
||||
"label": "map server",
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Server for converting map tiles. Use only one hostname or IP address",
|
||||
"category": "wifi client",
|
||||
"capabilities": {
|
||||
"obp40": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "mapTilePath",
|
||||
"label": "map tile path",
|
||||
"type": "string",
|
||||
"default": "map.php",
|
||||
"description": "Path to converter access e.g. index.php or map.php",
|
||||
"category": "wifi client",
|
||||
"capabilities": {
|
||||
"obp40": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "timeZone",
|
||||
"label": "Time Zone",
|
||||
@@ -75,6 +97,20 @@
|
||||
"obp40": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "chainLength",
|
||||
"label": "Anchor Chain Length [m]",
|
||||
"type": "number",
|
||||
"default": "0",
|
||||
"check": "checkMinMax",
|
||||
"min": 0,
|
||||
"max": 255,
|
||||
"description": "The length of the anchor chain [0...255m]",
|
||||
"category": "OBP40 Settings",
|
||||
"capabilities": {
|
||||
"obp40":"true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "fuelTank",
|
||||
"label": "Fuel Tank [l]",
|
||||
@@ -1243,6 +1279,7 @@
|
||||
"default": "Voltage",
|
||||
"description": "Type of page for page 1",
|
||||
"list": [
|
||||
"Anchor",
|
||||
"BME280",
|
||||
"Battery",
|
||||
"Battery2",
|
||||
@@ -1571,6 +1608,7 @@
|
||||
"default": "WindRose",
|
||||
"description": "Type of page for page 2",
|
||||
"list": [
|
||||
"Anchor",
|
||||
"BME280",
|
||||
"Battery",
|
||||
"Battery2",
|
||||
@@ -1890,6 +1928,7 @@
|
||||
"default": "OneValue",
|
||||
"description": "Type of page for page 3",
|
||||
"list": [
|
||||
"Anchor",
|
||||
"BME280",
|
||||
"Battery",
|
||||
"Battery2",
|
||||
@@ -2200,6 +2239,7 @@
|
||||
"default": "TwoValues",
|
||||
"description": "Type of page for page 4",
|
||||
"list": [
|
||||
"Anchor",
|
||||
"BME280",
|
||||
"Battery",
|
||||
"Battery2",
|
||||
@@ -2501,6 +2541,7 @@
|
||||
"default": "ThreeValues",
|
||||
"description": "Type of page for page 5",
|
||||
"list": [
|
||||
"Anchor",
|
||||
"BME280",
|
||||
"Battery",
|
||||
"Battery2",
|
||||
@@ -2793,6 +2834,7 @@
|
||||
"default": "FourValues",
|
||||
"description": "Type of page for page 6",
|
||||
"list": [
|
||||
"Anchor",
|
||||
"BME280",
|
||||
"Battery",
|
||||
"Battery2",
|
||||
@@ -3076,6 +3118,7 @@
|
||||
"default": "FourValues2",
|
||||
"description": "Type of page for page 7",
|
||||
"list": [
|
||||
"Anchor",
|
||||
"BME280",
|
||||
"Battery",
|
||||
"Battery2",
|
||||
@@ -3350,6 +3393,7 @@
|
||||
"default": "Clock",
|
||||
"description": "Type of page for page 8",
|
||||
"list": [
|
||||
"Anchor",
|
||||
"BME280",
|
||||
"Battery",
|
||||
"Battery2",
|
||||
@@ -3615,6 +3659,7 @@
|
||||
"default": "RollPitch",
|
||||
"description": "Type of page for page 9",
|
||||
"list": [
|
||||
"Anchor",
|
||||
"BME280",
|
||||
"Battery",
|
||||
"Battery2",
|
||||
@@ -3871,6 +3916,7 @@
|
||||
"default": "Battery2",
|
||||
"description": "Type of page for page 10",
|
||||
"list": [
|
||||
"Anchor",
|
||||
"BME280",
|
||||
"Battery",
|
||||
"Battery2",
|
||||
|
||||
@@ -85,7 +85,7 @@ void OBP60Init(GwApi *api){
|
||||
// Get CPU speed
|
||||
int freq = getCpuFrequencyMhz();
|
||||
logger->logDebug(GwLog::LOG,"CPU speed at boot: %i MHz", freq);
|
||||
|
||||
|
||||
// Settings for backlight
|
||||
String backlightMode = api->getConfig()->getConfigItem(api->getConfig()->backlight,true)->asString();
|
||||
logger->logDebug(GwLog::DEBUG,"Backlight Mode is: %s", backlightMode.c_str());
|
||||
@@ -223,7 +223,7 @@ void registerAllPages(PageList &list){
|
||||
extern PageDescription registerPageWind;
|
||||
list.add(®isterPageWind);
|
||||
extern PageDescription registerPageWindPlot;
|
||||
list.add(®isterPageWindPlot);
|
||||
list.add(®isterPageWindPlot);
|
||||
extern PageDescription registerPageWindRose;
|
||||
list.add(®isterPageWindRose);
|
||||
extern PageDescription registerPageWindRoseFlex;
|
||||
@@ -260,6 +260,8 @@ void registerAllPages(PageList &list){
|
||||
list.add(®isterPageFluid);
|
||||
extern PageDescription registerPageSkyView;
|
||||
list.add(®isterPageSkyView);
|
||||
extern PageDescription registerPageAnchor;
|
||||
list.add(®isterPageAnchor);
|
||||
}
|
||||
|
||||
// Undervoltage detection for shutdown display
|
||||
@@ -344,8 +346,6 @@ void OBP60Task(GwApi *api){
|
||||
|
||||
tN2kMsg N2kMsg;
|
||||
|
||||
QueueHandle_t keyboardQueue = api->getKbQueue();
|
||||
|
||||
LOG_DEBUG(GwLog::LOG,"obp60task started");
|
||||
for (auto it=allPages.pages.begin();it != allPages.pages.end();it++){
|
||||
LOG_DEBUG(GwLog::LOG,"found registered page %s",(*it)->pageName.c_str());
|
||||
@@ -404,7 +404,7 @@ void OBP60Task(GwApi *api){
|
||||
getdisplay().nextPage(); // Fast Refresh
|
||||
getdisplay().nextPage(); // Fast Refresh
|
||||
}
|
||||
|
||||
|
||||
// Init pages
|
||||
int numPages=1;
|
||||
PageStruct pages[MAX_PAGE_NUMBER];
|
||||
@@ -475,7 +475,7 @@ void OBP60Task(GwApi *api){
|
||||
for (auto it=description->fixedParam.begin();it != description->fixedParam.end();it++){
|
||||
GwApi::BoatValue *value=boatValues.findValueOrCreate(*it);
|
||||
LOG_DEBUG(GwLog::DEBUG,"added fixed value %s to page %d",value->getName().c_str(),i);
|
||||
pages[i].parameters.values.push_back(value);
|
||||
pages[i].parameters.values.push_back(value);
|
||||
}
|
||||
// Add boat history data to page parameters
|
||||
pages[i].parameters.boatHstry = &hstryBufList;
|
||||
@@ -551,11 +551,11 @@ void OBP60Task(GwApi *api){
|
||||
GwApi::BoatValue *hdop = boatValues.findValueOrCreate("HDOP"); // Load GpsHDOP
|
||||
|
||||
LOG_DEBUG(GwLog::LOG,"obp60task: start mainloop");
|
||||
|
||||
|
||||
commonData.time = boatValues.findValueOrCreate("GPST"); // Load GpsTime
|
||||
commonData.date = boatValues.findValueOrCreate("GPSD"); // Load GpsTime
|
||||
bool delayedDisplayUpdate = false; // If select a new pages then make a delayed full display update
|
||||
bool cpuspeedsetted = false; // Marker for change CPU speed
|
||||
bool cpuspeedsetted = false; // Marker for change CPU speed
|
||||
long firststart = millis(); // First start
|
||||
long starttime0 = millis(); // Mainloop
|
||||
long starttime1 = millis(); // Full display refresh for the first 5 min (more often as normal)
|
||||
@@ -584,7 +584,7 @@ void OBP60Task(GwApi *api){
|
||||
}
|
||||
}
|
||||
|
||||
// Set CPU speed after boot after 1min
|
||||
// Set CPU speed after boot after 1min
|
||||
if(millis() > firststart + (1 * 60 * 1000) && cpuspeedsetted == false){
|
||||
if(String(cpuspeed) == "80"){
|
||||
setCpuFrequencyMhz(80);
|
||||
@@ -605,7 +605,7 @@ void OBP60Task(GwApi *api){
|
||||
commonData.data=shared->getSensorData();
|
||||
commonData.data.actpage = pageNumber + 1;
|
||||
commonData.data.maxpage = numPages;
|
||||
|
||||
|
||||
// If GPS fix then LED off (HDOP)
|
||||
if(String(gpsFix) == "GPS Fix Lost" && hdop->value <= hdopAccuracy && hdop->valid == true){
|
||||
setFlashLED(false);
|
||||
@@ -614,18 +614,9 @@ void OBP60Task(GwApi *api){
|
||||
if((String(gpsFix) == "GPS Fix Lost" && hdop->value > hdopAccuracy && hdop->valid == true) || (String(gpsFix) == "GPS Fix Lost" && hdop->valid == false)){
|
||||
setFlashLED(true);
|
||||
}
|
||||
|
||||
// Keyboard messages from remote
|
||||
uint8_t remotekey = 0;
|
||||
if (xQueueReceive(keyboardQueue, &remotekey, 0) == pdPASS) {
|
||||
LOG_DEBUG(GwLog::LOG, "OBP received remote key: %d", remotekey);
|
||||
// inject into internal keyboard queue
|
||||
xQueueSend(allParameters.queue, &remotekey, 0);
|
||||
}
|
||||
|
||||
|
||||
// Check the keyboard message
|
||||
int keyboardMessage=0;
|
||||
|
||||
while (xQueueReceive(allParameters.queue,&keyboardMessage,0)){
|
||||
LOG_DEBUG(GwLog::LOG,"new key from keyboard %d",keyboardMessage);
|
||||
keypressed = true;
|
||||
@@ -688,13 +679,13 @@ void OBP60Task(GwApi *api){
|
||||
commonData.data.actpage = pageNumber + 1;
|
||||
commonData.data.maxpage = numPages;
|
||||
}
|
||||
|
||||
|
||||
// #9 or #10 Refresh display after a new page after 4s waiting time and if refresh is disabled
|
||||
if(refreshmode == true && (keyboardMessage == 9 || keyboardMessage == 10 || keyboardMessage == 4 || keyboardMessage == 3)){
|
||||
starttime4 = millis();
|
||||
starttime2 = millis(); // Reset the timer for full display update
|
||||
delayedDisplayUpdate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
LOG_DEBUG(GwLog::LOG,"set pagenumber to %d",pageNumber);
|
||||
}
|
||||
@@ -720,7 +711,7 @@ void OBP60Task(GwApi *api){
|
||||
commonData.sundata = calcSunsetSunriseRTC(&commonData.data.rtcTime, homelat, homelon, tz);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Full display update afer a new selected page and 8s wait time
|
||||
if(millis() > starttime4 + 8000 && delayedDisplayUpdate == true){
|
||||
starttime1 = millis();
|
||||
@@ -797,8 +788,8 @@ void OBP60Task(GwApi *api){
|
||||
// getdisplay().nextPage(); // Partial update
|
||||
// getdisplay().nextPage(); // Partial update
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Refresh display data, default all 1s
|
||||
currentPage = pages[pageNumber].page;
|
||||
int pagetime = 1000;
|
||||
|
||||
@@ -41,6 +41,8 @@ lib_deps =
|
||||
milesburton/DallasTemperature@3.11.0
|
||||
signetica/SunRise@2.0.2
|
||||
adafruit/Adafruit FRAM I2C@2.0.3
|
||||
WifiClientSecure
|
||||
HTTPClient
|
||||
build_flags=
|
||||
#https://thingpulse.com/usb-settings-for-logging-with-the-esp32-s3-in-platformio/?srsltid=AfmBOopGskbkr4GoeVkNlFaZXe_zXkLceKF6Rn-tmoXABCeAR2vWsdHL
|
||||
# -D CORE_DEBUG_LEVEL=1 #Debug level for CPU core via CDC (serial device)
|
||||
@@ -93,6 +95,8 @@ lib_deps =
|
||||
milesburton/DallasTemperature@3.11.0
|
||||
signetica/SunRise@2.0.2
|
||||
adafruit/Adafruit FRAM I2C@2.0.3
|
||||
WifiClientSecure
|
||||
HTTPClient
|
||||
build_flags=
|
||||
-D DISABLE_DIAGNOSTIC_OUTPUT #Disable diagnostic output for GxEPD2 lib
|
||||
-D BOARD_OBP40S3 #Board OBP40 with ESP32S3
|
||||
|
||||
@@ -189,10 +189,6 @@ public:
|
||||
{
|
||||
return api->getQueue();
|
||||
}
|
||||
virtual QueueHandle_t getKbQueue()
|
||||
{
|
||||
return api->getKbQueue();
|
||||
}
|
||||
virtual void sendN2kMessage(const tN2kMsg &msg,bool convert)
|
||||
{
|
||||
GWSYNCHRONIZED(mainLock);
|
||||
@@ -432,4 +428,4 @@ void GwUserCode::handleWebRequest(const String &url,AsyncWebServerRequest *req){
|
||||
}
|
||||
LOG_DEBUG(GwLog::DEBUG,"no task found for web request %s[%s]",url.c_str(),tname.c_str());
|
||||
req->send(404, "text/plain", "not found");
|
||||
}
|
||||
}
|
||||
11
src/main.cpp
11
src/main.cpp
@@ -158,8 +158,6 @@ GwCounter<unsigned long> countNMEA2KIn("countNMEA2000in");
|
||||
GwCounter<unsigned long> countNMEA2KOut("countNMEA2000out");
|
||||
GwIntervalRunner timers;
|
||||
|
||||
QueueHandle_t keyboardQueue = NULL;
|
||||
|
||||
bool checkPass(String hash){
|
||||
return config.checkPass(hash);
|
||||
}
|
||||
@@ -271,10 +269,6 @@ public:
|
||||
{
|
||||
return &mainQueue;
|
||||
}
|
||||
virtual QueueHandle_t getKbQueue()
|
||||
{
|
||||
return keyboardQueue;
|
||||
}
|
||||
virtual void sendN2kMessage(const tN2kMsg &msg,bool convert)
|
||||
{
|
||||
handleN2kMessage(msg,sourceId,!convert);
|
||||
@@ -866,8 +860,6 @@ void setup() {
|
||||
webserver.begin();
|
||||
xdrMappings.begin();
|
||||
logger.flush();
|
||||
// remote keyboard support
|
||||
keyboardQueue = xQueueCreate(10, sizeof(uint8_t));
|
||||
GwConverterConfig converterConfig;
|
||||
converterConfig.init(&config,&logger);
|
||||
nmea0183Converter= N2kDataToNMEA0183::create(&logger, &boatData,
|
||||
@@ -877,8 +869,7 @@ void setup() {
|
||||
,
|
||||
config.getString(config.talkerId,String("GP")),
|
||||
&xdrMappings,
|
||||
converterConfig,
|
||||
keyboardQueue
|
||||
converterConfig
|
||||
);
|
||||
|
||||
toN2KConverter= NMEA0183DataToN2K::create(&logger,&boatData,[](const tN2kMsg &msg, int sourceId)->bool{
|
||||
|
||||
@@ -88,16 +88,6 @@
|
||||
"description":"the brightness of the led (0..255)",
|
||||
"category":"system"
|
||||
},
|
||||
{
|
||||
"name": "swBankInstance",
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 252,
|
||||
"check": "checkMinMax",
|
||||
"description": "the instance of switch bank to receive data for (0..252)",
|
||||
"category": "system"
|
||||
},
|
||||
{
|
||||
"name": "talkerId",
|
||||
"label": "NMEA0183 ID",
|
||||
@@ -1297,4 +1287,4 @@
|
||||
"check": "checkXDR",
|
||||
"category": "xdr30"
|
||||
}
|
||||
]
|
||||
]
|
||||
Reference in New Issue
Block a user