From 6d052f8827d2b14d9e9f27f51cfdd0c7366d6b5d Mon Sep 17 00:00:00 2001 From: Thomas Hooge Date: Sun, 8 Mar 2026 19:52:51 +0100 Subject: [PATCH] More work on page anchor --- lib/obp60task/ConfigMenu.cpp | 217 +++++++++++++++++ lib/obp60task/ConfigMenu.h | 67 ++++++ lib/obp60task/OBP60Extensions.cpp | 31 ++- lib/obp60task/PageAnchor.cpp | 372 ++++++++++++++++++++++++++---- lib/obp60task/Pagedata.h | 1 + lib/obp60task/config_obp40.json | 23 -- lib/obp60task/config_obp60.json | 50 ++-- lib/obp60task/obp60task.cpp | 6 +- lib/obp60task/platformio.ini | 8 +- 9 files changed, 668 insertions(+), 107 deletions(-) create mode 100644 lib/obp60task/ConfigMenu.cpp create mode 100644 lib/obp60task/ConfigMenu.h diff --git a/lib/obp60task/ConfigMenu.cpp b/lib/obp60task/ConfigMenu.cpp new file mode 100644 index 0000000..56c6c64 --- /dev/null +++ b/lib/obp60task/ConfigMenu.cpp @@ -0,0 +1,217 @@ +/* + Menu system for online configuration + + A menu consists of a list of menuitems. + + Graphical representation is stored: + upper left corner: x, y + bounding box: + A menu consists of three columns + - menu text, if selected highlighted + - menu value with optional unit + - menu description or additional data for value + +*/ +#include "ConfigMenu.h" + +ConfigMenuItem::ConfigMenuItem(String itemtype, String itemlabel, uint16_t itemval, String itemunit) { + if (! (itemtype == "int" or itemtype == "bool")) { + valtype = "int"; + } else { + valtype = itemtype; + } + label = itemlabel; + min = 0; + max = std::numeric_limits::max(); + value = itemval; + unit = itemunit; + step = 1; +} + +void ConfigMenuItem::setRange(uint16_t valmin, uint16_t valmax, std::vector valsteps) { + min = valmin; + max = valmax; + steps = valsteps; + step = steps[0]; +}; + +bool ConfigMenuItem::checkRange(uint16_t checkval) { + return (checkval >= min) and (checkval <= max); +} + +String ConfigMenuItem::getLabel() { + return label; +}; + +uint16_t ConfigMenuItem::getValue() { + return value; +} + +bool ConfigMenuItem::setValue(uint16_t newval) { + if (valtype == "int") { + if (newval >= min and newval <= max) { + value = newval; + return true; + } + return false; // out of range + } else if (valtype == "bool") { + value = (newval != 0) ? 1 : 0; + return true; + } + return false; // invalid type +}; + +void ConfigMenuItem::incValue() { + // increase value by step + if (valtype == "int") { + if (value + step < max) { + value += step; + } else { + value = max; + } + } else if (valtype == "bool") { + value = !value; + } +}; + +void ConfigMenuItem::decValue() { + // decrease value by step + if (valtype == "int") { + if (value - step > min) { + value -= step; + } else { + value = min; + } + } else if (valtype == "bool") { + value = !value; + } +}; + +String ConfigMenuItem::getUnit() { + return unit; +} + +uint16_t ConfigMenuItem::getStep() { + return step; +} + +void ConfigMenuItem::setStep(uint16_t newstep) { + if (std::find(steps.begin(), steps.end(), newstep) == steps.end()) { + return; // invalid step: not in list of possible steps + } + step = newstep; +} + +int8_t ConfigMenuItem::getPos() { + return position; +}; + +void ConfigMenuItem::setPos(int8_t newpos) { + position = newpos; +}; + +String ConfigMenuItem::getType() { + return valtype; +} + +ConfigMenu::ConfigMenu(String menutitle, uint16_t menu_x, uint16_t menu_y) { + title = menutitle; + x = menu_x; + y = menu_y; +}; + +ConfigMenuItem* ConfigMenu::addItem(String key, String label, String valtype, uint16_t val, String valunit) { + if (items.find(key) != items.end()) { + // duplicate keys not allowed + return nullptr; + } + ConfigMenuItem *itm = new ConfigMenuItem(valtype, label, val, valunit); + items.insert(std::pair(key, itm)); + // Append key to index, index starting with 0 + int8_t ix = items.size() - 1; + index[ix] = key; + itm->setPos(ix); + return itm; +}; + +void ConfigMenu::setItemDimension(uint16_t itemwidth, uint16_t itemheight) { + w = itemwidth; + h = itemheight; +}; + +void ConfigMenu::setItemActive(String key) { + if (items.find(key) != items.end()) { + activeitem = items[key]->getPos(); + } else { + activeitem = -1; + } +}; + +int8_t ConfigMenu::getActiveIndex() { + return activeitem; +} + +ConfigMenuItem* ConfigMenu::getActiveItem() { + if (activeitem < 0) { + return nullptr; + } + return items[index[activeitem]]; +}; + +ConfigMenuItem* ConfigMenu::getItemByIndex(uint8_t ix) { + if (ix > index.size() - 1) { + return nullptr; + } + return items[index[ix]]; +}; + +ConfigMenuItem* ConfigMenu::getItemByKey(String key) { + if (items.find(key) == items.end()) { + return nullptr; + } + return items[key]; +}; + +uint8_t ConfigMenu::getItemCount() { + return items.size(); +}; + +void ConfigMenu::goPrev() { + if (activeitem == 0) { + activeitem = items.size() - 1; + } else { + activeitem--; + } +} + +void ConfigMenu::goNext() { + if (activeitem == items.size() - 1) { + activeitem = 0; + } else { + activeitem++; + } +} + +Point ConfigMenu::getXY() { + return {static_cast(x), static_cast(y)}; +} + +Rect ConfigMenu::getRect() { + return {static_cast(x), static_cast(y), + static_cast(w), static_cast(h)}; +} + +Rect ConfigMenu::getItemRect(int8_t index) { + return {static_cast(x), static_cast(y + index * h), + static_cast(w), static_cast(h)}; +} + +void ConfigMenu::setCallback(void (*callback)()) { + fptrCallback = callback; +} + +void ConfigMenu::storeValues() { + if (fptrCallback) { + fptrCallback(); + } +} diff --git a/lib/obp60task/ConfigMenu.h b/lib/obp60task/ConfigMenu.h new file mode 100644 index 0000000..5124d37 --- /dev/null +++ b/lib/obp60task/ConfigMenu.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include +#include "Graphics.h" // for Point and Rect + +class ConfigMenuItem { +private: + String label; + uint16_t value; + String unit; + String desc; // optional data to display + String valtype; // "int" | "bool" -> TODO "list" + uint16_t min; + uint16_t max; + std::vector steps; + uint16_t step; + int8_t position; // counted fom 0 + +public: + ConfigMenuItem(String itemtype, String itemlabel, uint16_t itemval, String itemunit); + void setRange(uint16_t valmin, uint16_t valmax, std::vector steps); + bool checkRange(uint16_t checkval); + String getLabel(); + uint16_t getValue(); + bool setValue(uint16_t newval); + void incValue(); + void decValue(); + String getUnit(); + uint16_t getStep(); + void setStep(uint16_t newstep); + int8_t getPos(); + void setPos(int8_t newpos); + String getType(); +}; + +class ConfigMenu { +private: + String title; + std::map items; + std::map index; + int8_t activeitem = -1; // refers to position of item + uint16_t x; + uint16_t y; + uint16_t w; + uint16_t h; + void (*fptrCallback)(); + +public: + ConfigMenu(String title, uint16_t menu_x, uint16_t menu_y); + ConfigMenuItem* addItem(String key, String label, String valtype, uint16_t val, String valunit); + void setItemDimension(uint16_t itemwidth, uint16_t itemheight); + int8_t getActiveIndex(); + void setItemActive(String key); + ConfigMenuItem* getActiveItem(); + ConfigMenuItem* getItemByIndex(uint8_t index); + ConfigMenuItem* getItemByKey(String key); + uint8_t getItemCount(); + void goPrev(); + void goNext(); + Point getXY(); + Rect getRect(); + Rect getItemRect(int8_t index); + void setCallback(void (*callback)()); + void storeValues(); +}; diff --git a/lib/obp60task/OBP60Extensions.cpp b/lib/obp60task/OBP60Extensions.cpp index 76c6334..0cce5bb 100644 --- a/lib/obp60task/OBP60Extensions.cpp +++ b/lib/obp60task/OBP60Extensions.cpp @@ -475,6 +475,30 @@ std::vector wordwrap(String &line, uint16_t maxwidth) { return lines; } +// Helper function to just get the exact width of a string +uint16_t getStringPixelWidth(const char* str, const GFXfont* font) { + int16_t minx = INT16_MAX; + int16_t maxx = INT16_MIN; + int16_t cursor_x = 0; + while (*str) { + char c = *str++; + if (c < font->first || c > font->last) { + continue; + } + GFXglyph* glyph = &font->glyph[c - font->first]; + if (glyph->width > 0) { + int16_t glyphStart = cursor_x + glyph->xOffset; + int16_t glyphEnd = glyphStart + glyph->width; + if (glyphStart < minx) minx = glyphStart; + if (glyphEnd > maxx) maxx = glyphEnd; + } + cursor_x += glyph->xAdvance; + } + if (minx > maxx) + return 0; + return maxx - minx; +} + // Draw centered text void drawTextCenter(int16_t cx, int16_t cy, String text) { int16_t x1, y1; @@ -638,20 +662,19 @@ void displayHeader(CommonData &commonData, GwApi::BoatValue *date, GwApi::BoatVa // Date and time String fmttype = commonData.config->getString(commonData.config->dateFormat); String timesource = commonData.config->getString(commonData.config->timeSource); - double tz = commonData.config->getString(commonData.config->timeZone).toDouble(); getdisplay().setTextColor(commonData.fgcolor); getdisplay().setFont(&Ubuntu_Bold8pt8b); getdisplay().setCursor(230, 15); if (timesource == "RTC" or timesource == "iRTC") { // TODO take DST into account if (commonData.data.rtcValid) { - time_t tv = mktime(&commonData.data.rtcTime) + (int)(tz * 3600); + time_t tv = mktime(&commonData.data.rtcTime) + (int)(commonData.tz * 3600); struct tm *local_tm = localtime(&tv); getdisplay().print(formatTime('m', local_tm->tm_hour, local_tm->tm_min, 0)); getdisplay().print(" "); getdisplay().print(formatDate(fmttype, local_tm->tm_year + 1900, local_tm->tm_mon + 1, local_tm->tm_mday)); getdisplay().print(" "); - getdisplay().print(tz == 0 ? "UTC" : "LOT"); + getdisplay().print(commonData.tz == 0 ? "UTC" : "LOT"); } else { drawTextRalign(396, 15, "RTC invalid"); } @@ -666,7 +689,7 @@ void displayHeader(CommonData &commonData, GwApi::BoatValue *date, GwApi::BoatVa getdisplay().print(" "); getdisplay().print(actdate); getdisplay().print(" "); - getdisplay().print(tz == 0 ? "UTC" : "LOT"); + getdisplay().print(commonData.tz == 0 ? "UTC" : "LOT"); } else{ if(commonData.config->getBool(commonData.config->useSimuData) == true){ diff --git a/lib/obp60task/PageAnchor.cpp b/lib/obp60task/PageAnchor.cpp index 11ce083..8d66e4b 100644 --- a/lib/obp60task/PageAnchor.cpp +++ b/lib/obp60task/PageAnchor.cpp @@ -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 #include "Pagedata.h" #include "OBP60Extensions.h" +#include "ConfigMenu.h" +// #include "miniz.h" // devices without PSRAM use + +// 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 (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: P4wwww wwww - 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 diff --git a/lib/obp60task/Pagedata.h b/lib/obp60task/Pagedata.h index 1cef664..77ebb03 100644 --- a/lib/obp60task/Pagedata.h +++ b/lib/obp60task/Pagedata.h @@ -110,6 +110,7 @@ typedef struct{ AlarmData alarm; GwApi::BoatValue *time = nullptr; GwApi::BoatValue *date = nullptr; + float tz = 0.0; // timezone from config uint16_t fgcolor; uint16_t bgcolor; bool keylock = false; diff --git a/lib/obp60task/config_obp40.json b/lib/obp60task/config_obp40.json index 22e9a50..fde0f4f 100644 --- a/lib/obp60task/config_obp40.json +++ b/lib/obp60task/config_obp40.json @@ -19,28 +19,6 @@ "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", @@ -1574,7 +1552,6 @@ "obp40": "true" } }, - { "name": "page1type", "label": "Type", diff --git a/lib/obp60task/config_obp60.json b/lib/obp60task/config_obp60.json index f447a89..1ccb846 100644 --- a/lib/obp60task/config_obp60.json +++ b/lib/obp60task/config_obp60.json @@ -19,28 +19,6 @@ "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", @@ -1129,6 +1107,34 @@ { "mapsource": ["Local Service"] } ] }, + { + "name": "mapServer", + "label": "map server", + "type": "string", + "default": "", + "description": "Server for converting map tiles. Use only one hostname or IP address", + "category": "OBP60 Navigation", + "capabilities": { + "obp60": "true" + }, + "condition": [ + { "mapsource": ["Remote Service"] } + ] + }, + { + "name": "mapTilePath", + "label": "map tile path", + "type": "string", + "default": "map.php", + "description": "Path to converter access e.g. index.php or map.php", + "category": "OBP40 Navigation", + "capabilities": { + "obp40": "true" + }, + "condition": [ + { "mapsource": ["Remote Service"] } + ] + }, { "name": "maptype", "label": "Map Type", diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index d9d601d..af6cb42 100644 --- a/lib/obp60task/obp60task.cpp +++ b/lib/obp60task/obp60task.cpp @@ -527,8 +527,8 @@ void OBP60Task(GwApi *api){ // Configuration values for main loop String gpsFix = api->getConfig()->getConfigItem(api->getConfig()->flashLED,true)->asString(); String gpsOn=api->getConfig()->getConfigItem(api->getConfig()->useGPS,true)->asString(); - float tz = api->getConfig()->getConfigItem(api->getConfig()->timeZone,true)->asFloat(); + commonData.tz = api->getConfig()->getConfigItem(api->getConfig()->timeZone,true)->asFloat(); commonData.backlight.mode = backlightMapping(config->getConfigItem(config->backlight,true)->asString()); commonData.backlight.color = colorMapping(config->getConfigItem(config->blColor,true)->asString()); commonData.backlight.brightness = uint(config->getConfigItem(config->blBrightness,true)->asInt()); @@ -703,7 +703,7 @@ void OBP60Task(GwApi *api){ starttime5 = millis(); if(time->valid == true && date->valid == true && lat->valid == true && lon->valid == true){ // Provide sundata to all pages - commonData.sundata = calcSunsetSunrise(time->value , date->value, lat->value, lon->value, tz); + commonData.sundata = calcSunsetSunrise(time->value , date->value, lat->value, lon->value, commonData.tz); // Backlight with sun control if (commonData.backlight.mode == BacklightMode::SUN) { // if(String(backlight) == "Control by Sun"){ @@ -716,7 +716,7 @@ void OBP60Task(GwApi *api){ } } else if (homevalid and commonData.data.rtcValid) { // No gps fix but valid home location and time configured - commonData.sundata = calcSunsetSunriseRTC(&commonData.data.rtcTime, homelat, homelon, tz); + commonData.sundata = calcSunsetSunriseRTC(&commonData.data.rtcTime, homelat, homelon, commonData.tz); } } diff --git a/lib/obp60task/platformio.ini b/lib/obp60task/platformio.ini index dc510f2..91d8135 100644 --- a/lib/obp60task/platformio.ini +++ b/lib/obp60task/platformio.ini @@ -45,8 +45,6 @@ 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) @@ -103,16 +101,14 @@ 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 -D HARDWARE_V10 #OBP40 hardware revision V1.0 SKU:DIE07300S V1.1 (CrowPanel 4.2) -D DISPLAY_GDEY042T81 #new E-Ink display from Good Display (Waveshare), R10 2.2 ohm - good (contast lost by shunshine) #-D DISPLAY_ZJY400300-042CAAMFGN #alternativ E-Ink display from ZZE Technology, R10 2.2 ohm - very good - -D LIPO_ACCU_1200 #Hardware extension, LiPo accu 3,7V 1200mAh - -D VOLTAGE_SENSOR #Hardware extension, LiPo voltage sensor with two resistors + #-D LIPO_ACCU_1200 #Hardware extension, LiPo accu 3,7V 1200mAh + #-D VOLTAGE_SENSOR #Hardware extension, LiPo voltage sensor with two resistors #-D ENABLE_PATCHES #enable patching of gateway code ${env.build_flags} upload_port = /dev/ttyUSB0 #OBP40 download via external USB/Serail converter