From 9b9bf76e4d8bec7014db4ce8e5c028659ad11f30 Mon Sep 17 00:00:00 2001 From: Thomas Hooge Date: Mon, 3 Nov 2025 20:04:31 +0100 Subject: [PATCH] Added page anchor with background map --- lib/obp60task/PageAnchor.cpp | 402 ++++++++++++++++++++++++++++++++ lib/obp60task/config.json | 47 +++- lib/obp60task/config_obp40.json | 46 ++++ lib/obp60task/obp60task.cpp | 2 + lib/obp60task/platformio.ini | 4 + 5 files changed, 500 insertions(+), 1 deletion(-) create mode 100644 lib/obp60task/PageAnchor.cpp diff --git a/lib/obp60task/PageAnchor.cpp b/lib/obp60task/PageAnchor.cpp new file mode 100644 index 0000000..11d3425 --- /dev/null +++ b/lib/obp60task/PageAnchor.cpp @@ -0,0 +1,402 @@ +// 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 +#include +#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; + + 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 + if (map_valid) { + 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 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) { + drawPoly(rotatePoints(c, pts_boat, RadToDeg(bv_hdt->value)), commonData->fgcolor); + } else { + // no heading available draw north oriented + drawPoly(pts_boat, commonData->fgcolor); + } + + // Draw wind arrow + const std::vector 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("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(rhumb(map_lat, map_lon, bv_lat->value, bv_lon->value))); + } 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) + "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 + } + + 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'; + commonData->keydata[1].label = "ALARM"; + } + 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) + + "&w=" + String(map_width) + "&h=" + String(map_height); + commonData->logger->logDebug(GwLog::LOG, "HTTP query: %s", String(url + parameter).c_str()); + http.begin(url + parameter); + 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 (e.g. 11 byte) + uint8_t header[14]; // max: P4wwww wwww + 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); + } + 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); + }; + + 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 diff --git a/lib/obp60task/config.json b/lib/obp60task/config.json index f633762..5534391 100644 --- a/lib/obp60task/config.json +++ b/lib/obp60task/config.json @@ -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", diff --git a/lib/obp60task/config_obp40.json b/lib/obp60task/config_obp40.json index 9addd67..bd92fb5 100644 --- a/lib/obp60task/config_obp40.json +++ b/lib/obp60task/config_obp40.json @@ -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", diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index b1c0e01..f6d1695 100644 --- a/lib/obp60task/obp60task.cpp +++ b/lib/obp60task/obp60task.cpp @@ -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 diff --git a/lib/obp60task/platformio.ini b/lib/obp60task/platformio.ini index 16cd23c..7b8e2e1 100644 --- a/lib/obp60task/platformio.ini +++ b/lib/obp60task/platformio.ini @@ -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