1
0
mirror of https://github.com/thooge/esp32-nmea2000-obp60.git synced 2026-03-28 18:06:37 +01:00

More work on page anchor

This commit is contained in:
2026-03-08 19:52:51 +01:00
parent 608b782b43
commit 6d052f8827
9 changed files with 668 additions and 107 deletions

View File

@@ -8,36 +8,40 @@
Boatdata used
DBS - Water depth
HDT - Boat heading
HDT - Boat heading, true
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
HDOP - Position error, horizontal
Drop / raise function in device OBP40 has to be done inside
config mode because of limited number of buttons.
Raise function in device OBP40 has to be done inside config mode
because of limited number of buttons.
TODO
gzip for data transfer,
miniz.c from ROM?
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)
windlass integration
chain counter
Map service options / URL parameters
- mandatory
lat: latitude
lon: longitude
width: image width in px
height: image height in px
width: image width in px (400)
height: image height in px (300)
- optional
zoom: zoom level, default 15
mrot: map rotation angle in degrees
mtype: map type, default="Open Street Map"
dtype: dithering type, default="Atkinson"
cutout: image cutout type 0=none
alpha: alpha blending for cutout
tab: tab size, 0=none
border: border line zize in px, default 2
symbol: synmol number, default=2 triangle
@@ -51,6 +55,12 @@
#include <HTTPClient.h>
#include "Pagedata.h"
#include "OBP60Extensions.h"
#include "ConfigMenu.h"
// #include "miniz.h" // devices without PSRAM use <rom/miniz.h>
// extern "C" {
#include "rom/miniz.h"
// }
#define anchor_width 16
#define anchor_height 16
@@ -64,6 +74,7 @@ class PageAnchor : public Page
private:
char mode = 'N'; // (N)ormal, (C)onfig
int8_t editmode = -1; // marker for menu/edit/set function
ConfigMenu *menu;
//uint8_t *mapbuf = new uint8_t[10000]; // 8450 Byte without header
//int mapbuf_size = 10000;
@@ -81,7 +92,7 @@ private:
String lengthformat;
double scale = 50; // Radius of display circle in meter, depends on lat
double scale = 50; // Radius of display circle in meter, depends on lat
uint8_t zoom = 15; // map zoom level
bool alarm = false;
@@ -97,24 +108,38 @@ private:
double anchor_depth;
int anchor_ts; // time stamp anchor dropped
GwApi::BoatValue *bv_dbs; // depth below surface
GwApi::BoatValue *bv_hdt; // true heading
GwApi::BoatValue *bv_aws; // apparent wind speed
GwApi::BoatValue *bv_awd; // apparent wind direction
GwApi::BoatValue *bv_lat; // latitude, current
GwApi::BoatValue *bv_lon; // longitude, current
GwApi::BoatValue *bv_hdop; // horizontal position error
bool simulation = false;
int last_mapsize = 0;
String errmsg = "";
int loops;
int readbytes = 0;
void displayModeNormal(PageData &pageData) {
// Boatvalues: DBS, HDT, AWS, AWD, LAT, LON, HDOP
GwApi::BoatValue *bv_dbs = pageData.values[0]; // DBS
// get currrent boatvalues
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
bv_hdt = pageData.values[1]; // HDT
String sval_hdt = formatValue(bv_hdt, *commonData).svalue;
GwApi::BoatValue *bv_aws = pageData.values[2]; // AWS
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
bv_awd = pageData.values[3]; // AWD
String sval_awd = formatValue(bv_awd, *commonData).svalue;
GwApi::BoatValue *bv_lat = pageData.values[4]; // LAT
bv_lat = pageData.values[4]; // LAT
String sval_lat = formatValue(bv_lat, *commonData).svalue;
GwApi::BoatValue *bv_lon = pageData.values[5]; // LON
bv_lon = pageData.values[5]; // LON
String sval_lon = formatValue(bv_lon, *commonData).svalue;
GwApi::BoatValue *bv_hdop = pageData.values[6]; // HDOP
bv_hdop = pageData.values[6]; // HDOP
String sval_hdop = formatValue(bv_hdop, *commonData).svalue;
String sunit_hdop = formatValue(bv_hdop, *commonData).unit;
@@ -156,7 +181,7 @@ private:
{b.x + 5, b.y - 10},
{b.x + 5, b.y}
};
//rotatePoints und dann Linien zeichnen
//rotatePoints then draw lines
// TODO rotate boat according to current heading
if (bv_hdt->valid) {
if (map_valid) {
@@ -165,7 +190,7 @@ private:
}
drawPoly(rotatePoints(c, pts_boat, bv_hdt->value * RAD_TO_DEG), commonData->fgcolor);
} else {
// no heading available draw north oriented
// no heading available: draw north oriented
if (map_valid) {
getdisplay().fillCircle(b.x, b.y - 8, 10, commonData->bgcolor);
}
@@ -276,23 +301,54 @@ private:
getdisplay().setFont(&Ubuntu_Bold8pt8b);
getdisplay().setCursor(8, 250);
getdisplay().print("Press MODE to leave config");
getdisplay().setCursor(8, 260);
getdisplay().print("Press BACK to leave config");
getdisplay().setCursor(8, 68);
/* getdisplay().setCursor(8, 68);
getdisplay().printf("Server: %s", server_name.c_str());
getdisplay().setCursor(8, 88);
getdisplay().printf("Port: %d", server_port);
getdisplay().setCursor(8, 108);
getdisplay().printf("Tilepath: %s", tile_path.c_str());
getdisplay().setCursor(8, 128);
getdisplay().printf("Last mapsize: %d", last_mapsize);
getdisplay().setCursor(8, 148);
getdisplay().printf("Last error: %s", errmsg);
getdisplay().setCursor(8, 168);
getdisplay().printf("Loops: %d, Readbytes: %d", loops, readbytes);
*/
GwApi::BoatValue *bv_lat = pageData.values[4]; // LAT
GwApi::BoatValue *bv_lon = pageData.values[5]; // LON
if (!bv_lat->valid or !bv_lon->valid) {
getdisplay().setCursor(8, 128);
getdisplay().setCursor(8, 228);
getdisplay().printf("No valid position: background map disabled");
}
// Display menu
getdisplay().setFont(&Ubuntu_Bold8pt8b);
for (int i = 0 ; i < menu->getItemCount(); i++) {
ConfigMenuItem *itm = menu->getItemByIndex(i);
if (!itm) {
commonData->logger->logDebug(GwLog::ERROR, "Menu item not found: %d", i);
} else {
Rect r = menu->getItemRect(i);
bool inverted = (i == menu->getActiveIndex());
drawTextBoxed(r, itm->getLabel(), commonData->fgcolor, commonData->bgcolor, inverted, false);
if (inverted and editmode > 0) {
// triangle as edit marker
getdisplay().fillTriangle(r.x + r.w + 20, r.y, r.x + r.w + 30, r.y + r.h / 2, r.x + r.w + 20, r.y + r.h, commonData->fgcolor);
}
getdisplay().setCursor(r.x + r.w + 40, r.y + r.h - 4);
if (itm->getType() == "int") {
getdisplay().print(itm->getValue());
getdisplay().print(itm->getUnit());
} else {
getdisplay().print(itm->getValue() == 0 ? "No" : "Yes");
}
}
}
}
public:
@@ -321,12 +377,32 @@ public:
lengthformat = common.config->getString(common.config->lengthFormat);
chain_length = common.config->getInt(common.config->chainLength);
if (simulation) {
map_lat = 53.56938345759218;
map_lon = 9.679658234303275;
}
canvas = new GFXcanvas1(264, 260); // Byte aligned, no padding!
// Initialize config menu
menu = new ConfigMenu("Options", 40, 80);
menu->setItemDimension(150, 20);
ConfigMenuItem *newitem;
newitem = menu->addItem("chain", "Chain out", "int", 0, "m");
newitem->setRange(0, 200, {1, 2, 5, 10});
newitem = menu->addItem("alarm", "Alarm", "bool", 0, "");
newitem = menu->addItem("alarm", "Alarm range", "int", 50, "m");
newitem->setRange(0, 200, {1, 2, 5, 10});
newitem = menu->addItem("raise", "Raise Anchor", "bool", 0, "");
newitem = menu->addItem("zoom", "Zoom", "int", 15, "");
newitem->setRange(14, 17, {1});
menu->setItemActive("chain");
}
void setupKeys(){
Page::setupKeys();
commonData->keydata[0].label = "MODE";
commonData->keydata[0].label = "CFG";
#ifdef BOARD_OBP40S3
commonData->keydata[1].label = "DROP";
#endif
@@ -337,13 +413,18 @@ public:
// TODO OBP40 / OBP60 different handling
int handleKey(int key) {
commonData->logger->logDebug(GwLog::LOG, "Page Anchor handle key %d", key);
if (key == 1) { // Switch between normal and config mode
if (mode == 'N') {
mode = 'C';
#ifdef BOARD_OBP40S3
commonData->keydata[0].label = "BACK";
commonData->keydata[1].label = "EDIT";
#endif
} else {
mode = 'N';
#ifdef BOARD_OBP40S3
commonData->keydata[0].label = "CFG";
commonData->keydata[1].label = anchor_set ? "RAISE": "DROP";
#endif
#ifdef BOARD_OBP60S3
@@ -353,12 +434,58 @@ public:
return 0;
}
if (key == 2) {
anchor_set = !anchor_set;
commonData->keydata[1].label = anchor_set ? "RAISE": "DROP";
return 0;
if (mode == 'N') {
anchor_set = !anchor_set;
commonData->keydata[1].label = anchor_set ? "ALARM": "DROP";
if (anchor_set) {
anchor_lat = bv_lat->value;
anchor_lon = bv_lon->value;
anchor_depth = bv_dbs->value;
// TODO set timestamp
// anchor_ts =
}
return 0;
} else if (mode == 'C') {
// Change edit mode
if (editmode > 0) {
editmode = 0;
commonData->keydata[1].label = "EDIT";
} else {
editmode = 1;
commonData->keydata[1].label = "OK";
}
}
}
if (key == 9) {
// OBP40 Down
if (mode == 'C') {
if (editmode > 0) {
// decrease current menu item
menu->getActiveItem()->decValue();
} else {
// move to next menu item
menu->goNext();
}
return 0;
}
}
if (key == 10) {
// OBP40 Up
if (mode == 'C') {
if (editmode > 0) {
// increase current menu item
ConfigMenuItem *itm = menu->getActiveItem();
commonData->logger->logDebug(GwLog::LOG, "step = %d", itm->getStep());
itm->incValue();
} else {
// move to previous menu item
menu->goPrev();
}
return 0;
}
}
// Code for keylock
if (key == 11){
if (key == 11) {
commonData->keylock = !commonData->keylock;
return 0;
}
@@ -386,43 +513,185 @@ public:
}
bool valid = false;
HTTPClient http;
const char* headerKeys[] = { "Content-Encoding", "Content-Length" };
http.collectHeaders(headerKeys, 2);
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
http.addHeader("Accept-Encoding", "deflate");
int httpCode = http.GET();
if (httpCode > 0) {
commonData->logger->logDebug(GwLog::LOG, "HTTP GET result code: %d", httpCode);
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)
String encoding = http.header("Content-Encoding");
commonData->logger->logDebug(GwLog::LOG, "HTTP size: %d, encoding: '%s'", size, encoding);
bool is_gzip = encoding.equalsIgnoreCase("deflate");
uint8_t header[14]; // max: P4<LF>wwww wwww<LF>
bool header_read = false;
int header_size = 0;
uint8_t* buf = canvas->getBuffer();
bool header_read = false;
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;
uint8_t* buf = canvas->getBuffer();
if (is_gzip) {
/* gzip compressed data
* has to be decompressed into a buffer big enough
* to hold the whole data.
* so the PBM header is included
* search a method to use that as canvas without
* additional copy
*/
commonData->logger->logDebug(GwLog::LOG, "Map received in gzip encoding");
#define HEADER_MAX 24
#define HTTP_CHUNK 512
uint8_t in_buf[HTTP_CHUNK];
uint8_t header_buf[HEADER_MAX];
tinfl_decompressor decomp;
tinfl_init(&decomp);
size_t bitmap_written = 0;
size_t header_written = 0;
bool header_done = false;
int row_bytes = 0;
size_t expected_bitmap = 0;
while (stream->connected() || stream->available()) {
int bytes_read = stream->read(in_buf, HTTP_CHUNK);
if (bytes_read <= 0) break;
commonData->logger->logDebug(GwLog::LOG, "stream: bytes_read=%d", bytes_read);
size_t in_ofs = 0; // offset
while (in_ofs < (size_t)bytes_read) {
size_t in_size = bytes_read - in_ofs;
size_t out_size;
uint8_t *out_ptr;
uint8_t *out_ptr_next;
if (!header_done) {
if (header_written >= HEADER_MAX) {
commonData->logger->logDebug(GwLog::LOG, "PBM header too large");
return false;
}
out_ptr = header_buf + header_written;
out_size = HEADER_MAX - header_written;
} else {
out_ptr = buf + bitmap_written;
out_size = expected_bitmap - bitmap_written;
}
commonData->logger->logDebug(GwLog::LOG, "in_size=%d, out_size=%d", in_size, out_size);
// TODO correct loop !!!
// tinfl_status tinfl_decompress(
// tinfl_decompressor *r,
// const mz_uint8 *pIn_buf_next,
// size_t *pIn_buf_size,
// mz_uint8 *pOut_buf_start
// mz_uint8 *pOut_buf_next,
// size_t *pOut_buf_size,
// const mz_uint32 decomp_flags)
tinfl_status status = tinfl_decompress(
&decomp,
in_buf + in_ofs, // start address in input buffer
&in_size, // number of bytes to process
out_ptr, // start of output buffer
out_ptr, // next write position in output buffer
&out_size, // free size in output buffer
// TINFL_FLAG_PARSE_ZLIB_HEADER |
TINFL_FLAG_HAS_MORE_INPUT |
TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF
);
if (status < 0) {
commonData->logger->logDebug(GwLog::LOG, "Decompression error (%d)", status);
return false;
}
in_ofs += in_size;
commonData->logger->logDebug(GwLog::LOG, "in_size=%d, in_ofs=%d", in_size, in_ofs);
if (!header_done) {
commonData->logger->logDebug(GwLog::LOG, "Decoding header");
header_written += out_size;
// Detect header end: two '\n'
char *first_nl = strchr((char*)header_buf, '\n');
if (!first_nl) continue;
char *second_nl = strchr(first_nl + 1, '\n');
if (!second_nl) continue;
// Null-terminate header for sscanf
header_buf[header_written < HEADER_MAX ? header_written : HEADER_MAX - 1] = 0;
// Check magic
if (strncmp((char*)header_buf, "P4", 2) != 0) {
commonData->logger->logDebug(GwLog::LOG, "Invalid PBM magic");
return false;
}
// Parse width and height strictly
int header_width = 0, header_height = 0;
if (sscanf((char*)header_buf, "P4\n%d %d", &header_width, &header_height) != 2) {
commonData->logger->logDebug(GwLog::LOG, "Failed to parse PBM dimensions");
return false;
}
if (header_width != map_width || header_height != map_height) {
commonData->logger->logDebug(GwLog::LOG, "PBM size mismatch: header %dx%d, requested %dx%d\n",
header_width, header_height, map_width, map_height);
return false;
}
commonData->logger->logDebug(GwLog::LOG, "Header: %dx%d", header_width, header_height);
// Compute row bytes and expected bitmap size
row_bytes = (header_width + 7) / 8;
commonData->logger->logDebug(GwLog::LOG, "row_bytes=%d", row_bytes);
expected_bitmap = (size_t)row_bytes * header_height;
commonData->logger->logDebug(GwLog::LOG, "expected_bitmap=%d", expected_bitmap);
// Copy any extra decompressed bitmap after header
size_t header_size = (second_nl + 1) - (char*)header_buf;
commonData->logger->logDebug(GwLog::LOG, "header_size=%d", header_size);
size_t extra_bitmap = header_written - header_size;
commonData->logger->logDebug(GwLog::LOG, "extra bitmap=%d", extra_bitmap);
header_done = true;
if (extra_bitmap > 0) {
memcpy(buf, header_buf + header_size, extra_bitmap);
bitmap_written = extra_bitmap;
}
} else {
bitmap_written += out_size;
if (bitmap_written >= expected_bitmap) {
commonData->logger->logDebug(GwLog::LOG, "Image fully received");
}
}
commonData->logger->logDebug(GwLog::LOG, "bitmap_written=%d", bitmap_written);
}
} else {
// write image data to canvas buffer
buf[ix++] = b;
}
}
if (n == size) {
valid = true;
} else {
// uncompressed data
commonData->logger->logDebug(GwLog::LOG, "Map received uncompressed");
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 {
@@ -446,6 +715,8 @@ public:
return;
}
errmsg = "";
map_lat = bv_lat->value; // save for later comparison
map_lon = bv_lon->value;
map_valid = getBackgroundMap(map_lat, map_lon, zoom);
@@ -456,11 +727,14 @@ public:
}
};
void display_side_keys() {
// An rechter Seite neben dem Rad inc, dec, set etc ?
}
int displayPage(PageData &pageData) {
GwLog *logger = commonData->logger;
// Logging boat values
logger->logDebug(GwLog::LOG, "Drawing at PageAnchor; Mode=%c", mode);
commonData->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