From 37d945a0ea53e898e57f982a03973e87581a3dc0 Mon Sep 17 00:00:00 2001 From: free-x Date: Tue, 9 Sep 2025 13:07:16 +0200 Subject: [PATCH 001/121] intermediate GPS 2.0 base --- lib/hardware/GwM5Base.h | 7 ++++++- webinstall/build.yaml | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/hardware/GwM5Base.h b/lib/hardware/GwM5Base.h index a3284ee..edc3a9e 100644 --- a/lib/hardware/GwM5Base.h +++ b/lib/hardware/GwM5Base.h @@ -37,6 +37,11 @@ GWRESOURCE_USE(SERIAL1,M5_GPS_KIT) #define _GWI_SERIAL1 BOARD_LEFT1,-1,GWSERIAL_TYPE_UNI,9600 #endif +#ifdef M5_GPSV2_KIT + GWRESOURCE_USE(BASE,M5_GPS_KIT) + GWRESOURCE_USE(SERIAL1,M5_GPS_KIT) + #define _GWI_SERIAL1 BOARD_LEFT1,-1,GWSERIAL_TYPE_UNI,115200 +#endif //M5 ProtoHub #ifdef M5_PROTO_HUB @@ -68,4 +73,4 @@ #define ESP32_CAN_RX_PIN BOARD_LEFT2 #endif -#endif \ No newline at end of file +#endif diff --git a/webinstall/build.yaml b/webinstall/build.yaml index 0fcca66..8344026 100644 --- a/webinstall/build.yaml +++ b/webinstall/build.yaml @@ -664,6 +664,11 @@ types: label: "Gps Base" url: "https://docs.m5stack.com/en/atom/atomicgps" resource: serial + - value: M5_GPSV2_KIT + description: "M5 Stack Gps Base v2.0" + label: "Gps Base" + url: "https://docs.m5stack.com/en/atom/Atomic_GPS_Base_v2.0" + resource: serial - value: M5_PROTO_HUB description: "M5 Stack HUB PROTO" url: "https://docs.m5stack.com/en/atom/atomhub" @@ -749,4 +754,4 @@ config: - <<: *spisensors base: busname: "2" - bus: "2" \ No newline at end of file + bus: "2" From 370fd47deb1746fcfac485e78948838c1e37314b Mon Sep 17 00:00:00 2001 From: free-x Date: Tue, 9 Sep 2025 15:03:48 +0200 Subject: [PATCH 002/121] add GPS V2 Base to webinstall configs --- webinstall/config/m5stack-atom-gps_v2-canunit.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 webinstall/config/m5stack-atom-gps_v2-canunit.json diff --git a/webinstall/config/m5stack-atom-gps_v2-canunit.json b/webinstall/config/m5stack-atom-gps_v2-canunit.json new file mode 100644 index 0000000..e7f13be --- /dev/null +++ b/webinstall/config/m5stack-atom-gps_v2-canunit.json @@ -0,0 +1 @@ +{"root:board":"m5stack-atom-generic","root:board:m5lightbase":"M5_GPSV2_KIT","root:board:m5groove":"CAN","root:board:m5groove:m5groovecan":"M5_CANUNIT"} From 2c97eacd766a5db5c1ffc883bdda8139dcf68295 Mon Sep 17 00:00:00 2001 From: free-x Date: Thu, 11 Sep 2025 19:36:09 +0200 Subject: [PATCH 003/121] minor corrections --- lib/hardware/GwM5Base.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/hardware/GwM5Base.h b/lib/hardware/GwM5Base.h index edc3a9e..f6371a9 100644 --- a/lib/hardware/GwM5Base.h +++ b/lib/hardware/GwM5Base.h @@ -38,8 +38,8 @@ #define _GWI_SERIAL1 BOARD_LEFT1,-1,GWSERIAL_TYPE_UNI,9600 #endif #ifdef M5_GPSV2_KIT - GWRESOURCE_USE(BASE,M5_GPS_KIT) - GWRESOURCE_USE(SERIAL1,M5_GPS_KIT) + GWRESOURCE_USE(BASE,M5_GPSV2_KIT) + GWRESOURCE_USE(SERIAL1,M5_GPSV2_KIT) #define _GWI_SERIAL1 BOARD_LEFT1,-1,GWSERIAL_TYPE_UNI,115200 #endif @@ -66,7 +66,7 @@ #endif //can kit for M5 Atom -#ifdef M5_CAN_KIT +#if defined (M5_CAN_KIT) GWRESOURCE_USE(BASE,M5_CAN_KIT) GWRESOURCE_USE(CAN,M5_CANKIT) #define ESP32_CAN_TX_PIN BOARD_LEFT1 From da6022cb28883dcf447c5ab7506a0abf161e424d Mon Sep 17 00:00:00 2001 From: free-x Date: Thu, 11 Sep 2025 20:23:52 +0200 Subject: [PATCH 004/121] #110: add GPS v1.1 unit --- lib/hardware/GwM5Grove.in | 7 +++++++ webinstall/build.yaml | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/lib/hardware/GwM5Grove.in b/lib/hardware/GwM5Grove.in index aed70a1..9858783 100644 --- a/lib/hardware/GwM5Grove.in +++ b/lib/hardware/GwM5Grove.in @@ -43,6 +43,13 @@ #define _GWI_SERIAL_GROOVE$GS$ GWSERIAL_TYPE_RX,9600 #endif +#GROVE +//https://docs.m5stack.com/en/unit/Unit-GPS%20v1.1 +#ifdef M5_GPSV11_UNIT$GS$ + GWRESOURCE_USE(GROOVE$G$,M5_GPSV11_UNIT$GS$) + #define _GWI_SERIAL_GROOVE$GS$ GWSERIAL_TYPE_RX,115200 +#endif + #GROVE //CAN via groove #ifdef M5_CANUNIT$GS$ diff --git a/webinstall/build.yaml b/webinstall/build.yaml index 8344026..fe7ec90 100644 --- a/webinstall/build.yaml +++ b/webinstall/build.yaml @@ -179,6 +179,11 @@ types: description: "M5 Gps Unit" url: "https://docs.m5stack.com/en/unit/gps" resource: serial + - label: "Gps Unit v1.1" + value: M5_GPSV11_UNIT#grv# + description: "M5 Gps Unit v1.1" + url: "https://docs.m5stack.com/en/unit/Unit-GPS%20v1.1" + resource: serial - label: "RS232/RS422" value: SERIAL_GROOVE_232#grv# description: "Generic RS232/RS422 Unit (bidirectional)" From b3e2dea45b528c62b921222ab53dc00650f09e8c Mon Sep 17 00:00:00 2001 From: Ulrich Meine Date: Fri, 12 Sep 2025 18:42:49 +0200 Subject: [PATCH 005/121] Code part for more chart plots --- lib/obp60task/OBPDataOperations.h | 1 + lib/obp60task/OBPcharts.cpp | 75 ++++++ lib/obp60task/OBPcharts.h | 103 ++++++++ lib/obp60task/PageWindPlot.cpp | 418 +++++++++++++++--------------- 4 files changed, 393 insertions(+), 204 deletions(-) create mode 100644 lib/obp60task/OBPcharts.cpp create mode 100644 lib/obp60task/OBPcharts.h diff --git a/lib/obp60task/OBPDataOperations.h b/lib/obp60task/OBPDataOperations.h index c93c1fe..b819e15 100644 --- a/lib/obp60task/OBPDataOperations.h +++ b/lib/obp60task/OBPDataOperations.h @@ -1,3 +1,4 @@ +// Function lib for history buffer handling, true wind calculation, and other operations on boat data #pragma once #include #include "OBPRingBuffer.h" diff --git a/lib/obp60task/OBPcharts.cpp b/lib/obp60task/OBPcharts.cpp new file mode 100644 index 0000000..44c782e --- /dev/null +++ b/lib/obp60task/OBPcharts.cpp @@ -0,0 +1,75 @@ +// Function lib for display of boat data in various chart formats +#include "OBPcharts.h" + +// --- Class Chart --------------- +void Chart::drawChrtHdr() { + // chart header label + lines + int i; + getdisplay().fillRect(0, top, cWidth, 2, commonData->fgcolor); + + // horizontal chart labels + getdisplay().drawLine(cStart.x, cStart.y, cWidth, cStart.y); + getdisplay().fillRect(cStart.x, cStart.y, cWidth, 2, commonData->fgcolor); + + getdisplay().setFont(&Ubuntu_Bold10pt8b); + getdisplay().setCursor(cStart.x, cStart.y + 12); + getdisplay().print(dbName); // Wind data name + + getdisplay().setFont(&Ubuntu_Bold12pt8b); + if (chrtSze == 0) { + i = -1 * (chrtIntv / 8 - 2); + } else { + i = -1 * (chrtIntv / 4 - 2); + } + for (int j = 50; j <= (cWidth - 50); j += 50 ) { + getdisplay().setCursor(cStart.x + j - 16, cStart.y + 12); + getdisplay().print(i++); // time interval + // i++; + getdisplay().drawLine(cStart.x + j - 30, cStart.y, cStart.x - 30, cHeight + top); + } +} + +void Chart::drawChrtGrd(const int chrtRng) { + // chart Y axis labels + lines + int i; + + getdisplay().setFont(&Ubuntu_Bold8pt8b); + if (chrtDir == 0) { + i = -1 * (chrtRng / 4 - 2); + for (int j = cStart.x; j <= (cHeight - (cHeight / 4)); j += cHeight / 4 ) { + getdisplay().drawLine(0, cStart.y, cWidth, cStart.y); + getdisplay().setCursor(0, cStart.y + 12); + if (i < 10) + getdisplay().printf("!!%1d", i); // Range value + else if (i < 100) + getdisplay().printf("!%2d", i); // Range value + else + getdisplay().printf("%3d", i); // Range value + i += (chrtRng / 4); + } + } else { + i = -1 * (chrtRng / 8 - 2); + for (int j = cStart.x; j <= (cHeight - (cHeight / 8)); j += cHeight / 8 ) { + getdisplay().drawLine(cStart.x + j - 30, cStart.y, cStart.x - 30, cHeight + top); + getdisplay().setCursor(0, cStart.y + 12); + if (i < 10) + getdisplay().printf("!!%1d", i); // Range value + else if (i < 100) + getdisplay().printf("!%2d", i); // Range value + else + getdisplay().printf("%3d", i); // Range value + i += (chrtRng / 4); + } + } +} + +bool Chart::drawChrt(int8_t chrtIntv, int dfltRng) { +// hstryBuf = buffer to display +// bValue = present value to display additionally to chart +// chrtDir = chart direction: [0] = vertical, [1] = horizontal +// chrtSze = chart size: [0] = full size, [1] = half size left half/top, [2] half size right half/bottom +// chrtIntv = chart time interval: [1] 4 min., [2] 8 min., [3] 12 min., [4] 16 min., [5] 32 min. +// dfltRng = default range of chart, e.g. 30 = [0..30] + +} +// --- Class Chart --------------- diff --git a/lib/obp60task/OBPcharts.h b/lib/obp60task/OBPcharts.h new file mode 100644 index 0000000..813c2da --- /dev/null +++ b/lib/obp60task/OBPcharts.h @@ -0,0 +1,103 @@ +// Function lib for display of boat data in various chart formats +#pragma once +#include "OBP60Extensions.h" +#include "Pagedata.h" +// #include "OBPDataOperations.h" +// #include "OBPRingBuffer.h" + +struct Point { + int x; + int y; +}; + +class Chart { +protected: + RingBuffer* dataBuf; // Buffer to display + GwApi::BoatValue* bValue; // Present value to display additionally to chart + int8_t chrtDir; // Chart direction: [0] = vertical, [1] = horizontal + int8_t chrtSze; // Chart size: [0] = full size, [1] = half size left/top, [2] half size right/bottom + int8_t chrtIntv; // Chart time interval: [4] 4 min., [8] 8 min., [12] 12 min., [16] 16 min., [32] 32 min. + int dfltRng; // Default range of chart, e.g. 30 = [0..30] + + int top = 48; // display top header lines + int bottom = 22; // display bottom lines + int gap = 4; // gap between 2 charts; actual gap is 2x + int cWidth; + int cHeight; + Point cStart; // start point for chart area + int cLines; // number of chart lines + int xCenter; // x center point of chart + + String dbName, dbFormat; + int16_t dbMAX_VAL; + size_t bufSize; + GwApi::BoatValue* bValue; + +public: + Chart(RingBuffer* dataBuf, GwApi::BoatValue* bValue, int8_t chrtDir, int8_t chrtSz, int8_t chrtIntv, int dfltRng, GwLog* logger) + : dataBuf(dataBuf) + , bValue(bValue) + , chrtDir(chrtDir) + , chrtSze(chrtSze) + , chrtIntv(chrtIntv) + , dfltRng(dfltRng) + { + cWidth = getdisplay().width(); + cHeight = getdisplay().height(); + cHeight = cHeight - top - bottom; + if (chrtDir == 0) { + // vertical chart + switch (chrtSze) { + case 0: + // default is already set + break; + case 1: + cWidth = cWidth; + cHeight = cHeight / 2 - gap; + cStart = { 30, cHeight + top }; + break; + case 2: + cWidth = cWidth; + cHeight = cHeight / 2 - gap; + cStart = { cWidth + gap, top }; + break; + default: + LOG_DEBUG(GwLog::DEBUG, "displayChart: wrong parameter"); + return; + } + } else if (chrtDir == 1) { + // horizontal chart + switch (chrtSze) { + case 0: + cStart = { 0, cHeight - bottom }; + break; + case 1: + cWidth = cWidth / 2 - gap; + cHeight = cHeight; + cStart = { 0, cHeight - bottom }; + break; + case 2: + cWidth = cWidth / 2 - gap; + cHeight = cHeight; + cStart = { cWidth + gap, cHeight - bottom }; + break; + default: + LOG_DEBUG(GwLog::DEBUG, "displayChart: wrong parameter"); + return; + } + } else { + LOG_DEBUG(GwLog::DEBUG, "displayChart: wrong parameter"); + return; + } + xCenter = cWidth / 2; + cLines = cHeight - 22; + + dataBuf->getMetaData(dbName, dbFormat); + dbMAX_VAL = dataBuf->getMaxVal(); + bufSize = dataBuf->getCapacity(); + bValue->setFormat(dataBuf->getFormat()); + }; + void drawChrtHdr(); + void drawChrtGrd(const int chrtRng); + bool drawChrt(int8_t chrtIntv, int dfltRng); +}; \ No newline at end of file diff --git a/lib/obp60task/PageWindPlot.cpp b/lib/obp60task/PageWindPlot.cpp index 4bc79f7..b3f55ea 100644 --- a/lib/obp60task/PageWindPlot.cpp +++ b/lib/obp60task/PageWindPlot.cpp @@ -1,10 +1,11 @@ #if defined BOARD_OBP60S3 || defined BOARD_OBP40S3 -#include "Pagedata.h" -#include "OBP60Extensions.h" -#include "OBPRingBuffer.h" -#include "OBPDataOperations.h" #include "BoatDataCalibration.h" +#include "OBP60Extensions.h" +#include "OBPDataOperations.h" +#include "OBPRingBuffer.h" +#include "OBPcharts.h" +#include "Pagedata.h" #include static const double radToDeg = 180.0 / M_PI; // Conversion factor from radians to degrees @@ -59,7 +60,7 @@ int getRng(const RingBuffer& windDirHstry, int center, size_t amount) value = windDirHstry.get(count - 1 - i); if (value == MAX_VAL) { - continue; // ignore invalid values + continue; // ignore invalid values } value = value / 1000.0 * radToDeg; @@ -99,13 +100,12 @@ public: // holdValues = common.config->getBool(common.config->holdvalues); flashLED = common.config->getString(common.config->flashLED); backlightMode = common.config->getString(common.config->backlight); - } virtual void setupKeys() { Page::setupKeys(); - // commonData->keydata[0].label = "MODE"; + commonData->keydata[0].label = "MODE"; #if defined BOARD_OBP60S3 commonData->keydata[1].label = "SRC"; commonData->keydata[4].label = "INTV"; @@ -161,17 +161,18 @@ public: return key; } - virtual void displayNew(PageData &pageData){ + virtual void displayNew(PageData& pageData) + { #ifdef BOARD_OBP40S3 String wndSrc; // Wind source true/apparant wind - preselection for OBP40 wndSrc = commonData->config->getString("page" + String(pageData.pageNumber) + "wndsrc"); - if (wndSrc =="True wind") { + if (wndSrc == "True wind") { showTruW = true; } else { showTruW = false; // Wind source is apparant wind } - commonData->logger->logDebug(GwLog::LOG,"New PageWindPlot: wind source=%s", wndSrc); + commonData->logger->logDebug(GwLog::LOG, "New PageWindPlot: wind source=%s", wndSrc); #endif oldShowTruW = !showTruW; // makes wind source being initialized at initial page call } @@ -280,225 +281,234 @@ public: oldShowTruW = showTruW; } - // Identify buffer size and buffer start position for chart - count = wdHstry->getCurrentSize(); - currIdx = wdHstry->getLastIdx(); - numAddedBufVals = (currIdx - lastAddedIdx + bufSize) % bufSize; // Number of values added to buffer since last display - if (dataIntv != oldDataIntv || count == 1) { - // new data interval selected by user - intvBufSize = cHeight * dataIntv; - numWndVals = min(count, (cHeight - 60) * dataIntv); - bufStart = max(0, count - numWndVals); - lastAddedIdx = currIdx; - oldDataIntv = dataIntv; - } else { - numWndVals = numWndVals + numAddedBufVals; - lastAddedIdx = currIdx; - if (count == bufSize) { - bufStart = max(0, bufStart - numAddedBufVals); + if (chrtMode == 'D') { + // Identify buffer size and buffer start position for chart + count = wdHstry->getCurrentSize(); + currIdx = wdHstry->getLastIdx(); + numAddedBufVals = (currIdx - lastAddedIdx + bufSize) % bufSize; // Number of values added to buffer since last display + if (dataIntv != oldDataIntv || count == 1) { + // new data interval selected by user + intvBufSize = cHeight * dataIntv; + numWndVals = min(count, (cHeight - 60) * dataIntv); + bufStart = max(0, count - numWndVals); + lastAddedIdx = currIdx; + oldDataIntv = dataIntv; + } else { + numWndVals = numWndVals + numAddedBufVals; + lastAddedIdx = currIdx; + if (count == bufSize) { + bufStart = max(0, bufStart - numAddedBufVals); + } } - } - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Dataset: count: %d, xWD: %.1f, xWS: %.2f, xWD_valid? %d, intvBufSize: %d, numWndVals: %d, bufStart: %d, numAddedBufVals: %d, lastIdx: %d, wind source: %s", - count, wdHstry->getLast() / 1000.0 * radToDeg, wsHstry->getLast() / 1000.0 * 1.94384, BDataValid[0], intvBufSize, numWndVals, bufStart, numAddedBufVals, wdHstry->getLastIdx(), - showTruW ? "True" : "App"); + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Dataset: count: %d, xWD: %.1f, xWS: %.2f, xWD_valid? %d, intvBufSize: %d, numWndVals: %d, bufStart: %d, numAddedBufVals: %d, lastIdx: %d, wind source: %s", + count, wdHstry->getLast() / 1000.0 * radToDeg, wsHstry->getLast() / 1000.0 * 1.94384, BDataValid[0], intvBufSize, numWndVals, bufStart, numAddedBufVals, wdHstry->getLastIdx(), + showTruW ? "True" : "App"); - // Set wndCenter from 1st real buffer value - if (wndCenter == INT_MAX || (wndCenter == 0 && count == 1)) { - wndCenter = getCntr(*wdHstry, numWndVals); - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Range Init: count: %d, xWD: %.1f, wndCenter: %d, diffRng: %d, chrtRng: %d, Min: %.0f, Max: %.0f", count, wdHstry->getLast() / 1000.0 * radToDeg, - wndCenter, diffRng, chrtRng, wdHstry->getMin(numWndVals) / 1000.0 * radToDeg, wdHstry->getMax(numWndVals) / 1000.0 * radToDeg); - } else { - // check and adjust range between left, center, and right chart limit - diffRng = getRng(*wdHstry, wndCenter, numWndVals); - diffRng = (diffRng == wdMAX_VAL ? 0 : diffRng); - if (diffRng > chrtRng) { - chrtRng = int((diffRng + (diffRng >= 0 ? 9 : -1)) / 10) * 10; // Round up to next 10 degree value - } else if (diffRng + 10 < chrtRng) { // Reduce chart range for higher resolution if possible - chrtRng = max(dfltRng, int((diffRng + (diffRng >= 0 ? 9 : -1)) / 10) * 10); - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Range adjust: wndCenter: %d, diffRng: %d, chrtRng: %d, Min: %.0f, Max: %.0f", wndCenter, diffRng, chrtRng, - wdHstry->getMin(numWndVals) / 1000.0 * radToDeg, wdHstry->getMax(numWndVals) / 1000.0 * radToDeg); + // Set wndCenter from 1st real buffer value + if (wndCenter == INT_MAX || (wndCenter == 0 && count == 1)) { + wndCenter = getCntr(*wdHstry, numWndVals); + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Range Init: count: %d, xWD: %.1f, wndCenter: %d, diffRng: %d, chrtRng: %d, Min: %.0f, Max: %.0f", count, wdHstry->getLast() / 1000.0 * radToDeg, + wndCenter, diffRng, chrtRng, wdHstry->getMin(numWndVals) / 1000.0 * radToDeg, wdHstry->getMax(numWndVals) / 1000.0 * radToDeg); + } else { + // check and adjust range between left, center, and right chart limit + diffRng = getRng(*wdHstry, wndCenter, numWndVals); + diffRng = (diffRng == wdMAX_VAL ? 0 : diffRng); + if (diffRng > chrtRng) { + chrtRng = int((diffRng + (diffRng >= 0 ? 9 : -1)) / 10) * 10; // Round up to next 10 degree value + } else if (diffRng + 10 < chrtRng) { // Reduce chart range for higher resolution if possible + chrtRng = max(dfltRng, int((diffRng + (diffRng >= 0 ? 9 : -1)) / 10) * 10); + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Range adjust: wndCenter: %d, diffRng: %d, chrtRng: %d, Min: %.0f, Max: %.0f", wndCenter, diffRng, chrtRng, + wdHstry->getMin(numWndVals) / 1000.0 * radToDeg, wdHstry->getMax(numWndVals) / 1000.0 * radToDeg); + } } - } - chrtScl = float(width) / float(chrtRng) / 2.0; // Chart scale: pixels per degree - wndLeft = wndCenter - chrtRng; - if (wndLeft < 0) - wndLeft += 360; - wndRight = (chrtRng < 180 ? wndCenter + chrtRng : wndCenter + chrtRng - 1); - if (wndRight >= 360) - wndRight -= 360; + chrtScl = float(width) / float(chrtRng) / 2.0; // Chart scale: pixels per degree + wndLeft = wndCenter - chrtRng; + if (wndLeft < 0) + wndLeft += 360; + wndRight = (chrtRng < 180 ? wndCenter + chrtRng : wndCenter + chrtRng - 1); + if (wndRight >= 360) + wndRight -= 360; - // Draw page - //*********************************************************************** + // Draw page + //*********************************************************************** - // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, width, height); // Set partial update - getdisplay().setTextColor(commonData->fgcolor); + // Set display in partial refresh mode + getdisplay().setPartialWindow(0, 0, width, height); // Set partial update + getdisplay().setTextColor(commonData->fgcolor); - // chart lines - getdisplay().fillRect(0, yOffset, width, 2, commonData->fgcolor); - getdisplay().fillRect(xCenter, yOffset, 1, cHeight, commonData->fgcolor); + // chart lines + getdisplay().fillRect(0, yOffset, width, 2, commonData->fgcolor); + getdisplay().fillRect(xCenter, yOffset, 1, cHeight, commonData->fgcolor); - // chart labels - char sWndLbl[4]; // char buffer for Wind angle label - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(xCenter - 88, yOffset - 3); - getdisplay().print(wdName); // Wind data name - snprintf(sWndLbl, 4, "%03d", (wndCenter < 0) ? (wndCenter + 360) : wndCenter); - drawTextCenter(xCenter, yOffset - 11, sWndLbl); - getdisplay().drawCircle(xCenter + 25, yOffset - 17, 2, commonData->fgcolor); // symbol - getdisplay().drawCircle(xCenter + 25, yOffset - 17, 3, commonData->fgcolor); // symbol - getdisplay().setCursor(1, yOffset - 3); - snprintf(sWndLbl, 4, "%03d", (wndLeft < 0) ? (wndLeft + 360) : wndLeft); - getdisplay().print(sWndLbl); // Wind left value - getdisplay().drawCircle(46, yOffset - 17, 2, commonData->fgcolor); // symbol - getdisplay().drawCircle(46, yOffset - 17, 3, commonData->fgcolor); // symbol - getdisplay().setCursor(width - 50, yOffset - 3); - snprintf(sWndLbl, 4, "%03d", (wndRight < 0) ? (wndRight + 360) : wndRight); - getdisplay().print(sWndLbl); // Wind right value - getdisplay().drawCircle(width - 5, yOffset - 17, 2, commonData->fgcolor); // symbol - getdisplay().drawCircle(width - 5, yOffset - 17, 3, commonData->fgcolor); // symbol + // chart labels + char sWndLbl[4]; // char buffer for Wind angle label + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().setCursor(xCenter - 88, yOffset - 3); + getdisplay().print(wdName); // Wind data name + snprintf(sWndLbl, 4, "%03d", (wndCenter < 0) ? (wndCenter + 360) : wndCenter); + drawTextCenter(xCenter, yOffset - 11, sWndLbl); + getdisplay().drawCircle(xCenter + 25, yOffset - 17, 2, commonData->fgcolor); // symbol + getdisplay().drawCircle(xCenter + 25, yOffset - 17, 3, commonData->fgcolor); // symbol + getdisplay().setCursor(1, yOffset - 3); + snprintf(sWndLbl, 4, "%03d", (wndLeft < 0) ? (wndLeft + 360) : wndLeft); + getdisplay().print(sWndLbl); // Wind left value + getdisplay().drawCircle(46, yOffset - 17, 2, commonData->fgcolor); // symbol + getdisplay().drawCircle(46, yOffset - 17, 3, commonData->fgcolor); // symbol + getdisplay().setCursor(width - 50, yOffset - 3); + snprintf(sWndLbl, 4, "%03d", (wndRight < 0) ? (wndRight + 360) : wndRight); + getdisplay().print(sWndLbl); // Wind right value + getdisplay().drawCircle(width - 5, yOffset - 17, 2, commonData->fgcolor); // symbol + getdisplay().drawCircle(width - 5, yOffset - 17, 3, commonData->fgcolor); // symbol - if (wdHstry->getMax() == wdMAX_VAL) { - // only values in buffer -> no valid wind data available - wndDataValid = false; - } else if (!BDataValid[0] && !useSimuData) { - // currently no valid xWD data available and no simulation mode - numNoData++; - wndDataValid = true; - if (numNoData > 3) { - // If more than 4 invalid values in a row, send message + if (wdHstry->getMax() == wdMAX_VAL) { + // only values in buffer -> no valid wind data available wndDataValid = false; + } else if (!BDataValid[0] && !useSimuData) { + // currently no valid xWD data available and no simulation mode + numNoData++; + wndDataValid = true; + if (numNoData > 3) { + // If more than 4 invalid values in a row, send message + wndDataValid = false; + } + } else { + numNoData = 0; // reset data error counter + wndDataValid = true; // At least some wind data available } - } else { - numNoData = 0; // reset data error counter - wndDataValid = true; // At least some wind data available - } - // Draw wind values in chart - //*********************************************************************** - if (wndDataValid) { - for (int i = 0; i < (numWndVals / dataIntv); i++) { - chrtVal = static_cast(wdHstry->get(bufStart + (i * dataIntv))); // show the latest wind values in buffer; keep 1st value constant in a rolling buffer - if (chrtVal == wdMAX_VAL) { - chrtPrevVal = wdMAX_VAL; - } else { - chrtVal = static_cast((chrtVal / 1000.0 * radToDeg) + 0.5); // Convert to degrees and round - x = ((chrtVal - wndLeft + 360) % 360) * chrtScl; - y = yOffset + cHeight - i; // Position in chart area + // Draw wind values in chart + //*********************************************************************** + if (wndDataValid) { + for (int i = 0; i < (numWndVals / dataIntv); i++) { + chrtVal = static_cast(wdHstry->get(bufStart + (i * dataIntv))); // show the latest wind values in buffer; keep 1st value constant in a rolling buffer + if (chrtVal == wdMAX_VAL) { + chrtPrevVal = wdMAX_VAL; + } else { + chrtVal = static_cast((chrtVal / 1000.0 * radToDeg) + 0.5); // Convert to degrees and round + x = ((chrtVal - wndLeft + 360) % 360) * chrtScl; + y = yOffset + cHeight - i; // Position in chart area - if (i >= (numWndVals / dataIntv) - 1) // log chart data of 1 line (adjust for test purposes) - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Chart: i: %d, chrtVal: %d, bufStart: %d, count: %d, linesToShow: %d", i, chrtVal, bufStart, count, (numWndVals / dataIntv)); + if (i >= (numWndVals / dataIntv) - 1) // log chart data of 1 line (adjust for test purposes) + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Chart: i: %d, chrtVal: %d, bufStart: %d, count: %d, linesToShow: %d", i, chrtVal, bufStart, count, (numWndVals / dataIntv)); - if ((i == 0) || (chrtPrevVal == wdMAX_VAL)) { - // just a dot for 1st chart point or after some invalid values + if ((i == 0) || (chrtPrevVal == wdMAX_VAL)) { + // just a dot for 1st chart point or after some invalid values + prevX = x; + prevY = y; + } else { + // cross borders check; shift values to [-180..0..180]; when crossing borders, range is 2x 180 degrees + int wndLeftDlt = -180 - ((wndLeft >= 180) ? (wndLeft - 360) : wndLeft); + int chrtVal180 = ((chrtVal + wndLeftDlt + 180) % 360 + 360) % 360 - 180; + int chrtPrevVal180 = ((chrtPrevVal + wndLeftDlt + 180) % 360 + 360) % 360 - 180; + if (((chrtPrevVal180 >= -180) && (chrtPrevVal180 < -90) && (chrtVal180 > 90)) || ((chrtPrevVal180 <= 179) && (chrtPrevVal180 > 90) && chrtVal180 <= -90)) { + // If current value crosses chart borders compared to previous value, split line + int xSplit = (((chrtPrevVal180 > 0 ? wndRight : wndLeft) - wndLeft + 360) % 360) * chrtScl; + getdisplay().drawLine(prevX, prevY, xSplit, y, commonData->fgcolor); + getdisplay().drawLine(prevX, prevY - 1, ((xSplit != prevX) ? xSplit : xSplit - 1), ((xSplit != prevX) ? y - 1 : y), commonData->fgcolor); + prevX = (((chrtVal180 > 0 ? wndRight : wndLeft) - wndLeft + 360) % 360) * chrtScl; + } + } + + // Draw line with 2 pixels width + make sure vertical line are drawn correctly + getdisplay().drawLine(prevX, prevY, x, y, commonData->fgcolor); + getdisplay().drawLine(prevX, prevY - 1, ((x != prevX) ? x : x - 1), ((x != prevX) ? y - 1 : y), commonData->fgcolor); + chrtPrevVal = chrtVal; prevX = x; prevY = y; - } else { - // cross borders check; shift values to [-180..0..180]; when crossing borders, range is 2x 180 degrees - int wndLeftDlt = -180 - ((wndLeft >= 180) ? (wndLeft - 360) : wndLeft); - int chrtVal180 = ((chrtVal + wndLeftDlt + 180) % 360 + 360) % 360 - 180; - int chrtPrevVal180 = ((chrtPrevVal + wndLeftDlt + 180) % 360 + 360) % 360 - 180; - if (((chrtPrevVal180 >= -180) && (chrtPrevVal180 < -90) && (chrtVal180 > 90)) || ((chrtPrevVal180 <= 179) && (chrtPrevVal180 > 90) && chrtVal180 <= -90)) { - // If current value crosses chart borders compared to previous value, split line - int xSplit = (((chrtPrevVal180 > 0 ? wndRight : wndLeft) - wndLeft + 360) % 360) * chrtScl; - getdisplay().drawLine(prevX, prevY, xSplit, y, commonData->fgcolor); - getdisplay().drawLine(prevX, prevY - 1, ((xSplit != prevX) ? xSplit : xSplit - 1), ((xSplit != prevX) ? y - 1 : y), commonData->fgcolor); - prevX = (((chrtVal180 > 0 ? wndRight : wndLeft) - wndLeft + 360) % 360) * chrtScl; + } + // Reaching chart area top end + if (i >= (cHeight - 1)) { + oldDataIntv = 0; // force reset of buffer start and number of values to show in next display loop + + int minWndDir = wdHstry->getMin(numWndVals) / 1000.0 * radToDeg; + int maxWndDir = wdHstry->getMax(numWndVals) / 1000.0 * radToDeg; + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot FreeTop: Minimum: %d, Maximum: %d, OldwndCenter: %d", minWndDir, maxWndDir, wndCenter); + // if (((minWndDir - wndCenter >= 0) && (minWndDir - wndCenter < 180)) || ((maxWndDir - wndCenter <= 0) && (maxWndDir - wndCenter >=180))) { + if ((wndRight > wndCenter && (minWndDir >= wndCenter && minWndDir <= wndRight)) || (wndRight <= wndCenter && (minWndDir >= wndCenter || minWndDir <= wndRight)) || (wndLeft < wndCenter && (maxWndDir <= wndCenter && maxWndDir >= wndLeft)) || (wndLeft >= wndCenter && (maxWndDir <= wndCenter || maxWndDir >= wndLeft))) { + // Check if all wind value are left or right of center value -> optimize chart center + wndCenter = getCntr(*wdHstry, numWndVals); } + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot FreeTop: cHeight: %d, bufStart: %d, numWndVals: %d, wndCenter: %d", cHeight, bufStart, numWndVals, wndCenter); + break; } - - // Draw line with 2 pixels width + make sure vertical line are drawn correctly - getdisplay().drawLine(prevX, prevY, x, y, commonData->fgcolor); - getdisplay().drawLine(prevX, prevY - 1, ((x != prevX) ? x : x - 1), ((x != prevX) ? y - 1 : y), commonData->fgcolor); - chrtPrevVal = chrtVal; - prevX = x; - prevY = y; } - // Reaching chart area top end - if (i >= (cHeight - 1)) { - oldDataIntv = 0; // force reset of buffer start and number of values to show in next display loop - int minWndDir = wdHstry->getMin(numWndVals) / 1000.0 * radToDeg; - int maxWndDir = wdHstry->getMax(numWndVals) / 1000.0 * radToDeg; - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot FreeTop: Minimum: %d, Maximum: %d, OldwndCenter: %d", minWndDir, maxWndDir, wndCenter); - // if (((minWndDir - wndCenter >= 0) && (minWndDir - wndCenter < 180)) || ((maxWndDir - wndCenter <= 0) && (maxWndDir - wndCenter >=180))) { - if ((wndRight > wndCenter && (minWndDir >= wndCenter && minWndDir <= wndRight)) || (wndRight <= wndCenter && (minWndDir >= wndCenter || minWndDir <= wndRight)) || (wndLeft < wndCenter && (maxWndDir <= wndCenter && maxWndDir >= wndLeft)) || (wndLeft >= wndCenter && (maxWndDir <= wndCenter || maxWndDir >= wndLeft))) { - // Check if all wind value are left or right of center value -> optimize chart center - wndCenter = getCntr(*wdHstry, numWndVals); + // Print wind speed value + int currentZone; + static int lastZone = 0; + static bool flipTws = false; + int xPosTws; + static const int yPosTws = yOffset + 40; + + xPosTws = flipTws ? 20 : width - 145; + currentZone = (y >= yPosTws - 38) && (y <= yPosTws + 6) && (x >= xPosTws - 4) && (x <= xPosTws + 146) ? 1 : 0; // Define current zone for TWS value + if (currentZone != lastZone) { + // Only flip when x moves to a different zone + if ((y >= yPosTws - 38) && (y <= yPosTws + 6) && (x >= xPosTws - 4) && (x <= xPosTws + 146)) { + flipTws = !flipTws; + xPosTws = flipTws ? 20 : width - 145; } - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot FreeTop: cHeight: %d, bufStart: %d, numWndVals: %d, wndCenter: %d", cHeight, bufStart, numWndVals, wndCenter); - break; } - } + lastZone = currentZone; - // Print wind speed value - int currentZone; - static int lastZone = 0; - static bool flipTws = false; - int xPosTws; - static const int yPosTws = yOffset + 40; + wsValue = wsHstry->getLast(); + wsBVal->value = wsValue / 1000.0; // temp variable to retreive data unit from OBP60Formater + wsBVal->valid = (static_cast(wsValue) != wsHstry->getMinVal()); + String swsValue = formatValue(wsBVal, *commonData).svalue; // value (string) + wsUnit = formatValue(wsBVal, *commonData).unit; // Unit of value + getdisplay().fillRect(xPosTws - 4, yPosTws - 38, 142, 44, commonData->bgcolor); // Clear area for TWS value + getdisplay().setFont(&DSEG7Classic_BoldItalic16pt7b); + getdisplay().setCursor(xPosTws, yPosTws); + getdisplay().print(swsValue); // Value + /* if (!wsBVal->valid) { + getdisplay().print("--.-"); + } else { + wsValue = wsValue / 1000.0 * 1.94384; // Wind speed value in knots + if (wsValue < 10.0) { + getdisplay().printf("!%3.1f", wsValue); // Value, round to 1 decimal + } else { + getdisplay().printf("%4.1f", wsValue); // Value, round to 1 decimal + } + } */ + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().setCursor(xPosTws + 82, yPosTws - 14); + getdisplay().print(wsName); // Name + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(xPosTws + 82, yPosTws + 1); + getdisplay().print(wsUnit); // Unit - xPosTws = flipTws ? 20 : width - 145; - currentZone = (y >= yPosTws - 38) && (y <= yPosTws + 6) && (x >= xPosTws - 4) && (x <= xPosTws + 146) ? 1 : 0; // Define current zone for TWS value - if (currentZone != lastZone) { - // Only flip when x moves to a different zone - if ((y >= yPosTws - 38) && (y <= yPosTws + 6) && (x >= xPosTws - 4) && (x <= xPosTws + 146)) { - flipTws = !flipTws; - xPosTws = flipTws ? 20 : width - 145; - } - } - lastZone = currentZone; - - wsValue = wsHstry->getLast(); - wsBVal->value = wsValue / 1000.0; // temp variable to retreive data unit from OBP60Formater - wsBVal->valid = (static_cast(wsValue) != wsHstry->getMinVal()); - String swsValue = formatValue(wsBVal, *commonData).svalue; // value (string) - wsUnit = formatValue(wsBVal, *commonData).unit; // Unit of value - getdisplay().fillRect(xPosTws - 4, yPosTws - 38, 142, 44, commonData->bgcolor); // Clear area for TWS value - getdisplay().setFont(&DSEG7Classic_BoldItalic16pt7b); - getdisplay().setCursor(xPosTws, yPosTws); - getdisplay().print(swsValue); // Value -/* if (!wsBVal->valid) { - getdisplay().print("--.-"); } else { - wsValue = wsValue / 1000.0 * 1.94384; // Wind speed value in knots - if (wsValue < 10.0) { - getdisplay().printf("!%3.1f", wsValue); // Value, round to 1 decimal - } else { - getdisplay().printf("%4.1f", wsValue); // Value, round to 1 decimal - } - } */ - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(xPosTws + 82, yPosTws - 14); - getdisplay().print(wsName); // Name + // No valid data available + LOG_DEBUG(GwLog::LOG, "PageWindPlot: No valid data available"); + getdisplay().setFont(&Ubuntu_Bold10pt8b); + getdisplay().fillRect(xCenter - 33, height / 2 - 20, 66, 24, commonData->bgcolor); // Clear area for message + drawTextCenter(xCenter, height / 2 - 10, "No data"); + } + + // chart Y axis labels; print at last to overwrite potential chart lines in label area + int yPos; + int chrtLbl; getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(xPosTws + 82, yPosTws + 1); - getdisplay().print(wsUnit); // Unit - - } else { - // No valid data available - LOG_DEBUG(GwLog::LOG, "PageWindPlot: No valid data available"); - getdisplay().setFont(&Ubuntu_Bold10pt8b); - getdisplay().fillRect(xCenter - 33, height / 2 - 20, 66, 24, commonData->bgcolor); // Clear area for message - drawTextCenter(xCenter, height / 2 - 10, "No data"); - } - - // chart Y axis labels; print at last to overwrite potential chart lines in label area - int yPos; - int chrtLbl; - getdisplay().setFont(&Ubuntu_Bold8pt8b); - for (int i = 1; i <= 3; i++) { - yPos = yOffset + (i * 60); - getdisplay().fillRect(0, yPos, width, 1, commonData->fgcolor); - getdisplay().fillRect(0, yPos - 8, 24, 16, commonData->bgcolor); // Clear small area to remove potential chart lines - getdisplay().setCursor(1, yPos + 4); - if (count >= intvBufSize) { - // Calculate minute value for label - chrtLbl = ((i - 1 + (prevY < yOffset + 30)) * dataIntv) * -1; // change label if last data point is more than 30 lines (= seconds) from chart line - } else { - int j = 3 - i; - chrtLbl = (int((((numWndVals / dataIntv) - 50) * dataIntv / 60) + 1) - (j * dataIntv)) * -1; // 50 lines left below last chart line + for (int i = 1; i <= 3; i++) { + yPos = yOffset + (i * 60); + getdisplay().fillRect(0, yPos, width, 1, commonData->fgcolor); + getdisplay().fillRect(0, yPos - 8, 24, 16, commonData->bgcolor); // Clear small area to remove potential chart lines + getdisplay().setCursor(1, yPos + 4); + if (count >= intvBufSize) { + // Calculate minute value for label + chrtLbl = ((i - 1 + (prevY < yOffset + 30)) * dataIntv) * -1; // change label if last data point is more than 30 lines (= seconds) from chart line + } else { + int j = 3 - i; + chrtLbl = (int((((numWndVals / dataIntv) - 50) * dataIntv / 60) + 1) - (j * dataIntv)) * -1; // 50 lines left below last chart line + } + getdisplay().printf("%3d", chrtLbl); // Wind value label } - getdisplay().printf("%3d", chrtLbl); // Wind value label + } else if (chrtMode == 'S') { + wsValue = wsHstry->getLast(); + Chart twsChart(wsHstry, wsBVal, 0, 0, dataIntv, dfltRng, logger); + twsChart.drawChrtHdr(); + twsChart.drawChrtGrd(40); + + } else if (chrtMode == 'B') { } LOG_DEBUG(GwLog::DEBUG, "PageWindPlot time: %ld", millis() - timer); From 47fcb26961ee7f6defedc9692d87051e9a68368e Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Tue, 16 Sep 2025 20:22:09 +0200 Subject: [PATCH 006/121] add a generic s3 (devkit-m) to cibuild --- platformio.ini | 11 +++++++ webinstall/build.yaml | 71 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/platformio.ini b/platformio.ini index df144b8..4125eb7 100644 --- a/platformio.ini +++ b/platformio.ini @@ -185,3 +185,14 @@ build_flags = ${env.build_flags} upload_port = /dev/esp32 upload_protocol = esptool + +[env:s3devkitm-generic] +extends = sensors +board = esp32-s3-devkitm-1 +lib_deps = + ${env.lib_deps} + ${sensors.lib_deps} +build_flags = + ${env.build_flags} +upload_port = /dev/esp32 +upload_protocol = esptool diff --git a/webinstall/build.yaml b/webinstall/build.yaml index 0fcca66..6b954c8 100644 --- a/webinstall/build.yaml +++ b/webinstall/build.yaml @@ -233,6 +233,46 @@ types: - 33 - 37 - 38 + + - &gpiopinvs3 + - {label: unset,value:} + - {label: "0: boot mode control",value: 0} + - 1 + - 2 + - {label: "3: JTAG control", value: 3} + - 4 + - 5 + - 6 + - 7 + - 8 + - 9 + - 10 + - 11 + - 12 + - 13 + - 14 + - 17 + - 18 + - 19 + - 20 + - 21 + - 33 + - 34 + - 35 + - 36 + - 37 + - 38 + - 39 + - 40 + - 41 + - 42 + - 43 + - 44 + - {label: "45: strapping pin", value: 45} + - {label: "46: strapping pin", value: 46} + - 47 + - 48 + - &gpioinput type: dropdown @@ -728,6 +768,37 @@ config: description: "Node mcu esp32" url: "https://docs.platformio.org/en/stable/boards/espressif32/nodemcu-32s.html" resource: *esp32default + children: + - *serial1 + - *serial2 + - *can + - *resetButton + - *led + - <<: *iicsensors + base: + busname: "1" + bus: "" + - <<: *iicsensors + base: + busname: "2" + bus: "2" + - <<: *spisensors + base: + busname: "1" + bus: "1" + - <<: *spisensors + base: + busname: "2" + bus: "2" + + - value: s3devkitm-generic + label: s3devkitm + description: "esp32 s3 generic, 8MB flash, no PSRAM " + url: "https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/hw-reference/esp32s3/user-guide-devkitm-1.html" + resource: *esp32default + base: + gpiopinv: *gpiopinvs3 + gpioinputv: *gpiopinvs3 children: - *serial1 - *serial2 From 7ebd582ca02d4053a0578fc7e758a7244cfe5218 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Wed, 17 Sep 2025 17:39:57 +0200 Subject: [PATCH 007/121] set serial mode to RX for M5 GPS base --- lib/hardware/GwM5Base.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/hardware/GwM5Base.h b/lib/hardware/GwM5Base.h index f6371a9..2e88f51 100644 --- a/lib/hardware/GwM5Base.h +++ b/lib/hardware/GwM5Base.h @@ -35,12 +35,12 @@ #ifdef M5_GPS_KIT GWRESOURCE_USE(BASE,M5_GPS_KIT) GWRESOURCE_USE(SERIAL1,M5_GPS_KIT) - #define _GWI_SERIAL1 BOARD_LEFT1,-1,GWSERIAL_TYPE_UNI,9600 + #define _GWI_SERIAL1 BOARD_LEFT1,-1,GWSERIAL_TYPE_RX,9600 #endif #ifdef M5_GPSV2_KIT GWRESOURCE_USE(BASE,M5_GPSV2_KIT) GWRESOURCE_USE(SERIAL1,M5_GPSV2_KIT) - #define _GWI_SERIAL1 BOARD_LEFT1,-1,GWSERIAL_TYPE_UNI,115200 + #define _GWI_SERIAL1 BOARD_LEFT1,-1,GWSERIAL_TYPE_RX,115200 #endif //M5 ProtoHub From 3d131c7d98e49ba30f0fa3fe8d37fa8271844f17 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Wed, 17 Sep 2025 17:40:28 +0200 Subject: [PATCH 008/121] correctly set the mode/type for serial channels --- lib/channel/GwChannelInterface.h | 1 + lib/channel/GwChannelList.cpp | 25 +++++++++++++------------ lib/serial/GwSerial.cpp | 3 +++ lib/serial/GwSerial.h | 2 ++ lib/socketserver/GwSocketServer.cpp | 5 ++++- lib/socketserver/GwSocketServer.h | 1 + lib/socketserver/GwTcpClient.cpp | 5 ++++- lib/socketserver/GwTcpClient.h | 1 + lib/socketserver/GwUdpReader.cpp | 4 +++- lib/socketserver/GwUdpReader.h | 1 + lib/socketserver/GwUdpWriter.cpp | 5 ++++- lib/socketserver/GwUdpWriter.h | 1 + 12 files changed, 38 insertions(+), 16 deletions(-) diff --git a/lib/channel/GwChannelInterface.h b/lib/channel/GwChannelInterface.h index 68f519b..a028eff 100644 --- a/lib/channel/GwChannelInterface.h +++ b/lib/channel/GwChannelInterface.h @@ -7,4 +7,5 @@ class GwChannelInterface{ virtual size_t sendToClients(const char *buffer, int sourceId, bool partial=false)=0; virtual Stream * getStream(bool partialWrites){ return NULL;} virtual String getMode(){return "UNKNOWN";} + virtual int getType()=0; //return the numeric type }; \ No newline at end of file diff --git a/lib/channel/GwChannelList.cpp b/lib/channel/GwChannelList.cpp index c9db748..87755b4 100644 --- a/lib/channel/GwChannelList.cpp +++ b/lib/channel/GwChannelList.cpp @@ -285,8 +285,8 @@ static ChannelParam channelParameters[]={ }; template -GwSerial* createSerial(GwLog *logger, T* s,int id, bool canRead=true){ - return new GwSerialImpl(logger,s,id,canRead); +GwSerial* createSerial(GwLog *logger, T* s,int id, int type, bool canRead=true){ + return new GwSerialImpl(logger,s,id,type,canRead); } static ChannelParam * findChannelParam(int id){ @@ -300,7 +300,7 @@ static ChannelParam * findChannelParam(int id){ return param; } -static GwSerial * createSerialImpl(GwConfigHandler *config,GwLog *logger, int idx,int rx,int tx, bool setLog=false){ +static GwSerial * createSerialImpl(GwConfigHandler *config,GwLog *logger, int idx,int type,int rx,int tx, bool setLog){ LOG_DEBUG(GwLog::DEBUG,"create serial: channel=%d, rx=%d,tx=%d", idx,rx,tx); ChannelParam *param=findChannelParam(idx); @@ -312,13 +312,13 @@ static GwSerial * createSerialImpl(GwConfigHandler *config,GwLog *logger, int id GwLog *streamLog=setLog?nullptr:logger; switch(param->id){ case USB_CHANNEL_ID: - serialStream=createSerial(streamLog,&USBSerial,param->id); + serialStream=createSerial(streamLog,&USBSerial,param->id,type); break; case SERIAL1_CHANNEL_ID: - serialStream=createSerial(streamLog,&Serial1,param->id); + serialStream=createSerial(streamLog,&Serial1,param->id,type); break; case SERIAL2_CHANNEL_ID: - serialStream=createSerial(streamLog,&Serial2,param->id); + serialStream=createSerial(streamLog,&Serial2,param->id,type); break; } if (serialStream == nullptr){ @@ -332,12 +332,13 @@ static GwSerial * createSerialImpl(GwConfigHandler *config,GwLog *logger, int id } return serialStream; } -static GwChannel * createChannel(GwLog *logger, GwConfigHandler *config, int id,GwChannelInterface *impl, int type=GWSERIAL_TYPE_BI){ +static GwChannel * createChannel(GwLog *logger, GwConfigHandler *config, int id,GwChannelInterface *impl){ ChannelParam *param=findChannelParam(id); if (param == nullptr){ LOG_DEBUG(GwLog::ERROR,"invalid channel id %d",id); return nullptr; } + int type=impl->getType(); bool canRead=false; bool canWrite=false; bool validType=false; @@ -425,10 +426,10 @@ void GwChannelList::begin(bool fallbackSerial){ GwChannel *channel=NULL; //usb if (! fallbackSerial){ - GwSerial *usbSerial=createSerialImpl(config, logger,USB_CHANNEL_ID,GWUSB_RX,GWUSB_TX,true); + GwSerial *usbSerial=createSerialImpl(config, logger,USB_CHANNEL_ID,GWSERIAL_TYPE_BI,GWUSB_RX,GWUSB_TX,true); if (usbSerial != nullptr){ usbSerial->enableWriteLock(); //as it is used for logging we need this additionally - GwChannel *usbChannel=createChannel(logger,config,USB_CHANNEL_ID,usbSerial,GWSERIAL_TYPE_BI); + GwChannel *usbChannel=createChannel(logger,config,USB_CHANNEL_ID,usbSerial); if (usbChannel != nullptr){ addChannel(usbChannel); } @@ -445,9 +446,9 @@ void GwChannelList::begin(bool fallbackSerial){ //new serial config handling for (auto &&init:serialInits){ LOG_INFO("creating serial channel %d, rx=%d,tx=%d,type=%d",init.serial,init.rx,init.tx,init.mode); - GwSerial *ser=createSerialImpl(config,logger,init.serial,init.rx,init.tx); + GwSerial *ser=createSerialImpl(config,logger,init.serial,init.mode,init.rx,init.tx,false); if (ser != nullptr){ - channel=createChannel(logger,config,init.serial,ser,init.mode); + channel=createChannel(logger,config,init.serial,ser); if (channel != nullptr){ addChannel(channel); } @@ -466,8 +467,8 @@ void GwChannelList::begin(bool fallbackSerial){ config->getInt(config->remotePort), config->getBool(config->readTCL) ); + addChannel(createChannel(logger,config,TCP_CLIENT_CHANNEL_ID,client)); } - addChannel(createChannel(logger,config,TCP_CLIENT_CHANNEL_ID,client)); //udp writer if (config->getBool(GwConfigDefinitions::udpwEnabled)){ diff --git a/lib/serial/GwSerial.cpp b/lib/serial/GwSerial.cpp index b23c52f..e6ec320 100644 --- a/lib/serial/GwSerial.cpp +++ b/lib/serial/GwSerial.cpp @@ -79,6 +79,9 @@ String GwSerial::getMode(){ } return "UNKNOWN"; } +int GwSerial::getType() { + return type; +} bool GwSerial::isInitialized() { return initialized; } size_t GwSerial::enqueue(const uint8_t *data, size_t len, bool partial) diff --git a/lib/serial/GwSerial.h b/lib/serial/GwSerial.h index 1932878..51c9100 100644 --- a/lib/serial/GwSerial.h +++ b/lib/serial/GwSerial.h @@ -43,6 +43,7 @@ class GwSerial : public GwChannelInterface{ bool getAvailableWrite(){return availableWrite;} virtual void begin(unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1)=0; virtual String getMode() override; + virtual int getType() override; friend GwSerialStream; }; @@ -122,6 +123,7 @@ template setError(serial,logger); }; + }; diff --git a/lib/socketserver/GwSocketServer.cpp b/lib/socketserver/GwSocketServer.cpp index 185e3a4..53ccb82 100644 --- a/lib/socketserver/GwSocketServer.cpp +++ b/lib/socketserver/GwSocketServer.cpp @@ -4,6 +4,7 @@ #include "GwBuffer.h" #include "GwSocketConnection.h" #include "GwSocketHelper.h" +#include "GwHardware.h" GwSocketServer::GwSocketServer(const GwConfigHandler *config, GwLog *logger, int minId) { @@ -185,4 +186,6 @@ int GwSocketServer::numClients() } GwSocketServer::~GwSocketServer() { -} \ No newline at end of file +} + +int GwSocketServer::getType() {return GWSERIAL_TYPE_BI;} \ No newline at end of file diff --git a/lib/socketserver/GwSocketServer.h b/lib/socketserver/GwSocketServer.h index 248fc95..dcee8fc 100644 --- a/lib/socketserver/GwSocketServer.h +++ b/lib/socketserver/GwSocketServer.h @@ -27,5 +27,6 @@ class GwSocketServer: public GwChannelInterface{ virtual size_t sendToClients(const char *buf,int sourceId, bool partialWrite=false); int numClients(); virtual void readMessages(GwMessageFetcher *writer); + virtual int getType() override; }; #endif \ No newline at end of file diff --git a/lib/socketserver/GwTcpClient.cpp b/lib/socketserver/GwTcpClient.cpp index 2792300..f3761db 100644 --- a/lib/socketserver/GwTcpClient.cpp +++ b/lib/socketserver/GwTcpClient.cpp @@ -2,6 +2,7 @@ #include #include #include "GwSocketHelper.h" +#include "GwHardware.h" class ResolveArgs{ public: @@ -291,4 +292,6 @@ void GwTcpClient::setResolved(IPAddress addr, bool valid){ GwTcpClient::ResolvedAddress GwTcpClient::getResolved(){ GWSYNCHRONIZED(locker); return resolvedAddress; -} \ No newline at end of file +} + +int GwTcpClient::getType(){return GWSERIAL_TYPE_BI;} \ No newline at end of file diff --git a/lib/socketserver/GwTcpClient.h b/lib/socketserver/GwTcpClient.h index 25cc654..8665e04 100644 --- a/lib/socketserver/GwTcpClient.h +++ b/lib/socketserver/GwTcpClient.h @@ -53,4 +53,5 @@ public: virtual void readMessages(GwMessageFetcher *writer); bool isConnected(); String getError(){return error;} + virtual int getType() override; }; \ No newline at end of file diff --git a/lib/socketserver/GwUdpReader.cpp b/lib/socketserver/GwUdpReader.cpp index 612eb10..f5927be 100644 --- a/lib/socketserver/GwUdpReader.cpp +++ b/lib/socketserver/GwUdpReader.cpp @@ -5,6 +5,7 @@ #include "GwSocketConnection.h" #include "GwSocketHelper.h" #include "GWWifi.h" +#include "GwHardware.h" GwUdpReader::GwUdpReader(const GwConfigHandler *config, GwLog *logger, int minId) @@ -164,4 +165,5 @@ size_t GwUdpReader::sendToClients(const char *buf, int source,bool partial) GwUdpReader::~GwUdpReader() { -} \ No newline at end of file +} +int GwUdpReader::getType(){return GWSERIAL_TYPE_BI;} \ No newline at end of file diff --git a/lib/socketserver/GwUdpReader.h b/lib/socketserver/GwUdpReader.h index 08c56bb..199096d 100644 --- a/lib/socketserver/GwUdpReader.h +++ b/lib/socketserver/GwUdpReader.h @@ -41,5 +41,6 @@ class GwUdpReader: public GwChannelInterface{ virtual void loop(bool handleRead=true,bool handleWrite=true); virtual size_t sendToClients(const char *buf,int sourceId, bool partialWrite=false); virtual void readMessages(GwMessageFetcher *writer); + virtual int getType() override; }; #endif \ No newline at end of file diff --git a/lib/socketserver/GwUdpWriter.cpp b/lib/socketserver/GwUdpWriter.cpp index c91880e..3ab9236 100644 --- a/lib/socketserver/GwUdpWriter.cpp +++ b/lib/socketserver/GwUdpWriter.cpp @@ -5,6 +5,7 @@ #include "GwSocketConnection.h" #include "GwSocketHelper.h" #include "GWWifi.h" +#include "GwHardware.h" GwUdpWriter::WriterSocket::WriterSocket(GwLog *l,int p,const String &src,const String &dst, SourceMode sm) : sourceMode(sm), source(src), destination(dst), port(p),logger(l) @@ -200,4 +201,6 @@ size_t GwUdpWriter::sendToClients(const char *buf, int source,bool partial) GwUdpWriter::~GwUdpWriter() { -} \ No newline at end of file +} + +int GwUdpWriter::getType() {return GWSERIAL_TYPE_BI;} \ No newline at end of file diff --git a/lib/socketserver/GwUdpWriter.h b/lib/socketserver/GwUdpWriter.h index e17a17e..3299cde 100644 --- a/lib/socketserver/GwUdpWriter.h +++ b/lib/socketserver/GwUdpWriter.h @@ -69,5 +69,6 @@ class GwUdpWriter: public GwChannelInterface{ virtual void loop(bool handleRead=true,bool handleWrite=true); virtual size_t sendToClients(const char *buf,int sourceId, bool partialWrite=false); virtual void readMessages(GwMessageFetcher *writer); + virtual int getType() override; }; #endif \ No newline at end of file From d5a9568b67c4351db19fb74cab590c21c4501976 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Wed, 17 Sep 2025 17:57:03 +0200 Subject: [PATCH 009/121] make birectional channels the default, clean up some channel handling --- lib/channel/GwChannel.cpp | 13 +++++++++++++ lib/channel/GwChannel.h | 3 ++- lib/channel/GwChannelInterface.h | 4 ++-- lib/hardware/GwChannelModes.h | 23 +++++++++++++++++++++++ lib/hardware/GwHardware.h | 6 +----- lib/serial/GwSerial.cpp | 13 ------------- lib/serial/GwSerial.h | 1 - lib/socketserver/GwSocketServer.cpp | 3 --- lib/socketserver/GwSocketServer.h | 1 - lib/socketserver/GwTcpClient.cpp | 3 --- lib/socketserver/GwTcpClient.h | 1 - lib/socketserver/GwUdpReader.cpp | 5 +---- lib/socketserver/GwUdpReader.h | 1 - lib/socketserver/GwUdpWriter.cpp | 3 --- lib/socketserver/GwUdpWriter.h | 1 - 15 files changed, 42 insertions(+), 39 deletions(-) create mode 100644 lib/hardware/GwChannelModes.h diff --git a/lib/channel/GwChannel.cpp b/lib/channel/GwChannel.cpp index 9c79983..1a0d06f 100644 --- a/lib/channel/GwChannel.cpp +++ b/lib/channel/GwChannel.cpp @@ -249,3 +249,16 @@ unsigned long GwChannel::countTx(){ if (! countOut) return 0UL; return countOut->getGlobal(); } +String GwChannel::typeString(int type){ + switch (type){ + case GWSERIAL_TYPE_UNI: + return "UNI"; + case GWSERIAL_TYPE_BI: + return "BI"; + case GWSERIAL_TYPE_RX: + return "RX"; + case GWSERIAL_TYPE_TX: + return "TX"; + } + return "UNKNOWN"; +} \ No newline at end of file diff --git a/lib/channel/GwChannel.h b/lib/channel/GwChannel.h index 66fb4ae..6b34432 100644 --- a/lib/channel/GwChannel.h +++ b/lib/channel/GwChannel.h @@ -77,7 +77,8 @@ class GwChannel{ if (maxSourceId < 0) return source == sourceId; return (source >= sourceId && source <= maxSourceId); } - String getMode(){return impl->getMode();} + static String typeString(int type); + String getMode(){return typeString(impl->getType());} int getMinId(){return sourceId;}; }; diff --git a/lib/channel/GwChannelInterface.h b/lib/channel/GwChannelInterface.h index a028eff..30550d9 100644 --- a/lib/channel/GwChannelInterface.h +++ b/lib/channel/GwChannelInterface.h @@ -1,11 +1,11 @@ #pragma once #include "GwBuffer.h" +#include "GwChannelModes.h" class GwChannelInterface{ public: virtual void loop(bool handleRead,bool handleWrite)=0; virtual void readMessages(GwMessageFetcher *writer)=0; virtual size_t sendToClients(const char *buffer, int sourceId, bool partial=false)=0; virtual Stream * getStream(bool partialWrites){ return NULL;} - virtual String getMode(){return "UNKNOWN";} - virtual int getType()=0; //return the numeric type + virtual int getType(){ return GWSERIAL_TYPE_BI;} //return the numeric type }; \ No newline at end of file diff --git a/lib/hardware/GwChannelModes.h b/lib/hardware/GwChannelModes.h new file mode 100644 index 0000000..a9d7757 --- /dev/null +++ b/lib/hardware/GwChannelModes.h @@ -0,0 +1,23 @@ +/* + This code is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This code is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + defines for the channel modes(types) +*/ +#ifndef _GWCHANNELMODES_H +#define _GWCHANNELMODES_H +#define GWSERIAL_TYPE_UNI 1 +#define GWSERIAL_TYPE_BI 2 +#define GWSERIAL_TYPE_RX 3 +#define GWSERIAL_TYPE_TX 4 +#define GWSERIAL_TYPE_UNK 0 +#endif \ No newline at end of file diff --git a/lib/hardware/GwHardware.h b/lib/hardware/GwHardware.h index 0b14ab3..41bca41 100644 --- a/lib/hardware/GwHardware.h +++ b/lib/hardware/GwHardware.h @@ -20,11 +20,7 @@ #endif #ifndef _GWHARDWARE_H #define _GWHARDWARE_H -#define GWSERIAL_TYPE_UNI 1 -#define GWSERIAL_TYPE_BI 2 -#define GWSERIAL_TYPE_RX 3 -#define GWSERIAL_TYPE_TX 4 -#define GWSERIAL_TYPE_UNK 0 +#include "GwChannelModes.h" #include #include #include "GwAppInfo.h" diff --git a/lib/serial/GwSerial.cpp b/lib/serial/GwSerial.cpp index e6ec320..ed469f9 100644 --- a/lib/serial/GwSerial.cpp +++ b/lib/serial/GwSerial.cpp @@ -66,19 +66,6 @@ GwSerial::~GwSerial() if (lock != nullptr) vSemaphoreDelete(lock); } -String GwSerial::getMode(){ - switch (type){ - case GWSERIAL_TYPE_UNI: - return "UNI"; - case GWSERIAL_TYPE_BI: - return "BI"; - case GWSERIAL_TYPE_RX: - return "RX"; - case GWSERIAL_TYPE_TX: - return "TX"; - } - return "UNKNOWN"; -} int GwSerial::getType() { return type; } diff --git a/lib/serial/GwSerial.h b/lib/serial/GwSerial.h index 51c9100..ae2ca1f 100644 --- a/lib/serial/GwSerial.h +++ b/lib/serial/GwSerial.h @@ -42,7 +42,6 @@ class GwSerial : public GwChannelInterface{ virtual Stream *getStream(bool partialWrites); bool getAvailableWrite(){return availableWrite;} virtual void begin(unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1)=0; - virtual String getMode() override; virtual int getType() override; friend GwSerialStream; }; diff --git a/lib/socketserver/GwSocketServer.cpp b/lib/socketserver/GwSocketServer.cpp index 53ccb82..0e0b79e 100644 --- a/lib/socketserver/GwSocketServer.cpp +++ b/lib/socketserver/GwSocketServer.cpp @@ -4,7 +4,6 @@ #include "GwBuffer.h" #include "GwSocketConnection.h" #include "GwSocketHelper.h" -#include "GwHardware.h" GwSocketServer::GwSocketServer(const GwConfigHandler *config, GwLog *logger, int minId) { @@ -187,5 +186,3 @@ int GwSocketServer::numClients() GwSocketServer::~GwSocketServer() { } - -int GwSocketServer::getType() {return GWSERIAL_TYPE_BI;} \ No newline at end of file diff --git a/lib/socketserver/GwSocketServer.h b/lib/socketserver/GwSocketServer.h index dcee8fc..248fc95 100644 --- a/lib/socketserver/GwSocketServer.h +++ b/lib/socketserver/GwSocketServer.h @@ -27,6 +27,5 @@ class GwSocketServer: public GwChannelInterface{ virtual size_t sendToClients(const char *buf,int sourceId, bool partialWrite=false); int numClients(); virtual void readMessages(GwMessageFetcher *writer); - virtual int getType() override; }; #endif \ No newline at end of file diff --git a/lib/socketserver/GwTcpClient.cpp b/lib/socketserver/GwTcpClient.cpp index f3761db..a2c68eb 100644 --- a/lib/socketserver/GwTcpClient.cpp +++ b/lib/socketserver/GwTcpClient.cpp @@ -2,7 +2,6 @@ #include #include #include "GwSocketHelper.h" -#include "GwHardware.h" class ResolveArgs{ public: @@ -293,5 +292,3 @@ GwTcpClient::ResolvedAddress GwTcpClient::getResolved(){ GWSYNCHRONIZED(locker); return resolvedAddress; } - -int GwTcpClient::getType(){return GWSERIAL_TYPE_BI;} \ No newline at end of file diff --git a/lib/socketserver/GwTcpClient.h b/lib/socketserver/GwTcpClient.h index 8665e04..25cc654 100644 --- a/lib/socketserver/GwTcpClient.h +++ b/lib/socketserver/GwTcpClient.h @@ -53,5 +53,4 @@ public: virtual void readMessages(GwMessageFetcher *writer); bool isConnected(); String getError(){return error;} - virtual int getType() override; }; \ No newline at end of file diff --git a/lib/socketserver/GwUdpReader.cpp b/lib/socketserver/GwUdpReader.cpp index f5927be..29d854e 100644 --- a/lib/socketserver/GwUdpReader.cpp +++ b/lib/socketserver/GwUdpReader.cpp @@ -5,8 +5,6 @@ #include "GwSocketConnection.h" #include "GwSocketHelper.h" #include "GWWifi.h" -#include "GwHardware.h" - GwUdpReader::GwUdpReader(const GwConfigHandler *config, GwLog *logger, int minId) { @@ -165,5 +163,4 @@ size_t GwUdpReader::sendToClients(const char *buf, int source,bool partial) GwUdpReader::~GwUdpReader() { -} -int GwUdpReader::getType(){return GWSERIAL_TYPE_BI;} \ No newline at end of file +} \ No newline at end of file diff --git a/lib/socketserver/GwUdpReader.h b/lib/socketserver/GwUdpReader.h index 199096d..08c56bb 100644 --- a/lib/socketserver/GwUdpReader.h +++ b/lib/socketserver/GwUdpReader.h @@ -41,6 +41,5 @@ class GwUdpReader: public GwChannelInterface{ virtual void loop(bool handleRead=true,bool handleWrite=true); virtual size_t sendToClients(const char *buf,int sourceId, bool partialWrite=false); virtual void readMessages(GwMessageFetcher *writer); - virtual int getType() override; }; #endif \ No newline at end of file diff --git a/lib/socketserver/GwUdpWriter.cpp b/lib/socketserver/GwUdpWriter.cpp index 3ab9236..ef0020c 100644 --- a/lib/socketserver/GwUdpWriter.cpp +++ b/lib/socketserver/GwUdpWriter.cpp @@ -5,7 +5,6 @@ #include "GwSocketConnection.h" #include "GwSocketHelper.h" #include "GWWifi.h" -#include "GwHardware.h" GwUdpWriter::WriterSocket::WriterSocket(GwLog *l,int p,const String &src,const String &dst, SourceMode sm) : sourceMode(sm), source(src), destination(dst), port(p),logger(l) @@ -202,5 +201,3 @@ size_t GwUdpWriter::sendToClients(const char *buf, int source,bool partial) GwUdpWriter::~GwUdpWriter() { } - -int GwUdpWriter::getType() {return GWSERIAL_TYPE_BI;} \ No newline at end of file diff --git a/lib/socketserver/GwUdpWriter.h b/lib/socketserver/GwUdpWriter.h index 3299cde..e17a17e 100644 --- a/lib/socketserver/GwUdpWriter.h +++ b/lib/socketserver/GwUdpWriter.h @@ -69,6 +69,5 @@ class GwUdpWriter: public GwChannelInterface{ virtual void loop(bool handleRead=true,bool handleWrite=true); virtual size_t sendToClients(const char *buf,int sourceId, bool partialWrite=false); virtual void readMessages(GwMessageFetcher *writer); - virtual int getType() override; }; #endif \ No newline at end of file From 95df5858accce238f84ea559ee1990bde4315376 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Wed, 17 Sep 2025 20:05:45 +0200 Subject: [PATCH 010/121] #112: clearify licenses to be GPL v2 or later --- Readme.md | 4 ++++ lib/config/GwConverterConfig.h | 2 +- lib/hardware/GwChannelModes.h | 2 +- lib/hardware/GwHardware.h | 2 +- lib/hardware/GwM5Base.h | 2 +- lib/hardware/GwM5Grove.h | 2 +- lib/hardware/GwM5Grove.in | 2 +- lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h | 2 +- lib/sensors/GwSensor.cpp | 2 +- lib/sensors/GwSensor.h | 2 +- lib/spitask/GWDMS22B.cpp | 2 +- lib/spitask/GWDMS22B.h | 2 +- lib/spitask/GwSpiSensor.h | 2 +- lib/spitask/GwSpiTask.cpp | 2 +- lib/spitask/GwSpiTask.h | 2 +- src/main.cpp | 2 +- 16 files changed, 19 insertions(+), 15 deletions(-) diff --git a/Readme.md b/Readme.md index a9a305f..d3fae7f 100644 --- a/Readme.md +++ b/Readme.md @@ -43,6 +43,10 @@ What is included For the details of the mapped PGNs and NMEA sentences refer to [Conversions](doc/Conversions.pdf). +License +------- +This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either [version 2 of the License](LICENSE), or (at your option) any later version. + Hardware -------- The software is prepared to run on different kinds of ESP32 based modules and accessoirs. For some of them prebuild binaries are available that only need to be flashed, others would require to add some definitions of the used PINs and features and to build the binary. diff --git a/lib/config/GwConverterConfig.h b/lib/config/GwConverterConfig.h index 4a93a71..25ca39e 100644 --- a/lib/config/GwConverterConfig.h +++ b/lib/config/GwConverterConfig.h @@ -3,7 +3,7 @@ This code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + version 2 of the License, or (at your option) any later version. This code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU diff --git a/lib/hardware/GwChannelModes.h b/lib/hardware/GwChannelModes.h index a9d7757..e6c42de 100644 --- a/lib/hardware/GwChannelModes.h +++ b/lib/hardware/GwChannelModes.h @@ -2,7 +2,7 @@ This code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + version 2 of the License, or (at your option) any later version. This code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU diff --git a/lib/hardware/GwHardware.h b/lib/hardware/GwHardware.h index 41bca41..10a7404 100644 --- a/lib/hardware/GwHardware.h +++ b/lib/hardware/GwHardware.h @@ -2,7 +2,7 @@ This code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + version 2 of the License, or (at your option) any later version. This code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU diff --git a/lib/hardware/GwM5Base.h b/lib/hardware/GwM5Base.h index 2e88f51..67c865e 100644 --- a/lib/hardware/GwM5Base.h +++ b/lib/hardware/GwM5Base.h @@ -2,7 +2,7 @@ This code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + version 2 of the License, or (at your option) any later version. This code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU diff --git a/lib/hardware/GwM5Grove.h b/lib/hardware/GwM5Grove.h index 220ee01..44761a1 100644 --- a/lib/hardware/GwM5Grove.h +++ b/lib/hardware/GwM5Grove.h @@ -2,7 +2,7 @@ This code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + version 2 of the License, or (at your option) any later version. This code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU diff --git a/lib/hardware/GwM5Grove.in b/lib/hardware/GwM5Grove.in index 9858783..c98d4d1 100644 --- a/lib/hardware/GwM5Grove.in +++ b/lib/hardware/GwM5Grove.in @@ -2,7 +2,7 @@ This code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + version 2 of the License, or (at your option) any later version. This code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU diff --git a/lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h b/lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h index 169f181..a627952 100644 --- a/lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h +++ b/lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h @@ -2,7 +2,7 @@ This code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + version 2 of the License, or (at your option) any later version. This code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU diff --git a/lib/sensors/GwSensor.cpp b/lib/sensors/GwSensor.cpp index d0d580b..abdb508 100644 --- a/lib/sensors/GwSensor.cpp +++ b/lib/sensors/GwSensor.cpp @@ -3,7 +3,7 @@ This code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + version 2 of the License, or (at your option) any later version. This code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU diff --git a/lib/sensors/GwSensor.h b/lib/sensors/GwSensor.h index 3077360..48eb5b2 100644 --- a/lib/sensors/GwSensor.h +++ b/lib/sensors/GwSensor.h @@ -3,7 +3,7 @@ This code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + version 2 of the License, or (at your option) any later version. This code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU diff --git a/lib/spitask/GWDMS22B.cpp b/lib/spitask/GWDMS22B.cpp index fca345e..0447fb2 100644 --- a/lib/spitask/GWDMS22B.cpp +++ b/lib/spitask/GWDMS22B.cpp @@ -3,7 +3,7 @@ This code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + version 2 of the License, or (at your option) any later version. This code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU diff --git a/lib/spitask/GWDMS22B.h b/lib/spitask/GWDMS22B.h index de53804..29a0b1b 100644 --- a/lib/spitask/GWDMS22B.h +++ b/lib/spitask/GWDMS22B.h @@ -3,7 +3,7 @@ This code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + version 2 of the License, or (at your option) any later version. This code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU diff --git a/lib/spitask/GwSpiSensor.h b/lib/spitask/GwSpiSensor.h index c12a410..ec21afe 100644 --- a/lib/spitask/GwSpiSensor.h +++ b/lib/spitask/GwSpiSensor.h @@ -3,7 +3,7 @@ This code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + version 2 of the License, or (at your option) any later version. This code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU diff --git a/lib/spitask/GwSpiTask.cpp b/lib/spitask/GwSpiTask.cpp index a5dda8a..54f9f3d 100644 --- a/lib/spitask/GwSpiTask.cpp +++ b/lib/spitask/GwSpiTask.cpp @@ -3,7 +3,7 @@ This code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + version 2 of the License, or (at your option) any later version. This code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU diff --git a/lib/spitask/GwSpiTask.h b/lib/spitask/GwSpiTask.h index 0714a31..b42b462 100644 --- a/lib/spitask/GwSpiTask.h +++ b/lib/spitask/GwSpiTask.h @@ -3,7 +3,7 @@ This code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + version 2 of the License, or (at your option) any later version. This code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU diff --git a/src/main.cpp b/src/main.cpp index 6daaa51..de56930 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,7 +3,7 @@ This code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + version 2 of the License, or (at your option) any later version. This code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU From e8c5440a7924ca0c4f6c18fe1840975f06b5b39d Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Fri, 19 Sep 2025 12:13:41 +0200 Subject: [PATCH 011/121] #114: correctly distinguish between type 1..3 ais messages when converting from N2K --- lib/nmea2kto0183/N2kDataToNMEA0183.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp index dbf6af5..23ce436 100644 --- a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp +++ b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp @@ -713,7 +713,6 @@ private: void HandleAISClassAPosReport(const tN2kMsg &N2kMsg) { - unsigned char SID; tN2kAISRepeat _Repeat; uint32_t _UserID; // MMSI double _Latitude =N2kDoubleNA; @@ -732,7 +731,7 @@ private: uint8_t _MessageType = 1; tNMEA0183AISMsg NMEA0183AISMsg; - if (ParseN2kPGN129038(N2kMsg, SID, _Repeat, _UserID, _Latitude, _Longitude, _Accuracy, _RAIM, _Seconds, + if (ParseN2kPGN129038(N2kMsg, _MessageType, _Repeat, _UserID, _Latitude, _Longitude, _Accuracy, _RAIM, _Seconds, _COG, _SOG, _Heading, _ROT, _NavStatus,_AISTransceiverInformation,_SID)) { @@ -770,6 +769,7 @@ private: Serial.println(_NavStatus); #endif + if (_MessageType < 1 || _MessageType > 3) _MessageType=1; //only allow type 1...3 for 129038 if (SetAISClassABMessage1(NMEA0183AISMsg, _MessageType, _Repeat, _UserID, _Latitude, _Longitude, _Accuracy, _RAIM, _Seconds, _COG, _SOG, _Heading, _ROT, _NavStatus)) { From 7fd1457296cb0de70a7c4b92c59603c14ed983b2 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Fri, 19 Sep 2025 20:43:28 +0200 Subject: [PATCH 012/121] add conversion from NMEA0183 AIS type 21 (aton) to PGN 129041 --- lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h | 37 +++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h b/lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h index a627952..70ba754 100644 --- a/lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h +++ b/lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h @@ -27,6 +27,8 @@ const double nmTom = 1.852 * 1000; uint16_t DaysSince1970 = 0; +#define boolbit(b) (b?1:0) + class MyAisDecoder : public AIS::AisDecoder { public: @@ -255,9 +257,40 @@ class MyAisDecoder : public AIS::AisDecoder send(N2kMsg); } - - virtual void onType21(unsigned int , unsigned int , const std::string &, bool , int , int , unsigned int , unsigned int , unsigned int , unsigned int ) override { + //mmsi, aidType, name + nameExt, posAccuracy, posLon, posLat, toBow, toStern, toPort, toStarboard + virtual void onType21(unsigned int mmsi , unsigned int aidType , const std::string & name, bool accuracy, int posLon, int posLat, unsigned int toBow, unsigned int toStern, unsigned int toPort, unsigned int toStarboard) override { //Serial.println("21"); + //in principle we should use tN2kAISAtoNReportData to directly call the library + //function for 129041. But this makes the conversion really complex. + int repeat=0; //TODO: should be part of the parameters + int seconds=0; + bool raim=false; + bool offPosition=false; + bool assignedMode=false; + bool virtualAton=false; + tN2kGNSStype gnssType=tN2kGNSStype::N2kGNSSt_GPS; //canboat considers 0 as undefined... + tN2kAISTransceiverInformation transceiverInfo=tN2kAISTransceiverInformation::N2kaischannel_A_VDL_reception; + tN2kMsg N2kMsg; + N2kMsg.SetPGN(129041); + N2kMsg.Priority=4; + N2kMsg.AddByte((repeat & 0x03) << 6 | (21 & 0x3f)); + N2kMsg.Add4ByteUInt(mmsi); //N2kData.UserID + N2kMsg.Add4ByteDouble(posLon / 600000.0, 1e-07); + N2kMsg.Add4ByteDouble(posLat / 600000.0, 1e-07); + N2kMsg.AddByte((seconds & 0x3f)<<2 | boolbit(raim)<<1 | boolbit(accuracy)); + N2kMsg.Add2ByteUDouble(toBow+toStern, 0.1); + N2kMsg.Add2ByteUDouble(toPort+toStarboard, 0.1); + N2kMsg.Add2ByteUDouble(toStarboard, 0.1); + N2kMsg.Add2ByteUDouble(toBow, 0.1); + N2kMsg.AddByte(boolbit(assignedMode) << 7 + | boolbit(virtualAton) << 6 + | boolbit(offPosition) << 5 + | (aidType & 0x1f)); + N2kMsg.AddByte((gnssType & 0x0F) << 1 | 0xe0); + N2kMsg.AddByte(N2kUInt8NA); //status + N2kMsg.AddByte((transceiverInfo & 0x1f) | 0xe0); + N2kMsg.AddVarStr(name.c_str()); + send(N2kMsg); } virtual void onType24A(unsigned int _uMsgType, unsigned int _repeat, unsigned int _uMmsi, From 6e0d56316b76e6f13895326f4133e43818ed01bc Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Sun, 21 Sep 2025 16:40:24 +0200 Subject: [PATCH 013/121] add some testing tools --- tools/gen3byte.py | 32 ++++++++++++++++++++++++++++++++ tools/sendDelay.py | 19 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100755 tools/gen3byte.py create mode 100755 tools/sendDelay.py diff --git a/tools/gen3byte.py b/tools/gen3byte.py new file mode 100755 index 0000000..2d84518 --- /dev/null +++ b/tools/gen3byte.py @@ -0,0 +1,32 @@ +#! /usr/bin/env python3 +#generate 3 byte codes for the RGB bytes +#refer to https://controllerstech.com/ws2812-leds-using-spi/ +ONE_BIT='110' +ZERO_BIT='100' + +currentStr='' + +def checkAndPrint(curr): + if len(curr) >= 8: + print("0b%s,"%curr[0:8],end='') + return curr[8:] + return curr +first=True + +print("uint8_t colorTo3Byte[256][3]=") +print("{") +for i in range(0,256): + if not first: + print("},") + first=False + print("{/*%02d*/"%i,end='') + mask=0x80 + for b in range(0,8): + if (i & mask) != 0: + currentStr+=ONE_BIT + else: + currentStr+=ZERO_BIT + mask=mask >> 1 + currentStr=checkAndPrint(currentStr) +print("}") +print("};") diff --git a/tools/sendDelay.py b/tools/sendDelay.py new file mode 100755 index 0000000..ab91737 --- /dev/null +++ b/tools/sendDelay.py @@ -0,0 +1,19 @@ +#! /usr/bin/env python3 +import sys +import os +import time + +def usage(): + print(f"usage: {sys.argv[0]} file delay") + sys.exit(1) + +if len(sys.argv) < 3: + usage() + +delay=float(sys.argv[2]) +fn=sys.argv[1] +with open (fn,"r") as fh: + for line in fh: + print(line,end="",flush=True) + time.sleep(delay) + From e578b428c9b10ce0e6bb4da1439d8885583d6f13 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Sun, 21 Sep 2025 19:32:02 +0200 Subject: [PATCH 014/121] more parameters for AIS type 21 --- lib/aisparser/ais_decoder.cpp | 14 ++++--- lib/aisparser/ais_decoder.h | 3 +- lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h | 51 +++++++++++------------ 3 files changed, 35 insertions(+), 33 deletions(-) diff --git a/lib/aisparser/ais_decoder.cpp b/lib/aisparser/ais_decoder.cpp index 6fece95..3c5dc00 100644 --- a/lib/aisparser/ais_decoder.cpp +++ b/lib/aisparser/ais_decoder.cpp @@ -627,7 +627,7 @@ void AisDecoder::decodeType21(PayloadBuffer &_buffer, unsigned int _uMsgType, in } // decode message fields (binary buffer has to go through all fields, but some fields are not used) - _buffer.getUnsignedValue(2); // repeatIndicator + auto repeat=_buffer.getUnsignedValue(2); // repeatIndicator auto mmsi = _buffer.getUnsignedValue(30); auto aidType = _buffer.getUnsignedValue(5); auto name = _buffer.getString(120); @@ -640,11 +640,11 @@ void AisDecoder::decodeType21(PayloadBuffer &_buffer, unsigned int _uMsgType, in auto toStarboard = _buffer.getUnsignedValue(6); _buffer.getUnsignedValue(4); // epfd type - _buffer.getUnsignedValue(6); // timestamp - _buffer.getBoolValue(); // off position + auto timestamp=_buffer.getUnsignedValue(6); // timestamp + auto offPosition=_buffer.getBoolValue(); // off position _buffer.getUnsignedValue(8); // reserved - _buffer.getBoolValue(); // RAIM - _buffer.getBoolValue(); // virtual aid + auto raim=_buffer.getBoolValue(); // RAIM + auto virtualAton=_buffer.getBoolValue(); // virtual aid _buffer.getBoolValue(); // assigned mode _buffer.getUnsignedValue(1); // spare @@ -654,7 +654,9 @@ void AisDecoder::decodeType21(PayloadBuffer &_buffer, unsigned int _uMsgType, in nameExt = _buffer.getString(88); } - onType21(mmsi, aidType, name + nameExt, posAccuracy, posLon, posLat, toBow, toStern, toPort, toStarboard); + onType21(mmsi, aidType, name + nameExt, posAccuracy, posLon, posLat, + toBow, toStern, toPort, toStarboard, + repeat,timestamp, raim, virtualAton, offPosition); } /* decode Voyage Report and Static Data (type nibble already pulled from buffer) */ diff --git a/lib/aisparser/ais_decoder.h b/lib/aisparser/ais_decoder.h index c908839..7872d3b 100644 --- a/lib/aisparser/ais_decoder.h +++ b/lib/aisparser/ais_decoder.h @@ -297,7 +297,8 @@ namespace AIS bool assigned, unsigned int repeat, bool raim) = 0; virtual void onType21(unsigned int _uMmsi, unsigned int _uAidType, const std::string &_strName, bool _bPosAccuracy, int _iPosLon, int _iPosLat, - unsigned int _uToBow, unsigned int _uToStern, unsigned int _uToPort, unsigned int _uToStarboard) = 0; + unsigned int _uToBow, unsigned int _uToStern, unsigned int _uToPort, unsigned int _uToStarboard, + unsigned int repeat,unsigned int timestamp, bool raim, bool virtualAton, bool offPosition) = 0; virtual void onType24A(unsigned int _uMsgType, unsigned int _repeat, unsigned int _uMmsi, const std::string &_strName) = 0; diff --git a/lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h b/lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h index 70ba754..5f4e5ee 100644 --- a/lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h +++ b/lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h @@ -84,25 +84,24 @@ class MyAisDecoder : public AIS::AisDecoder tN2kMsg N2kMsg; - // PGN129038 - - N2kMsg.SetPGN(129038L); - N2kMsg.Priority = 4; - N2kMsg.AddByte((_Repeat & 0x03) << 6 | (_uMsgType & 0x3f)); - N2kMsg.Add4ByteUInt(_uMmsi); - N2kMsg.Add4ByteDouble(_iPosLon / 600000.0, 1e-07); - N2kMsg.Add4ByteDouble(_iPosLat / 600000.0, 1e-07); - N2kMsg.AddByte((_timestamp & 0x3f) << 2 | (_Raim & 0x01) << 1 | (_bPosAccuracy & 0x01)); - N2kMsg.Add2ByteUDouble(decodeCog(_iCog), 1e-04); - N2kMsg.Add2ByteUDouble(_uSog * knToms/10.0, 0.01); - N2kMsg.AddByte(0x00); // Communication State (19 bits) - N2kMsg.AddByte(0x00); - N2kMsg.AddByte(0x00); // AIS transceiver information (5 bits) - N2kMsg.Add2ByteUDouble(decodeHeading(_iHeading), 1e-04); - N2kMsg.Add2ByteDouble(decodeRot(_iRot), 3.125E-05); // 1e-3/32.0 - N2kMsg.AddByte(0xF0 | (_uNavstatus & 0x0f)); - N2kMsg.AddByte(0xff); // Reserved - N2kMsg.AddByte(0xff); // SID (NA) + SetN2kPGN129038( + N2kMsg, + _uMsgType, + (tN2kAISRepeat)_Repeat, + _uMmsi, + _iPosLon/ 600000.0, + _iPosLat / 600000.0, + _bPosAccuracy, + _Raim, + _timestamp, + decodeCog(_iCog), + _uSog * knToms/10.0, + tN2kAISTransceiverInformation::N2kaischannel_A_VDL_reception, + decodeHeading(_iHeading), + decodeRot(_iRot), + (tN2kAISNavStatus)_uNavstatus, + 0xff + ); send(N2kMsg); } @@ -258,16 +257,14 @@ class MyAisDecoder : public AIS::AisDecoder } //mmsi, aidType, name + nameExt, posAccuracy, posLon, posLat, toBow, toStern, toPort, toStarboard - virtual void onType21(unsigned int mmsi , unsigned int aidType , const std::string & name, bool accuracy, int posLon, int posLat, unsigned int toBow, unsigned int toStern, unsigned int toPort, unsigned int toStarboard) override { + virtual void onType21(unsigned int mmsi , unsigned int aidType , const std::string & name, bool accuracy, int posLon, int posLat, unsigned int toBow, + unsigned int toStern, unsigned int toPort, unsigned int toStarboard, + unsigned int repeat,unsigned int timestamp, bool raim, bool virtualAton, bool offPosition) override { //Serial.println("21"); + //the name can be at most 120bit+88bit (35 byte) + termination -> 36 Byte //in principle we should use tN2kAISAtoNReportData to directly call the library //function for 129041. But this makes the conversion really complex. - int repeat=0; //TODO: should be part of the parameters - int seconds=0; - bool raim=false; - bool offPosition=false; bool assignedMode=false; - bool virtualAton=false; tN2kGNSStype gnssType=tN2kGNSStype::N2kGNSSt_GPS; //canboat considers 0 as undefined... tN2kAISTransceiverInformation transceiverInfo=tN2kAISTransceiverInformation::N2kaischannel_A_VDL_reception; tN2kMsg N2kMsg; @@ -277,7 +274,7 @@ class MyAisDecoder : public AIS::AisDecoder N2kMsg.Add4ByteUInt(mmsi); //N2kData.UserID N2kMsg.Add4ByteDouble(posLon / 600000.0, 1e-07); N2kMsg.Add4ByteDouble(posLat / 600000.0, 1e-07); - N2kMsg.AddByte((seconds & 0x3f)<<2 | boolbit(raim)<<1 | boolbit(accuracy)); + N2kMsg.AddByte((timestamp & 0x3f)<<2 | boolbit(raim)<<1 | boolbit(accuracy)); N2kMsg.Add2ByteUDouble(toBow+toStern, 0.1); N2kMsg.Add2ByteUDouble(toPort+toStarboard, 0.1); N2kMsg.Add2ByteUDouble(toStarboard, 0.1); @@ -289,6 +286,8 @@ class MyAisDecoder : public AIS::AisDecoder N2kMsg.AddByte((gnssType & 0x0F) << 1 | 0xe0); N2kMsg.AddByte(N2kUInt8NA); //status N2kMsg.AddByte((transceiverInfo & 0x1f) | 0xe0); + //bit offset 208 (see canboat/pgns.xml) -> 26 bytes from start + //as MaxDataLen is 223 and the string can be at most 36 bytes + 2 byte heading - no further check here N2kMsg.AddVarStr(name.c_str()); send(N2kMsg); } From 3df2571ca25c1bdcccfb1a6eb673b177428bf057 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Sun, 21 Sep 2025 21:02:31 +0200 Subject: [PATCH 015/121] intermediate: prepare AIS class 21 to 129041 --- lib/nmea2kto0183/N2kDataToNMEA0183.cpp | 35 ++ lib/nmea2ktoais/NMEA0183AISMessages.cpp | 574 ------------------------ lib/nmea2ktoais/NMEA0183AISMessages.h | 86 ---- lib/nmea2ktoais/NMEA0183AISMsg.cpp | 297 ------------ lib/nmea2ktoais/NMEA0183AISMsg.h | 92 ---- lib/nmea2ktoais/README.md | 49 -- platformio.ini | 25 ++ 7 files changed, 60 insertions(+), 1098 deletions(-) delete mode 100644 lib/nmea2ktoais/NMEA0183AISMessages.cpp delete mode 100644 lib/nmea2ktoais/NMEA0183AISMessages.h delete mode 100644 lib/nmea2ktoais/NMEA0183AISMsg.cpp delete mode 100644 lib/nmea2ktoais/NMEA0183AISMsg.h delete mode 100644 lib/nmea2ktoais/README.md diff --git a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp index 23ce436..945ad4c 100644 --- a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp +++ b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp @@ -1076,6 +1076,40 @@ private: return; } + //***************************************************************************** + // PGN 129041 Aton + void HandleAISMessage21(const tN2kMsg &N2kMsg) + { + tN2kAISAtoNReportData data; + if (ParseN2kPGN129041(N2kMsg,data)){ + tNMEA0183AISMsg nmea0183Msg; + if (SetAISMessage21( + nmea0183Msg, + data.Repeat, + data.UserID, + data.Latitude, + data.Longitude, + data.Accuracy, + data.RAIM, + data.Seconds, + data.Length, + data.Beam, + data.PositionReferenceStarboard, + data.PositionReferenceTrueNorth, + data.AtoNType, + data.OffPositionIndicator, + data.VirtualAtoNFlag, + data.AssignedModeFlag, + data.GNSSType, + data.AtoNStatus, + data.AISTransceiverInformation, + data.AtoNName + )){ + //TODO: SendMessage(nmea0183Msg); + } + } + } + void HandleSystemTime(const tN2kMsg &msg){ unsigned char sid=-1; uint16_t DaysSince1970=N2kUInt16NA; @@ -1614,6 +1648,7 @@ private: converters.registerConverter(129794UL, &N2kToNMEA0183Functions::HandleAISClassAMessage5); // AIS Class A Ship Static and Voyage related data, Message Type 5 converters.registerConverter(129809UL, &N2kToNMEA0183Functions::HandleAISClassBMessage24A); // AIS Class B "CS" Static Data Report, Part A converters.registerConverter(129810UL, &N2kToNMEA0183Functions::HandleAISClassBMessage24B); // AIS Class B "CS" Static Data Report, Part B + converters.registerConverter(129041UL, &N2kToNMEA0183Functions::HandleAISMessage21); // AIS Aton #endif } diff --git a/lib/nmea2ktoais/NMEA0183AISMessages.cpp b/lib/nmea2ktoais/NMEA0183AISMessages.cpp deleted file mode 100644 index a0f9ec0..0000000 --- a/lib/nmea2ktoais/NMEA0183AISMessages.cpp +++ /dev/null @@ -1,574 +0,0 @@ -/* -NMEA0183AISMessages.cpp - -Copyright (c) 2019 Ronnie Zeiller - -Based on the works of Timo Lappalainen NMEA2000 and NMEA0183 Library -Thanks to Eric S. Raymond (https://gpsd.gitlab.io/gpsd/AIVDM.html) -and Kurt Schwehr for their informations on AIS encoding. - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the -Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE -OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -*/ - -#include -#include -#include -#include -//#include -//#include -#include -#include -#include - -const double pi=3.1415926535897932384626433832795; -const double kmhToms=1000.0/3600.0; -const double knToms=1852.0/3600.0; -const double degToRad=pi/180.0; -const double radToDeg=180.0/pi; -const double msTokmh=3600.0/1000.0; -const double msTokn=3600.0/1852.0; -const double nmTom=1.852*1000; -const double mToFathoms=0.546806649; -const double mToFeet=3.2808398950131; -const double radsToDegMin = 60 * 360.0 / (2 * pi); // [rad/s -> degree/minute] -const char Prefix='!'; - -std::vector vships; - -int numShips(){return vships.size();} -// ************************ Helper for AIS *********************************** -static bool AddMessageType(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageType); -static bool AddRepeat(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t Repeat); -static bool AddUserID(tNMEA0183AISMsg &NMEA0183AISMsg, uint32_t UserID); -static bool AddIMONumber(tNMEA0183AISMsg &NMEA0183AISMsg, uint32_t &IMONumber); -static bool AddText(tNMEA0183AISMsg &NMEA0183AISMsg, char *FieldVal, uint8_t length); -static bool AddDimensions(tNMEA0183AISMsg &NMEA0183AISMsg, double Length, double Beam, double PosRefStbd, double PosRefBow); -static bool AddNavStatus(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t &NavStatus); -static bool AddROT(tNMEA0183AISMsg &NMEA0183AISMsg, double &rot); -static bool AddSOG (tNMEA0183AISMsg &NMEA0183AISMsg, double &sog); -static bool AddLongitude(tNMEA0183AISMsg &NMEA0183AISMsg, double &Longitude); -static bool AddLatitude(tNMEA0183AISMsg &NMEA0183AISMsg, double &Latitude); -static bool AddHeading (tNMEA0183AISMsg &NMEA0183AISMsg, double &heading); -static bool AddCOG(tNMEA0183AISMsg &NMEA0183AISMsg, double cog); -static bool AddSeconds (tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t &Seconds); -static bool AddEPFDFixType(tNMEA0183AISMsg &NMEA0183AISMsg, tN2kGNSStype &GNSStype); -static bool AddStaticDraught(tNMEA0183AISMsg &NMEA0183AISMsg, double &Draught); -static bool AddETADateTime(tNMEA0183AISMsg &NMEA0183AISMsg, uint16_t &ETAdate, double &ETAtime); - -//***************************************************************************** -// Types 1, 2 and 3: Position Report Class A or B -> https://gpsd.gitlab.io/gpsd/AIVDM.html -// total of 168 bits, occupying one AIVDM sentence -// Example: !AIVDM,1,1,,A,133m@ogP00PD;88MD5MTDww@2D7k,0*46 -// Payload: Payload: 133m@ogP00PD;88MD5MTDww@2D7k -// Message type 1 has a payload length of 168 bits. -// because AIS encodes messages using a 6-bits ASCII mechanism and 168 divided by 6 is 28. -// -// Got values from: ParseN2kPGN129038() -bool SetAISClassABMessage1( tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageType, uint8_t Repeat, - uint32_t UserID, double Latitude, double Longitude, bool Accuracy, bool RAIM, uint8_t Seconds, - double COG, double SOG, double Heading, double ROT, uint8_t NavStatus ) { - - NMEA0183AISMsg.ClearAIS(); - if ( !AddMessageType(NMEA0183AISMsg, MessageType) ) return false; // 0 - 5 | 6 Message Type -> Constant: 1 - if ( !AddRepeat(NMEA0183AISMsg, Repeat) ) return false; // 6 - 7 | 2 Repeat Indicator: 0 = default; 3 = do not repeat any more - if ( !AddUserID(NMEA0183AISMsg, UserID) ) return false; // 8 - 37 | 30 MMSI - if ( !AddNavStatus(NMEA0183AISMsg, NavStatus) ) return false; // 38-41 | 4 Navigational Status e.g.: "Under way sailing" - if ( !AddROT(NMEA0183AISMsg, ROT) ) return false; // 42-49 | 8 Rate of Turn (ROT) - if ( !AddSOG(NMEA0183AISMsg, SOG) ) return false; // 50-59 | 10 [m/s -> kts] SOG with one digit x10, 1023 = N/A - if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Accuracy, 1) ) return false;// 60 | 1 GPS Accuracy 1 oder 0, Default 0 - if ( !AddLongitude(NMEA0183AISMsg, Longitude) ) return false; // 61-88 | 28 Longitude in Minutes / 10000 - if ( !AddLatitude(NMEA0183AISMsg, Latitude) ) return false; // 89-115 | 27 Latitude in Minutes / 10000 - if ( !AddCOG(NMEA0183AISMsg, COG) ) return false; // 116-127 | 12 Course over ground will be 3600 (0xE10) if that data is not available. - if ( !AddHeading (NMEA0183AISMsg, Heading) ) return false; // 128-136 | 9 True Heading (HDG) - if ( !AddSeconds(NMEA0183AISMsg, Seconds) ) return false; // 137-142 | 6 Seconds in UTC timestamp) - if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 2) ) return false; // 143-144 | 2 Maneuver Indicator: 0 (default) 1, 2 (not delivered within this PGN) - if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 3) ) return false; // 145-147 | 3 Spare - if ( !NMEA0183AISMsg.AddBoolToPayloadBin(RAIM, 1) ) return false; // 148-148 | 1 RAIM flag 0 = RAIM not in use (default), 1 = RAIM in use - if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 19) ) return false; // 149-167 | 19 Radio Status (-> 0 NOT SENT WITH THIS PGN!!!!!) - - if ( !NMEA0183AISMsg.Init("VDM","AI", Prefix) ) return false; - if ( !NMEA0183AISMsg.AddStrField("1") ) return false; - if ( !NMEA0183AISMsg.AddStrField("1") ) return false; - if ( !NMEA0183AISMsg.AddEmptyField() ) return false; - if ( !NMEA0183AISMsg.AddStrField("A") ) return false; - if ( !NMEA0183AISMsg.AddStrField( NMEA0183AISMsg.GetPayload() ) ) return false; - if ( !NMEA0183AISMsg.AddStrField("0") ) return false; // Message 1,2,3 has always Zero Padding - - return true; -} - -// ***************************************************************************** -// https://www.navcen.uscg.gov/?pageName=AISMessagesAStatic# -// AIS class A Static and Voyage Related Data -// Values derived from ParseN2kPGN129794(); -bool SetAISClassAMessage5(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, - uint32_t UserID, uint32_t IMONumber, char *Callsign, char *Name, - uint8_t VesselType, double Length, double Beam, double PosRefStbd, - double PosRefBow, uint16_t ETAdate, double ETAtime, double Draught, - char *Destination, tN2kGNSStype GNSStype, uint8_t DTE ) { - - // AIS Type 5 Message - NMEA0183AISMsg.ClearAIS(); - if ( !AddMessageType(NMEA0183AISMsg, 5) ) return false; // 0 - 5 | 6 Message Type -> Constant: 5 - if ( !AddRepeat(NMEA0183AISMsg, Repeat) ) return false; // 6 - 7 | 2 Repeat Indicator: 0 = default; 3 = do not repeat any more - if ( !AddUserID(NMEA0183AISMsg, UserID) ) return false; // 8 - 37 | 30 MMSI - if ( !NMEA0183AISMsg.AddIntToPayloadBin(1, 2) ) return false; // 38 - 39 | 2 AIS Version -> 0 oder 1 NOT DERIVED FROM N2k, Always 1!!!! - if ( !AddIMONumber(NMEA0183AISMsg, IMONumber) ) return false; // 40 - 69 | 30 IMO Number unisgned - if ( !AddText(NMEA0183AISMsg, Callsign, 42) ) return false; // 70 - 111 | 42 Call Sign WDE4178 -> 7 6-bit characters -> Ascii lt. Table) - if ( !AddText(NMEA0183AISMsg, Name, 120) ) return false; // 112-231 | 120 Vessel Name POINT FERMIN -> 20 6-bit characters -> Ascii lt. Table - if ( !NMEA0183AISMsg.AddIntToPayloadBin(VesselType, 8) ) return false; // 232-239 | 8 Ship Type 0....255 e.g. 31 Towing - if ( !AddDimensions(NMEA0183AISMsg, Length, Beam, PosRefStbd, PosRefBow) ) return false; // 240 - 269 | 30 Dimensions - if ( !AddEPFDFixType(NMEA0183AISMsg, GNSStype) ) return false; // 270-273 | 4 Position Fix Type, 0 (default) - if ( !AddETADateTime(NMEA0183AISMsg, ETAdate, ETAtime) ) return false; // 274 -293 | 20 Estimated time of arrival; MMDDHHMM UTC - if ( !AddStaticDraught(NMEA0183AISMsg, Draught) ) return false; // 294-301 | 8 Maximum Present Static Draught - if ( !AddText(NMEA0183AISMsg, Destination, 120) ) return false; // 302-421 | 120 | 20 Destination 20 6-bit characters - if ( !NMEA0183AISMsg.AddIntToPayloadBin(DTE, 1) ) return false; // 422 | 1 | Data terminal equipment (DTE) ready (0 = available, 1 = not available = default) - if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 1) ) return false; // 423 | 1 | spare - - return true; -} - -// **************************************************************************** -// AIS position report (class B 129039) -> Type 18: Standard Class B CS Position Report -// ParseN2kPGN129039(const tN2kMsg &N2kMsg, uint8_t &MessageID, tN2kAISRepeat &Repeat, uint32_t &UserID, -// double &Latitude, double &Longitude, bool &Accuracy, bool &RAIM, -// uint8_t &Seconds, double &COG, double &SOG, double &Heading, tN2kAISUnit &Unit, -// bool &Display, bool &DSC, bool &Band, bool &Msg22, tN2kAISMode &Mode, bool &State) -// VDM, VDO (AIS VHF Data-link message 18) -bool SetAISClassBMessage18(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, uint32_t UserID, - double Latitude, double Longitude, bool Accuracy, bool RAIM, - uint8_t Seconds, double COG, double SOG, double Heading, tN2kAISUnit Unit, - bool Display, bool DSC, bool Band, bool Msg22, bool Mode, bool State) { - // - NMEA0183AISMsg.ClearAIS(); - if ( !AddMessageType(NMEA0183AISMsg, MessageID) ) return false; // 0 - 5 | 6 Message Type -> Constant: 18 - if ( !AddRepeat(NMEA0183AISMsg, Repeat) ) return false; // 6 - 7 | 2 Repeat Indicator: 0 = default; 3 = do not repeat any more - if ( !AddUserID(NMEA0183AISMsg, UserID) ) return false; // 8 - 37 | 30 MMSI - if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 8) ) return false; // 38-45 | 8 Regional Reserved - if ( !AddSOG(NMEA0183AISMsg, SOG) ) return false; // 46-55 | 10 [m/s -> kts] SOG with one digit x10, 1023 = N/A - if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Accuracy, 1)) return false; // 56 | 1 GPS Accuracy 1 oder 0, Default 0 - if ( !AddLongitude(NMEA0183AISMsg, Longitude) ) return false; // 57-84 | 28 Longitude in Minutes / 10000 - if ( !AddLatitude(NMEA0183AISMsg, Latitude) ) return false; // 85-111 | 27 Latitude in Minutes / 10000 - if ( !AddCOG(NMEA0183AISMsg, COG) ) return false; // 112-123 | 12 Course over ground will be 3600 (0xE10) if that data is not available. - if ( !AddHeading (NMEA0183AISMsg, Heading) ) return false; // 124-132 | 9 True Heading (HDG) - if ( !AddSeconds(NMEA0183AISMsg, Seconds) ) return false; // 133-138 | 6 Seconds in UTC timestamp) - if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 2) ) return false; // 139-140 | 2 Regional Reserved - if ( !NMEA0183AISMsg.AddIntToPayloadBin(Unit, 1) ) return false; // 141 | 1 0=Class B SOTDMA unit 1=Class B CS (Carrier Sense) unit - if ( !NMEA0183AISMsg.AddIntToPayloadBin(Display, 1) ) return false; // 142 | 1 0=No visual display, 1=Has display, (Probably not reliable). - if ( !NMEA0183AISMsg.AddBoolToPayloadBin(DSC, 1) ) return false; // 143 | 1 If 1, unit is attached to a VHF voice radio with DSC capability. - if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Band, 1) ) return false; // 144 | 1 If this flag is 1, the unit can use any part of the marine channel. - if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Msg22, 1) ) return false; // 145 | 1 If 1, unit can accept a channel assignment via Message Type 22. - if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Mode, 1) ) return false; // 146 | 1 Assigned-mode flag: 0 = autonomous mode (default), 1 = assigned mode - if ( !NMEA0183AISMsg.AddBoolToPayloadBin(RAIM, 1) ) return false; // 147 | 1 as for Message Type 1,2,3 - if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 20) ) return false; // 148-167 | 20 Radio Status not in PGN 129039 - - if ( !NMEA0183AISMsg.Init("VDM","AI", Prefix) ) return false; - if ( !NMEA0183AISMsg.AddStrField("1") ) return false; - if ( !NMEA0183AISMsg.AddStrField("1") ) return false; - if ( !NMEA0183AISMsg.AddEmptyField() ) return false; - if ( !NMEA0183AISMsg.AddStrField("B") ) return false; - if ( !NMEA0183AISMsg.AddStrField( NMEA0183AISMsg.GetPayload() ) ) return false; - if ( !NMEA0183AISMsg.AddStrField("0") ) return false; // Message 18, has always Zero Padding - - return true; -} - -// **************************************************************************** -// Type 24: Static Data Report -// Equivalent of a Type 5 message for ships using Class B equipment. Also used to associate an MMSI -// with a name on either class A or class B equipment. -// -// A "Type 24" may be in part A or part B format; According to the standard, parts A and B are expected -// to be broadcast in adjacent pairs; in the real world they may (due to quirks in various aggregation methods) -// be separated by other sentences or even interleaved with different Type 24 pairs; decoders must cope with this. -// The interpretation of some fields in Type B format changes depending on the range of the Type B MMSI field. -// -// 160 bits for part A, 168 bits for part B. -// According to the standard, both the A and B parts are supposed to be 168 bits. -// However, in the wild, A parts are often transmitted with only 160 bits, omitting the spare 7 bits at the end. -// Implementers should be permissive about this. -// -// If the Part Number field is 0, the rest of the message is interpreted as a Part A; -// If it is 1, the rest of the message is interpreted as a Part B; values 2 and 3 are not allowed. -// -// PGN 129809 AIS Class B "CS" Static Data Report, Part A -> AIS VHF Data-link message 24 -// PGN 129810 AIS Class B "CS" Static Data Report, Part B -> AIS VHF Data-link message 24 -// ParseN2kPGN129809 (const tN2kMsg &N2kMsg, uint8_t &MessageID, tN2kAISRepeat &Repeat, uint32_t &UserID, char *Name) -> store to vector -// ParseN2kPGN129810(const tN2kMsg &N2kMsg, uint8_t &MessageID, tN2kAISRepeat &Repeat, uint32_t &UserID, -// uint8_t &VesselType, char *Vendor, char *Callsign, double &Length, double &Beam, -// double &PosRefStbd, double &PosRefBow, uint32_t &MothershipID); -// -// Part A: MessageID, Repeat, UserID, ShipName -> store in vector to call on Part B arrivals!!! -// Part B: MessageID, Repeat, UserID, VesselType (5), Callsign (5), Length & Beam, PosRefBow,.. (5) -bool SetAISClassBMessage24PartA(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, uint32_t UserID, char *Name) { - - bool found = false; - for (size_t i = 0; i < vships.size(); i++) { - if ( vships[i]->_userID == UserID ) { - found = true; - break; - } - } - if ( ! found ) { - std::string nm; - nm+= Name; - vships.push_back(new ship(UserID, nm)); - } - return true; -} - -// *************************************************************************************************************** -bool SetAISClassBMessage24(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, - uint32_t UserID, uint8_t VesselType, char *VendorID, char *Callsign, - double Length, double Beam, double PosRefStbd, double PosRefBow, uint32_t MothershipID ) { - - uint8_t PartNr = 0; // Identifier for the message part number; always 0 for Part A - char *ShipName = (char*)" "; // get from vector to look up for sent Messages Part A - - uint8_t i; - for ( i = 0; i < vships.size(); i++) { - if ( vships[i]->_userID == UserID ) { - ShipName = const_cast( vships[i]->_shipName.c_str() ); - } - } - if ( i > MAX_SHIP_IN_VECTOR ) { - std::vector::iterator it=vships.begin(); - delete *it; - vships.erase(it); - } - - // AIS Type 24 Message - NMEA0183AISMsg.ClearAIS(); - // Common for PART A AND Part B Bit 0 - 39 / len 40 - if ( !AddMessageType(NMEA0183AISMsg, 24) ) return false; // 0 - 5 | 6 Message Type -> Constant: 24 - if ( !AddRepeat(NMEA0183AISMsg, Repeat) ) return false; // 6 - 7 | 2 Repeat Indicator: 0 = default; 3 = do not repeat any more - if ( !AddUserID(NMEA0183AISMsg, UserID) ) return false; // 8 - 37 | 30 MMSI - if ( !NMEA0183AISMsg.AddIntToPayloadBin(PartNr, 2) ) return false; // 38-39 | 2 Part Number 0-1 -> - - // Part A: 40 + 128 = len 168 - if ( !AddText(NMEA0183AISMsg, ShipName, 120) ) return false; // 40-159 | 120 Vessel Name 20 6-bit characters -> Ascii Table - if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 8) ) return false; // 160-167 | 8 Spare - - // https://www.navcen.uscg.gov/?pageName=AISMessagesB - // PART B: 40 + 128 = len 168 - if ( !NMEA0183AISMsg.AddIntToPayloadBin(VesselType, 8) ) return false; // 168-175 | 40-47 | 8 Ship Type 0....99 - if ( !AddText(NMEA0183AISMsg, VendorID, 42) ) return false; // 176-217 | 48-89 | 42 Vendor ID + Unit Model Code + Serial Number - if ( !AddText(NMEA0183AISMsg, Callsign, 42) ) return false; // 218-259 | 90-131 | 42 Call Sign WDE4178 -> 7 6-bit characters, as in Msg Type 5 - if ( !AddDimensions(NMEA0183AISMsg, Length, Beam, PosRefStbd, PosRefBow) ) return false; // 260-289 | 132-161 | 30 Dimensions - if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 6) ) return false; // 290-295 | 162-167 | 6 Spare - - return true; -} - -//****************************************************************************** -// Validations and Unit Transformations -//****************************************************************************** - -// ***************************************************************************** -// 6bit Message Type -> Constant: 1 or 3, 5, 24 etc. -bool AddMessageType(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageType) { - - if (MessageType < 0 || MessageType > 24 ) MessageType = 1; - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(MessageType, 6) ) return false; - return true; -} - -// ***************************************************************************** -// 2bit Repeat Indicator: 0 = default; 3 = do not repeat any more -bool AddRepeat(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t Repeat) { - - if (Repeat < 0 || Repeat > 3) Repeat = 0; - if ( !NMEA0183AISMsg.AddIntToPayloadBin(Repeat, 2) ) return false; - return true; -} - -// ***************************************************************************** -// 30bit UserID = MMSI (9 decimal digits) -bool AddUserID(tNMEA0183AISMsg &NMEA0183AISMsg, uint32_t UserID) { - - if (UserID < 0||UserID > 999999999) UserID = 0; - if ( !NMEA0183AISMsg.AddIntToPayloadBin(UserID, 30) ) return false; - return true; -} - -// ***************************************************************************** -// 30 bit IMO Number -// 0 = not available = default – Not applicable to SAR aircraft -// 0000000001-0000999999 not used -// 0001000000-0009999999 = valid IMO number; -// 0010000000-1073741823 = official flag state number. -bool AddIMONumber(tNMEA0183AISMsg &NMEA0183AISMsg, uint32_t &IMONumber) { - uint32_t iTemp; - ( (IMONumber >= 999999 && IMONumber <= 9999999)||(IMONumber >= 10000000 && IMONumber <= 1073741823) )? iTemp = IMONumber : iTemp = 0; - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 30) ) return false; - return true; -} - -// ***************************************************************************** -// 42bit Callsign alphanumeric value, max 7 six-bit characters -// 120bit Name or Destination -bool AddText(tNMEA0183AISMsg &NMEA0183AISMsg, char *FieldVal, uint8_t length) { - uint8_t len = length/6; - - if ( strlen(FieldVal) > len ) FieldVal[len] = 0; - if ( !NMEA0183AISMsg.AddEncodedCharToPayloadBin(FieldVal, length) ) return false; - return true; -} - -// ***************************************************************************** -// Calculate Dimension A, B, C, D -// double PosRefBow 240-248 | 9 [m] Dimension to Bow, reference for pos. A -// Length - PosRefBow 249-257 | 9 [m] Dimension to Stern, reference for pos. B -// Beam - PosRefStbd 258-263 | 6 [m] Dimension to Port, reference for pos. C -// PosRefStbd 264-269 | 6 [m] Dimension to Starboard, reference for pos. D -// Ship dimensions will be 0 if not available. For the dimensions to bow and stern, -// the special value 511 indicates 511 meters or greater; -// for the dimensions to port and starboard, the special value 63 indicates 63 meters or greater. -// 30 Bit -bool AddDimensions(tNMEA0183AISMsg &NMEA0183AISMsg, double Length, double Beam, double PosRefStbd, double PosRefBow) { - uint16_t _PosRefBow = 0; - uint16_t _PosRefStern = 0; - uint16_t _PosRefStbd = 0; - uint16_t _PosRefPort = 0; - - if (PosRefBow < 0) PosRefBow=0; //could be N2kIsNA - if ( PosRefBow <= 511.0 ) { - _PosRefBow = round(PosRefBow); - } else { - _PosRefBow = 511; - } - if (PosRefStbd < 0 ) PosRefStbd=0; //could be N2kIsNA - if (PosRefStbd <= 63.0 ) { - _PosRefStbd = round(PosRefStbd); - } else { - _PosRefStbd = 63; - } - - if ( !N2kIsNA(Length) ) { - if (Length >= PosRefBow){ - _PosRefStern=round(Length - PosRefBow); - } - if ( _PosRefStern > 511 ) _PosRefStern = 511; - } - if ( !N2kIsNA(Beam) ) { - if (Beam >= PosRefStbd){ - _PosRefPort = round( Beam - PosRefStbd); - } - if ( _PosRefPort > 63 ) _PosRefPort = 63; - } - - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(_PosRefBow, 9) ) return false; - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(_PosRefStern, 9) ) return false; - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(_PosRefPort, 6) ) return false; - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(_PosRefStbd, 6) ) return false; - return true; -} - -// ***************************************************************************** -// 4 Bit Navigational Status e.g.: "Under way sailing" -// Same values used as in tN2kAISNavStatus, so we can use direct numbers -bool AddNavStatus(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t &NavStatus) { - uint8_t iTemp; - (NavStatus >= 0 && NavStatus <= 15 )? iTemp = NavStatus : iTemp = 15; - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 4) ) return false; - return true; -} - -// ***************************************************************************** -// 8bit [rad/s -> degree/minute] Rate of Turn ROT 128 = N/A -// 0 = not turning -// 1…126 = turning right at up to 708 degrees per minute or higher -// 1…-126 = turning left at up to 708 degrees per minute or higher -// 127 = turning right at more than 5deg/30s (No TI available) -// -127 = turning left at more than 5deg/30s (No TI available) -// 128 (80 hex) indicates no turn information available (default) -bool AddROT(tNMEA0183AISMsg &NMEA0183AISMsg, double &rot) { - int8_t iTemp; - if ( N2kIsNA(rot)) iTemp = 128; - else { - rot *= radsToDegMin; - (rot > -128.0 && rot < 128.0)? iTemp = aRoundToInt(rot) : iTemp = 128; - } - - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 8) ) return false; - return true; -} - -// ***************************************************************************** -// 10 bit [m/s -> kts] SOG x10, 1023 = N/A -// Speed over ground is in 0.1-knot resolution from 0 to 102 knots. -// Value 1023 indicates speed is not available, value 1022 indicates 102.2 knots or higher. -bool AddSOG (tNMEA0183AISMsg &NMEA0183AISMsg, double &sog) { - int16_t iTemp; - if ( sog < 0.0 ) iTemp = 1023; - else { - sog *= msTokn; - if (sog > 102.2) iTemp = 1023; - else iTemp = aRoundToInt( 10 * sog ); - } - - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 10) ) return false; - return true; -} - -// ***************************************************************************** -// 28 bit @TODO check negative values -// Values up to plus or minus 180 degrees, East = positive, West = negative. -// A value of 181 degrees (0x6791AC0 hex) indicates that longitude is not available and is the default. -// AIS Longitude is given in in 1/10000 min; divide by 600000.0 to obtain degrees. -bool AddLongitude(tNMEA0183AISMsg &NMEA0183AISMsg, double &Longitude) { - int32_t iTemp; - (Longitude >= -180.0 && Longitude <= 180.0)? iTemp = (int) (Longitude * 600000) : iTemp = 181 * 600000; - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 28) ) return false; - return true; -} - -// ***************************************************************************** -// 27 bit -// Values up to plus or minus 90 degrees, North = positive, South = negative. -// A value of 91 degrees (0x3412140 hex) indicates latitude is not available and is the default. -bool AddLatitude(tNMEA0183AISMsg &NMEA0183AISMsg, double &Latitude) { - int32_t iTemp; - (Latitude >= -90.0 && Latitude <= 90.0)? iTemp = (int) (Latitude * 600000) : iTemp = 91 * 600000; - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 27) ) return false; - return true; -} - -// **************************************************************************** -// 9 bit True Heading (HDG) 0 to 359 degrees, 511 = not available. -bool AddHeading (tNMEA0183AISMsg &NMEA0183AISMsg, double &heading) { - uint16_t iTemp; - if ( N2kIsNA(heading) ) iTemp = 511; - else { - heading *= radToDeg; - (heading >= 0.0 && heading <= 359.0 )? iTemp = aRoundToInt( heading ) : iTemp = 511; - } - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 9) ) return false; - return true; -} - -// ***************************************************************************** -// 12bit Relative to true north, to 0.1 degree precision -bool AddCOG(tNMEA0183AISMsg &NMEA0183AISMsg, double cog) { - int16_t iTemp; - cog *= radToDeg; - if ( cog >= 0.0 && cog < 360.0 ) { iTemp = aRoundToInt( cog * 10 ); } else { iTemp = 3600; } - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 12) ) return false; - return true; -} - -// ***************************************************************************** -// 6bit Seconds in UTC timestamp should be 0-59, except for these special values: -// 60 if time stamp is not available (default) -// 61 if positioning system is in manual input mode -// 62 if Electronic Position Fixing System operates in estimated (dead reckoning) mode, -// 63 if the positioning system is inoperative. -bool AddSeconds (tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t &Seconds) { - uint8_t iTemp; - (Seconds >= 0 && Seconds <= 63 )? iTemp = Seconds : iTemp = 60; - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 6) ) return false; - return true; -} - -// ***************************************************************************** -// 4 bit Position Fix Type, See "EPFD Fix Types" 0 (default) -bool AddEPFDFixType(tNMEA0183AISMsg &NMEA0183AISMsg, tN2kGNSStype &GNSStype) { - // Translate tN2kGNSStype to AIS conventions - // 3 & 4 not defined in AIS -> we take 1 for GPS - uint8_t fixType = 0; - switch (GNSStype) { - case 0: // GPS - case 3: // GPS+SBAS/WAAS - case 4: // GPS+SBAS/WAAS+GLONASS - fixType = 1; break; - case 1: // GLONASS - fixType = 2; break; - case 2: // GPS+GLONASS - fixType = 3; break; - case 5: // Chayka - fixType = 5; break; - case 6: // integrated - fixType = 6; break; - case 7: // surveyed - fixType = 7; break; - case 8: // Galileo - fixType = 8; break; - default: - fixType = 0; - } - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(fixType, 4) ) return false; - return true; -} - -// ***************************************************************************** -// 8 bit Maxiumum present static draught -// In 1/10 m, 255 = draught 25.5 m or greater, 0 = not available = default; in accordance with IMO Resolution A.851 -bool AddStaticDraught(tNMEA0183AISMsg &NMEA0183AISMsg, double &Draught) { - uint8_t staticDraught; - if ( N2kIsNA(Draught) ) staticDraught = 0; - else if (Draught < 0.0) staticDraught = 0; - else if (Draught>25.5) staticDraught = 255; - else staticDraught = (int) ceil( 10.0 * Draught); - - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(staticDraught, 8) ) return false; - return true; -} - -// ***************************************************************************** -// 20bit Estimated time of arrival; MMDDHHMM UTC -// 4 Bits 19-16: month; 1-12; 0 = not available = default -// 5 Bits 15-11: day; 1-31; 0 = not available = default -// 5 Bits 10-6: hour; 0-23; 24 = not available = default -// 6 Bits 5-0: minute; 0-59; 60 = not available = default -// N2k Field #7: ETA Time - Seconds since midnight Bits: 32 Units: s -// Type: Time Resolution: 0.0001 Signed: false e.g. 36000.00 -// N2k Field #8: ETA Date - Days since January 1, 1970 Bits: 16 -// Units: days Type: Date Resolution: 1 Signed: false e.g. 18184 -bool AddETADateTime(tNMEA0183AISMsg &NMEA0183AISMsg, uint16_t &ETAdate, double &ETAtime) { - - uint8_t month = 0; - uint8_t day = 0; - uint8_t hour = 24; - uint8_t minute = 60; - - if (!N2kIsNA(ETAdate) && ETAdate > 0 ) { - tmElements_t tm; - #ifndef _Time_h - time_t t=NMEA0183AISMsg.daysToTime_t(ETAdate); - #else - time_t t=ETAdate*86400; - #endif - NMEA0183AISMsg.breakTime(t, tm); - month = (uint8_t) NMEA0183AISMsg.GetMonth(tm); - day = (uint8_t) NMEA0183AISMsg.GetDay(tm); - } - if ( !N2kIsNA(ETAtime) && ETAtime >= 0 ) { - double temp = ETAtime / 3600; - hour = (int) temp; - minute = (int) ((temp - hour) * 60); - } else { - hour = 24; - minute = 60; - } - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(month, 4) ) return false; - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(day, 5) ) return false; - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(hour, 5) ) return false; - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(minute, 6) ) return false; - return true; -} diff --git a/lib/nmea2ktoais/NMEA0183AISMessages.h b/lib/nmea2ktoais/NMEA0183AISMessages.h deleted file mode 100644 index a124574..0000000 --- a/lib/nmea2ktoais/NMEA0183AISMessages.h +++ /dev/null @@ -1,86 +0,0 @@ -/* -NMEA0183AISMessages.h - -Copyright (c) 2019 Ronnie Zeiller, www.zeiller.eu - -Based on the works of Timo Lappalainen and Eric S. Raymond and Kurt Schwehr https://gpsd.gitlab.io/gpsd/AIVDM.html - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the -Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE -OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -*/ - -#ifndef _tNMEA0183AISMessages_H_ -#define _tNMEA0183AISMessages_H_ - -#include -#include -#include -#include -#include -#include -#include -#include - -#define MAX_SHIP_IN_VECTOR 200 -class ship { -public: - uint32_t _userID; - std::string _shipName; - - ship(uint32_t UserID, std::string ShipName) : _userID(UserID), _shipName(ShipName) {} -}; - - -// Types 1, 2 and 3: Position Report Class A or B -bool SetAISClassABMessage1(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageType, uint8_t Repeat, - uint32_t UserID, double Latitude, double Longitude, bool Accuracy, bool RAIM, uint8_t Seconds, - double COG, double SOG, double Heading, double ROT, uint8_t NavStatus); - -//***************************************************************************** -// AIS Class A Static and Voyage Related Data Message Type 5 -bool SetAISClassAMessage5(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, - uint32_t UserID, uint32_t IMONumber, char *Callsign, char *Name, - uint8_t VesselType, double Length, double Beam, double PosRefStbd, - double PosRefBow, uint16_t ETAdate, double ETAtime, double Draught, - char *Destination, tN2kGNSStype GNSStype, uint8_t DTE ); - -//***************************************************************************** -// AIS position report (class B 129039) -> Standard Class B CS Position Report Message Type 18 Part B -bool SetAISClassBMessage18(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, uint32_t UserID, - double Latitude, double Longitude, bool Accuracy, bool RAIM, - uint8_t Seconds, double COG, double SOG, double Heading, tN2kAISUnit Unit, - bool Display, bool DSC, bool Band, bool Msg22, bool Mode, bool State); - -//***************************************************************************** -// Static Data Report Class B, Message Type 24 -// PGN 129809 Handle AIS Class B "CS" Static Data Report, Part A -bool SetAISClassBMessage24PartA(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, uint32_t UserID, char *Name); - -//***************************************************************************** -// Static Data Report Class B, Message Type 24 -bool SetAISClassBMessage24(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, - uint32_t UserID, uint8_t VesselType, char *VendorID, char *Callsign, - double Length, double Beam, double PosRefStbd, double PosRefBow, uint32_t MothershipID ); - -int numShips(); -inline int32_t aRoundToInt(double x) { - return x >= 0 - ? (int32_t) floor(x + 0.5) - : (int32_t) ceil(x - 0.5); -} -#endif diff --git a/lib/nmea2ktoais/NMEA0183AISMsg.cpp b/lib/nmea2ktoais/NMEA0183AISMsg.cpp deleted file mode 100644 index 6abcf42..0000000 --- a/lib/nmea2ktoais/NMEA0183AISMsg.cpp +++ /dev/null @@ -1,297 +0,0 @@ -/* -NMEA0183AISMsg.cpp - -Copyright (c) 2019 Ronnie Zeiller, www.zeiller.eu -Based on the works of Timo Lappalainen NMEA2000 and NMEA0183 Library - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the -Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE -OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -*/ - -#include "NMEA0183AISMsg.h" -#include -#include -#include -#include -#include -#include -#include - -const char AsciiChar[] = "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_ !\"#$%&\'()*+,-./0123456789:;<=>?"; -const char *tNMEA0183AISMsg::EmptyAISField = "000000"; - -//***************************************************************************** -tNMEA0183AISMsg::tNMEA0183AISMsg() { - ClearAIS(); -} - -//***************************************************************************** -void tNMEA0183AISMsg::ClearAIS() { - - PayloadBin[0]=0; - Payload[0]=0; - iAddPldBin=0; - iAddPld=0; -} - -//***************************************************************************** -// Add 6bit with no data. -bool tNMEA0183AISMsg::AddEmptyFieldToPayloadBin(uint8_t iBits) { - - if ( (iAddPldBin + iBits * 6) >= AIS_BIN_MAX_LEN ) return false; // Is there room for any data - - for (uint8_t i=0;i= AIS_BIN_MAX_LEN ) return false; // Is there room for any data - - AISBitSet bset(ival); - - PayloadBin[iAddPldBin]=0; - uint16_t iAdd=iAddPldBin; - - for(int i = countBits-1; i >= 0 ; i--) { - PayloadBin[iAdd] = bset[i]?'1':'0'; - iAdd++; - } - - iAddPldBin += countBits; - PayloadBin[iAddPldBin]=0; - - return true; -} - -// **************************************************************************** -bool tNMEA0183AISMsg::AddBoolToPayloadBin(bool &bval, uint8_t size) { - int8_t iTemp; - (bval == true)? iTemp = 1 : iTemp = 0; - if ( ! AddIntToPayloadBin(iTemp, size) ) return false; - return true; -} - -// ***************************************************************************** -// converts sval into binary 6-bit AScii encoded string and appends it to PayloadBin -// filled up with "@" == "000000" to given bit-size -bool tNMEA0183AISMsg::AddEncodedCharToPayloadBin(char *sval, size_t countBits) { - - if ( (iAddPldBin + countBits ) >= AIS_BIN_MAX_LEN ) return false; // Is there room for any data - - PayloadBin[iAddPldBin]=0; - std::bitset<6> bs; - char * ptr; - size_t len = strlen(sval); // e.g.: should be 7 for Callsign - if ( len * 6 > countBits ) len = countBits / 6; - - for (int i = 0; i= 0){ - AddIntToPayloadBin(index, 6); - } - } else { - AddIntToPayloadBin(0, 6); - } - } - - PayloadBin[iAddPldBin+1]=0; - - // fill up with "@", also covers empty sval - if ( len * 6 < countBits ) { - for (int i=0;i<(countBits/6-len);i++) { - AddIntToPayloadBin(0, 6); - } - } - PayloadBin[iAddPldBin]=0; - return true; -} - -// ***************************************************************************** -bool tNMEA0183AISMsg::ConvertBinaryAISPayloadBinToAscii(const char *payloadbin) { - uint16_t len; - - len = strlen( payloadbin ) / 6; // 28 - uint32_t offset; - char s[7]; - uint8_t dec; - int i; - for ( i=0; i -#include -#include -#include -#include -#include -#include - - -#ifndef AIS_MSG_MAX_LEN -#define AIS_MSG_MAX_LEN 100 // maximum length of AIS Payload -#endif - -#ifndef AIS_BIN_MAX_LEN -#define AIS_BIN_MAX_LEN 500 // maximum length of AIS Binary Payload (before encoding to Ascii) -#endif - -#define BITSET_LENGTH 120 - -typedef std::bitset AISBitSet; -class tNMEA0183AISMsg : public tNMEA0183Msg { - - protected: // AIS-NMEA - static const char *EmptyAISField; // 6bits 0 not used yet..... - static const char *AsciChar; - - uint16_t iAddPldBin; - char Payload[AIS_MSG_MAX_LEN]; - uint8_t iAddPld; - - public: - char PayloadBin[AIS_BIN_MAX_LEN]; - char PayloadBin2[AIS_BIN_MAX_LEN]; - // Clear message - void ClearAIS(); - - public: - tNMEA0183AISMsg(); - const char *GetPayload(); - const char *GetPayloadType5_Part1(); - const char *GetPayloadType5_Part2(); - const char *GetPayloadType24_PartA(); - const char *GetPayloadType24_PartB(); - const char *GetPayloadBin() const { return PayloadBin; } - - const tNMEA0183AISMsg& BuildMsg5Part1(tNMEA0183AISMsg &AISMsg); - const tNMEA0183AISMsg& BuildMsg5Part2(tNMEA0183AISMsg &AISMsg); - const tNMEA0183AISMsg& BuildMsg24PartA(tNMEA0183AISMsg &AISMsg); - const tNMEA0183AISMsg& BuildMsg24PartB(tNMEA0183AISMsg &AISMsg); - - // Generally Used - bool AddIntToPayloadBin(int32_t ival, uint16_t countBits); - bool AddBoolToPayloadBin(bool &bval, uint8_t size); - bool AddEncodedCharToPayloadBin(char *sval, size_t Length); - bool AddEmptyFieldToPayloadBin(uint8_t iBits); - bool ConvertBinaryAISPayloadBinToAscii(const char *payloadbin); - - // AIS Helper functions - protected: - inline int32_t aRoundToInt(double x) { - return (x >= 0) ? (int32_t) floor(x + 0.5) : (int32_t) ceil(x - 0.5); - } -}; -#endif diff --git a/lib/nmea2ktoais/README.md b/lib/nmea2ktoais/README.md deleted file mode 100644 index 9d83553..0000000 --- a/lib/nmea2ktoais/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# NMEA2000 -> NMEA0183 AIS converter v1.0.0 - -Import from https://github.com/ronzeiller/NMEA0183-AIS - -NMEA0183 AIS library © Ronnie Zeiller, www.zeiller.eu - -Addendum for NMEA2000 and NMEA0183 Library from Timo Lappalainen https://github.com/ttlappalainen - - -## Conversions: - -- NMEA2000 PGN 129038 => AIS CLASS A Position Report (Message Type 1) 1.) 2.) 3.) -- NMEA2000 PGN 129039 => AIS Class B Position Report, Message Type 18 -- NMEA2000 PGN 129794 => AIS Class A Ship Static and Voyage related data, Message Type 5 4.) -- NMEA2000 PGN 129809 => AIS Class B "CS" Static Data Report, making a list of UserID (MMSI) and Ship Names used for Message 24 Part A -- NMEA2000 PGN 129810 => AIS Class B "CS" Static Data Report, Message 24 Part A+B - -### Remarks -1. Message Type could be set to 1 or 3 (identical messages) on demand -2. Maneuver Indicator (not part of NMEA2000 PGN 129038) => will be set to 0 (default) -3. Radio Status (not part of NMEA2000 PGN 129038) => will be set to 0 -4. AIS Version (not part of NMEA2000 PGN 129794) => will be set to 1 - -## Dependencies - -To use this library you need also: - - - NMEA2000 library - - - NMEA0183 library - - - Related CAN libraries. - -## License - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the -Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE -OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/platformio.ini b/platformio.ini index 4125eb7..4e0773a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -28,7 +28,21 @@ lib_deps = ESPmDNS WiFi Update + nmea2kto183ais=https://github.com/ronzeiller/NMEA0183-AIS#7d2bfab54e3e5bfaab36fe6aa356241baa7251c2 +[devdeps] +lib_deps= + ttlappalainen/NMEA2000-library @ 4.22.0 + ttlappalainen/NMEA0183 @ 1.10.1 + ArduinoJson @ 6.18.5 + AsyncTCP-esphome @ 2.0.1 + ottowinter/ESPAsyncWebServer-esphome@2.0.1 + FS + Preferences + ESPmDNS + WiFi + Update + nmea2kto183ais=symlink://../esp32n2kto183ais [env] platform = espressif32 @ 6.8.1 framework = arduino @@ -67,6 +81,17 @@ lib_deps = adafruit/Adafruit BusIO @ 1.14.5 adafruit/Adafruit Unified Sensor @ 1.1.13 +[env:m5stack-atom-dev] +board = m5stack-atom +lib_deps = + ${devdeps.lib_deps} + fastled/FastLED @ 3.6.0 + +build_flags = + -D BOARD_M5ATOM + ${env.build_flags} +upload_port = /dev/esp32 +upload_protocol = esptool [env:m5stack-atom] board = m5stack-atom lib_deps = ${env.lib_deps} From ec807c6925e52a6e5c6cb2c0112a333b8e6cc229 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Tue, 23 Sep 2025 13:05:28 +0200 Subject: [PATCH 016/121] intermediate: adapt handling to new n2ktoais lib --- lib/nmea2kto0183/N2kDataToNMEA0183.cpp | 200 ++----------------------- 1 file changed, 11 insertions(+), 189 deletions(-) diff --git a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp index 945ad4c..ade2c3f 100644 --- a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp +++ b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp @@ -735,39 +735,7 @@ private: _COG, _SOG, _Heading, _ROT, _NavStatus,_AISTransceiverInformation,_SID)) { -// Debug -#ifdef SERIAL_PRINT_AIS_FIELDS - Serial.println("–––––––––––––––––––––––– Msg 1 ––––––––––––––––––––––––––––––––"); - const double pi = 3.1415926535897932384626433832795; - const double radToDeg = 180.0 / pi; - const double msTokn = 3600.0 / 1852.0; - const double radsToDegMin = 60 * 360.0 / (2 * pi); // [rad/s -> degree/minute] - Serial.print("Repeat: "); - Serial.println(_Repeat); - Serial.print("UserID: "); - Serial.println(_UserID); - Serial.print("Latitude: "); - Serial.println(_Latitude); - Serial.print("Longitude: "); - Serial.println(_Longitude); - Serial.print("Accuracy: "); - Serial.println(_Accuracy); - Serial.print("RAIM: "); - Serial.println(_RAIM); - Serial.print("Seconds: "); - Serial.println(_Seconds); - Serial.print("COG: "); - Serial.println(_COG * radToDeg); - Serial.print("SOG: "); - Serial.println(_SOG * msTokn); - Serial.print("Heading: "); - Serial.println(_Heading * radToDeg); - Serial.print("ROT: "); - Serial.println(_ROT * radsToDegMin); - Serial.print("NavStatus: "); - Serial.println(_NavStatus); -#endif if (_MessageType < 1 || _MessageType > 3) _MessageType=1; //only allow type 1...3 for 129038 if (SetAISClassABMessage1(NMEA0183AISMsg, _MessageType, _Repeat, _UserID, _Latitude, _Longitude, _Accuracy, @@ -776,20 +744,6 @@ private: SendMessage(NMEA0183AISMsg); -#ifdef SERIAL_PRINT_AIS_NMEA - // Debug Print AIS-NMEA - Serial.print(NMEA0183AISMsg.GetPrefix()); - Serial.print(NMEA0183AISMsg.Sender()); - Serial.print(NMEA0183AISMsg.MessageCode()); - for (int i = 0; i < NMEA0183AISMsg.FieldCount(); i++) - { - Serial.print(","); - Serial.print(NMEA0183AISMsg.Field(i)); - } - char buf[7]; - sprintf(buf, "*%02X\r\n", NMEA0183AISMsg.GetCheckSum()); - Serial.print(buf); -#endif } } } // end 129038 AIS Class A Position Report Message 1/3 @@ -826,83 +780,18 @@ private: _AISversion, _GNSStype, _DTE, _AISinfo,_SID)) { -#ifdef SERIAL_PRINT_AIS_FIELDS - // Debug Print N2k Values - Serial.println("––––––––––––––––––––––– Msg 5 –––––––––––––––––––––––––––––––––"); - Serial.print("MessageID: "); - Serial.println(_MessageID); - Serial.print("Repeat: "); - Serial.println(_Repeat); - Serial.print("UserID: "); - Serial.println(_UserID); - Serial.print("IMONumber: "); - Serial.println(_IMONumber); - Serial.print("Callsign: "); - Serial.println(_Callsign); - Serial.print("VesselType: "); - Serial.println(_VesselType); - Serial.print("Name: "); - Serial.println(_Name); - Serial.print("Length: "); - Serial.println(_Length); - Serial.print("Beam: "); - Serial.println(_Beam); - Serial.print("PosRefStbd: "); - Serial.println(_PosRefStbd); - Serial.print("PosRefBow: "); - Serial.println(_PosRefBow); - Serial.print("ETAdate: "); - Serial.println(_ETAdate); - Serial.print("ETAtime: "); - Serial.println(_ETAtime); - Serial.print("Draught: "); - Serial.println(_Draught); - Serial.print("Destination: "); - Serial.println(_Destination); - Serial.print("GNSStype: "); - Serial.println(_GNSStype); - Serial.print("DTE: "); - Serial.println(_DTE); - Serial.println("––––––––––––––––––––––– Msg 5 –––––––––––––––––––––––––––––––––"); -#endif if (SetAISClassAMessage5(NMEA0183AISMsg, _MessageID, _Repeat, _UserID, _IMONumber, _Callsign, _Name, _VesselType, _Length, _Beam, _PosRefStbd, _PosRefBow, _ETAdate, _ETAtime, _Draught, _Destination, _GNSStype, _DTE)) { - - SendMessage(NMEA0183AISMsg.BuildMsg5Part1(NMEA0183AISMsg)); - -#ifdef SERIAL_PRINT_AIS_NMEA - // Debug Print AIS-NMEA Message Type 5, Part 1 - char buf[7]; - Serial.print(NMEA0183AISMsg.GetPrefix()); - Serial.print(NMEA0183AISMsg.Sender()); - Serial.print(NMEA0183AISMsg.MessageCode()); - for (int i = 0; i < NMEA0183AISMsg.FieldCount(); i++) - { - Serial.print(","); - Serial.print(NMEA0183AISMsg.Field(i)); + if (NMEA0183AISMsg.BuildMsg5Part1()){ + SendMessage(NMEA0183AISMsg); } - sprintf(buf, "*%02X\r\n", NMEA0183AISMsg.GetCheckSum()); - Serial.print(buf); -#endif - - SendMessage(NMEA0183AISMsg.BuildMsg5Part2(NMEA0183AISMsg)); - -#ifdef SERIAL_PRINT_AIS_NMEA - // Print AIS-NMEA Message Type 5, Part 2 - Serial.print(NMEA0183AISMsg.GetPrefix()); - Serial.print(NMEA0183AISMsg.Sender()); - Serial.print(NMEA0183AISMsg.MessageCode()); - for (int i = 0; i < NMEA0183AISMsg.FieldCount(); i++) - { - Serial.print(","); - Serial.print(NMEA0183AISMsg.Field(i)); + if (NMEA0183AISMsg.BuildMsg5Part2()){ + SendMessage(NMEA0183AISMsg); } - sprintf(buf, "*%02X\r\n", NMEA0183AISMsg.GetCheckSum()); - Serial.print(buf); -#endif + } } } @@ -941,20 +830,6 @@ private: SendMessage(NMEA0183AISMsg); -#ifdef SERIAL_PRINT_AIS_NMEA - // Debug Print AIS-NMEA - Serial.print(NMEA0183AISMsg.GetPrefix()); - Serial.print(NMEA0183AISMsg.Sender()); - Serial.print(NMEA0183AISMsg.MessageCode()); - for (int i = 0; i < NMEA0183AISMsg.FieldCount(); i++) - { - Serial.print(","); - Serial.print(NMEA0183AISMsg.Field(i)); - } - char buf[7]; - sprintf(buf, "*%02X\r\n", NMEA0183AISMsg.GetCheckSum()); - Serial.print(buf); -#endif } } return; @@ -1005,72 +880,19 @@ private: _Length, _Beam, _PosRefStbd, _PosRefBow, _MothershipID,_AISInfo,_SID)) { -// -#ifdef SERIAL_PRINT_AIS_FIELDS - // Debug Print N2k Values - Serial.println("––––––––––––––––––––––– Msg 24 ––––––––––––––––––––––––––––––––"); - Serial.print("MessageID: "); - Serial.println(_MessageID); - Serial.print("Repeat: "); - Serial.println(_Repeat); - Serial.print("UserID: "); - Serial.println(_UserID); - Serial.print("VesselType: "); - Serial.println(_VesselType); - Serial.print("Vendor: "); - Serial.println(_Vendor); - Serial.print("Callsign: "); - Serial.println(_Callsign); - Serial.print("Length: "); - Serial.println(_Length); - Serial.print("Beam: "); - Serial.println(_Beam); - Serial.print("PosRefStbd: "); - Serial.println(_PosRefStbd); - Serial.print("PosRefBow: "); - Serial.println(_PosRefBow); - Serial.print("MothershipID: "); - Serial.println(_MothershipID); - Serial.println("––––––––––––––––––––––– Msg 24 ––––––––––––––––––––––––––––––––"); -#endif - tNMEA0183AISMsg NMEA0183AISMsg; if (SetAISClassBMessage24(NMEA0183AISMsg, _MessageID, _Repeat, _UserID, _VesselType, _Vendor, _Callsign, _Length, _Beam, _PosRefStbd, _PosRefBow, _MothershipID)) { - - SendMessage(NMEA0183AISMsg.BuildMsg24PartA(NMEA0183AISMsg)); - -#ifdef SERIAL_PRINT_AIS_NMEA - // Debug Print AIS-NMEA - char buf[7]; - Serial.print(NMEA0183AISMsg.GetPrefix()); - Serial.print(NMEA0183AISMsg.Sender()); - Serial.print(NMEA0183AISMsg.MessageCode()); - for (int i = 0; i < NMEA0183AISMsg.FieldCount(); i++) - { - Serial.print(","); - Serial.print(NMEA0183AISMsg.Field(i)); + if (NMEA0183AISMsg.BuildMsg24PartA()){ + SendMessage(NMEA0183AISMsg); } - sprintf(buf, "*%02X\r\n", NMEA0183AISMsg.GetCheckSum()); - Serial.print(buf); -#endif - SendMessage(NMEA0183AISMsg.BuildMsg24PartB(NMEA0183AISMsg)); - -#ifdef SERIAL_PRINT_AIS_NMEA - Serial.print(NMEA0183AISMsg.GetPrefix()); - Serial.print(NMEA0183AISMsg.Sender()); - Serial.print(NMEA0183AISMsg.MessageCode()); - for (int i = 0; i < NMEA0183AISMsg.FieldCount(); i++) - { - Serial.print(","); - Serial.print(NMEA0183AISMsg.Field(i)); + if (NMEA0183AISMsg.BuildMsg24PartB()){ + SendMessage(NMEA0183AISMsg); } - sprintf(buf, "*%02X\r\n", NMEA0183AISMsg.GetCheckSum()); - Serial.print(buf); -#endif + } } return; @@ -1105,7 +927,7 @@ private: data.AISTransceiverInformation, data.AtoNName )){ - //TODO: SendMessage(nmea0183Msg); + SendMessage(nmea0183Msg); } } } From 13eac9508d554a043d4099678352c55888ba82b7 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Tue, 23 Sep 2025 18:29:43 +0200 Subject: [PATCH 017/121] add some helper tools for converting candumps --- tools/getPgnType.py | 29 +++ tools/sendN2K.py | 527 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 556 insertions(+) create mode 100755 tools/getPgnType.py create mode 100644 tools/sendN2K.py diff --git a/tools/getPgnType.py b/tools/getPgnType.py new file mode 100755 index 0000000..edbdbb2 --- /dev/null +++ b/tools/getPgnType.py @@ -0,0 +1,29 @@ +#! /usr/bin/env python3 +import sys +import json + +def err(txt): + print(txt,file=sys.stderr) + sys.exit(1) + +HDR=''' +PGNM_Fast=0 +PGNM_Single=1 +PGNM_ISO=2 +PGN_MODES={ +''' +FOOTER=''' + } +''' +with open(sys.argv[1],"r") as ih: + data=json.load(ih) + pgns=data.get('PGNs') + if pgns is None: + err("no pgns") + print(HDR) + for p in pgns: + t=p['Type'] + pgn=p['PGN'] + if t and pgn: + print(f" {pgn}: PGNM_{t},") + print(FOOTER) \ No newline at end of file diff --git a/tools/sendN2K.py b/tools/sendN2K.py new file mode 100644 index 0000000..c05bbda --- /dev/null +++ b/tools/sendN2K.py @@ -0,0 +1,527 @@ +#! /usr/bin/env python3 +import re +import sys +import os +import datetime + +###generated with getPgnType.py from canboat pgns.json +PGNM_Fast=0 +PGNM_Single=1 +PGNM_ISO=2 +PGN_MODES={ + + 59392: PGNM_Single, + 59904: PGNM_Single, + 60160: PGNM_Single, + 60416: PGNM_Single, + 60416: PGNM_Single, + 60416: PGNM_Single, + 60416: PGNM_Single, + 60416: PGNM_Single, + 60928: PGNM_Single, + 61184: PGNM_Single, + 61184: PGNM_Single, + 61184: PGNM_Single, + 65001: PGNM_Single, + 65002: PGNM_Single, + 65003: PGNM_Single, + 65004: PGNM_Single, + 65005: PGNM_Single, + 65006: PGNM_Single, + 65007: PGNM_Single, + 65008: PGNM_Single, + 65009: PGNM_Single, + 65010: PGNM_Single, + 65011: PGNM_Single, + 65012: PGNM_Single, + 65013: PGNM_Single, + 65014: PGNM_Single, + 65015: PGNM_Single, + 65016: PGNM_Single, + 65017: PGNM_Single, + 65018: PGNM_Single, + 65019: PGNM_Single, + 65020: PGNM_Single, + 65021: PGNM_Single, + 65022: PGNM_Single, + 65023: PGNM_Single, + 65024: PGNM_Single, + 65025: PGNM_Single, + 65026: PGNM_Single, + 65027: PGNM_Single, + 65028: PGNM_Single, + 65029: PGNM_Single, + 65030: PGNM_Single, + 65240: PGNM_ISO, + 65280: PGNM_Single, + 65284: PGNM_Single, + 65285: PGNM_Single, + 65285: PGNM_Single, + 65286: PGNM_Single, + 65286: PGNM_Single, + 65287: PGNM_Single, + 65287: PGNM_Single, + 65288: PGNM_Single, + 65289: PGNM_Single, + 65290: PGNM_Single, + 65292: PGNM_Single, + 65293: PGNM_Single, + 65293: PGNM_Single, + 65302: PGNM_Single, + 65305: PGNM_Single, + 65305: PGNM_Single, + 65305: PGNM_Single, + 65305: PGNM_Single, + 65305: PGNM_Single, + 65309: PGNM_Single, + 65312: PGNM_Single, + 65340: PGNM_Single, + 65341: PGNM_Single, + 65345: PGNM_Single, + 65350: PGNM_Single, + 65359: PGNM_Single, + 65360: PGNM_Single, + 65361: PGNM_Single, + 65371: PGNM_Single, + 65374: PGNM_Single, + 65379: PGNM_Single, + 65408: PGNM_Single, + 65409: PGNM_Single, + 65410: PGNM_Single, + 65420: PGNM_Single, + 65480: PGNM_Single, + 126208: PGNM_Fast, + 126208: PGNM_Fast, + 126208: PGNM_Fast, + 126208: PGNM_Fast, + 126208: PGNM_Fast, + 126208: PGNM_Fast, + 126208: PGNM_Fast, + 126464: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126983: PGNM_Fast, + 126984: PGNM_Fast, + 126985: PGNM_Fast, + 126986: PGNM_Fast, + 126987: PGNM_Fast, + 126988: PGNM_Fast, + 126992: PGNM_Single, + 126993: PGNM_Single, + 126996: PGNM_Fast, + 126998: PGNM_Fast, + 127233: PGNM_Fast, + 127237: PGNM_Fast, + 127245: PGNM_Single, + 127250: PGNM_Single, + 127251: PGNM_Single, + 127252: PGNM_Single, + 127257: PGNM_Single, + 127258: PGNM_Single, + 127488: PGNM_Single, + 127489: PGNM_Fast, + 127490: PGNM_Fast, + 127491: PGNM_Fast, + 127493: PGNM_Single, + 127494: PGNM_Fast, + 127495: PGNM_Fast, + 127496: PGNM_Fast, + 127497: PGNM_Fast, + 127498: PGNM_Fast, + 127500: PGNM_Single, + 127501: PGNM_Single, + 127502: PGNM_Single, + 127503: PGNM_Fast, + 127504: PGNM_Fast, + 127505: PGNM_Single, + 127506: PGNM_Fast, + 127507: PGNM_Fast, + 127508: PGNM_Single, + 127509: PGNM_Fast, + 127510: PGNM_Fast, + 127511: PGNM_Single, + 127512: PGNM_Single, + 127513: PGNM_Fast, + 127514: PGNM_Single, + 127744: PGNM_Single, + 127745: PGNM_Single, + 127746: PGNM_Single, + 127750: PGNM_Single, + 127751: PGNM_Single, + 128000: PGNM_Single, + 128001: PGNM_Single, + 128002: PGNM_Single, + 128003: PGNM_Single, + 128006: PGNM_Single, + 128007: PGNM_Single, + 128008: PGNM_Single, + 128259: PGNM_Single, + 128267: PGNM_Single, + 128275: PGNM_Fast, + 128520: PGNM_Fast, + 128538: PGNM_Fast, + 128768: PGNM_Single, + 128769: PGNM_Single, + 128776: PGNM_Single, + 128777: PGNM_Single, + 128778: PGNM_Single, + 128780: PGNM_Single, + 129025: PGNM_Single, + 129026: PGNM_Single, + 129027: PGNM_Single, + 129028: PGNM_Single, + 129029: PGNM_Fast, + 129033: PGNM_Single, + 129038: PGNM_Fast, + 129039: PGNM_Fast, + 129040: PGNM_Fast, + 129041: PGNM_Fast, + 129044: PGNM_Fast, + 129045: PGNM_Fast, + 129283: PGNM_Single, + 129284: PGNM_Fast, + 129285: PGNM_Fast, + 129291: PGNM_Single, + 129301: PGNM_Fast, + 129302: PGNM_Fast, + 129538: PGNM_Fast, + 129539: PGNM_Single, + 129540: PGNM_Fast, + 129541: PGNM_Fast, + 129542: PGNM_Fast, + 129545: PGNM_Fast, + 129546: PGNM_Single, + 129547: PGNM_Fast, + 129549: PGNM_Fast, + 129550: PGNM_Fast, + 129551: PGNM_Fast, + 129556: PGNM_Fast, + 129792: PGNM_Fast, + 129793: PGNM_Fast, + 129794: PGNM_Fast, + 129795: PGNM_Fast, + 129796: PGNM_Fast, + 129797: PGNM_Fast, + 129798: PGNM_Fast, + 129799: PGNM_Fast, + 129800: PGNM_Fast, + 129801: PGNM_Fast, + 129802: PGNM_Fast, + 129803: PGNM_Fast, + 129804: PGNM_Fast, + 129805: PGNM_Fast, + 129806: PGNM_Fast, + 129807: PGNM_Fast, + 129808: PGNM_Fast, + 129808: PGNM_Fast, + 129809: PGNM_Fast, + 129810: PGNM_Fast, + 130052: PGNM_Fast, + 130053: PGNM_Fast, + 130054: PGNM_Fast, + 130060: PGNM_Fast, + 130061: PGNM_Fast, + 130064: PGNM_Fast, + 130065: PGNM_Fast, + 130066: PGNM_Fast, + 130067: PGNM_Fast, + 130068: PGNM_Fast, + 130069: PGNM_Fast, + 130070: PGNM_Fast, + 130071: PGNM_Fast, + 130072: PGNM_Fast, + 130073: PGNM_Fast, + 130074: PGNM_Fast, + 130306: PGNM_Single, + 130310: PGNM_Single, + 130311: PGNM_Single, + 130312: PGNM_Single, + 130313: PGNM_Single, + 130314: PGNM_Single, + 130315: PGNM_Single, + 130316: PGNM_Single, + 130320: PGNM_Fast, + 130321: PGNM_Fast, + 130322: PGNM_Fast, + 130323: PGNM_Fast, + 130324: PGNM_Fast, + 130330: PGNM_Fast, + 130560: PGNM_Single, + 130561: PGNM_Fast, + 130562: PGNM_Fast, + 130563: PGNM_Fast, + 130564: PGNM_Fast, + 130565: PGNM_Fast, + 130566: PGNM_Fast, + 130567: PGNM_Fast, + 130569: PGNM_Fast, + 130570: PGNM_Fast, + 130571: PGNM_Fast, + 130572: PGNM_Fast, + 130573: PGNM_Fast, + 130574: PGNM_Fast, + 130576: PGNM_Single, + 130577: PGNM_Fast, + 130578: PGNM_Fast, + 130579: PGNM_Single, + 130580: PGNM_Fast, + 130581: PGNM_Fast, + 130582: PGNM_Single, + 130583: PGNM_Fast, + 130584: PGNM_Fast, + 130585: PGNM_Single, + 130586: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130817: PGNM_Fast, + 130817: PGNM_Fast, + 130818: PGNM_Fast, + 130819: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130821: PGNM_Fast, + 130821: PGNM_Fast, + 130822: PGNM_Fast, + 130823: PGNM_Fast, + 130824: PGNM_Fast, + 130824: PGNM_Fast, + 130825: PGNM_Fast, + 130827: PGNM_Fast, + 130828: PGNM_Fast, + 130831: PGNM_Fast, + 130832: PGNM_Fast, + 130833: PGNM_Fast, + 130834: PGNM_Fast, + 130835: PGNM_Fast, + 130836: PGNM_Fast, + 130836: PGNM_Fast, + 130837: PGNM_Fast, + 130837: PGNM_Fast, + 130838: PGNM_Fast, + 130839: PGNM_Fast, + 130840: PGNM_Fast, + 130842: PGNM_Fast, + 130842: PGNM_Fast, + 130842: PGNM_Fast, + 130843: PGNM_Fast, + 130843: PGNM_Fast, + 130845: PGNM_Fast, + 130845: PGNM_Fast, + 130846: PGNM_Fast, + 130846: PGNM_Fast, + 130847: PGNM_Fast, + 130850: PGNM_Fast, + 130850: PGNM_Fast, + 130850: PGNM_Fast, + 130851: PGNM_Fast, + 130856: PGNM_Fast, + 130860: PGNM_Fast, + 130880: PGNM_Fast, + 130881: PGNM_Fast, + 130944: PGNM_Fast, + + } + + + + +def logError(fmt,*args): + print("ERROR:" +fmt%(args)) + +def dataToSep(data,maxbytes=None): + pd=None + dl=int(len(data)/2) + if maxbytes is not None and maxbytes < dl: + dl=maxbytes + for p in range(0,dl): + i=2*p + if pd is None: + pd=data[i:i+2] + else: + pd+=","+data[i:i+2] + return pd + +class CanFrame: + DUMP_PAT=re.compile(r'\(([^)]*)\) *([^ ]*) *([^#]*)#(.*)') + + def __init__(self,ts,pgn,src=1,dst=255,prio=1,data=None): + self.pgn=pgn + self.mode=PGN_MODES.get(pgn) + self.ts=ts + self.src=src + self.dst=dst + self.data=data + self.prio=prio + self.sequence=None + self.frame=None + if self.mode == PGNM_Fast and data is not None and len(self.data) >= 2: + fb=int(data[0:2],16) + self.frame=fb & 0x1f + self.sequence=fb >> 5 + + def key(self): + if self.sequence is None or self.pgn == 0: + return None + return f"{self.pgn}-{self.sequence}" + def getFPNum(self,bytes=False): + if self.frame != 0: + return None + if len(self.data) < 4: + return None + numbytes=int(self.data[2:4],16) + if bytes: + return numbytes + frames=int((numbytes-6-1)/7)+1+1 if numbytes > 6 else 1 + return frames + + def __str__(self): + return f"{self.ts},{self.prio},{self.pgn},{self.src},{self.dst},{int(len(self.data)/2 if self.data else 0)},{dataToSep(self.data)}" + + + @classmethod + def fromDump(cls,line): + '''(1658058069.867835) can0 09F80103#ACAF6C20B79AAC06''' + match=cls.DUMP_PAT.search(line) + if match is None: + logError("no dump pattern in line %s",line) + return + ts=match[1] + dt=datetime.datetime.fromtimestamp(float(ts),tz=datetime.UTC) + tstr=dt.strftime("%F-%T.")+dt.strftime("%f")[0:3] + data=match[4] + hdr=match[3] + hdrval=int(hdr,16) + #see candump2analyzer + src=hdrval & 0xff + prio=(hdrval >> 26) & 0x7 + PF=(hdrval >> 16) & 0xff + PS=(hdrval >> 8) & 0xff + RDP=(hdrval >> 24) & 3 + pgn=0 + if PF < 240: + dst=PS + pgn=(RDP << 16) + (PF << 8) + else: + dst=0xff + pgn=(RDP << 16) + (PF << 8)+PS + return CanFrame(tstr,pgn,src=src,dst=dst,prio=prio,data=data) + +class MultiFrame: + def __init__(self,firstFrame: CanFrame): + self.bytes="" + self.firstFrame=firstFrame + self.numFrames=firstFrame.getFPNum(bytes=False) + self.numBytes=firstFrame.getFPNum(bytes=True) + self.finished=False + self.addFrame(firstFrame) + def addFrame(self,frame:CanFrame): + if self.finished: + return False + if frame.frame is None: + return False + if frame.frame == 0: + self.bytes+=frame.data[4:] + else: + self.bytes+=frame.data[2:] + if frame.frame >= (self.numFrames-1): + self.finished=True + return True + + def __str__(self): + return f"{self.firstFrame.ts},{self.firstFrame.prio},{self.firstFrame.pgn},{self.firstFrame.src},{self.firstFrame.dst},{self.numBytes},{dataToSep(self.bytes,self.numBytes)}" + + +if __name__ == '__main__': + with open (sys.argv[1],"r") as fh: + buffer={} + lnr=0 + for line in fh: + lnr+=1 + frame=CanFrame.fromDump(line) + if frame.sequence is None: + print(frame) + else: + key=frame.key() + mf=buffer.get(key) + mustDelete=False + if mf is None: + if frame.frame != 0: + print(f"floating multi frame in line {lnr}: {frame}",file=sys.stderr) + continue + mf=MultiFrame(frame) + if not mf.finished: + buffer[key]=mf + else: + mf.addFrame(frame) + mustDelete=True + if mf.finished: + print(mf) + del buffer[key] + \ No newline at end of file From 4b03fa5a236ed16010c0ff1dc9fe600e8db948ce Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Tue, 23 Sep 2025 19:07:53 +0200 Subject: [PATCH 018/121] add filter to sendN2K --- tools/sendN2K.py | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) mode change 100644 => 100755 tools/sendN2K.py diff --git a/tools/sendN2K.py b/tools/sendN2K.py old mode 100644 new mode 100755 index c05bbda..0b84c96 --- a/tools/sendN2K.py +++ b/tools/sendN2K.py @@ -3,6 +3,7 @@ import re import sys import os import datetime +import getopt ###generated with getPgnType.py from canboat pgns.json PGNM_Fast=0 @@ -429,7 +430,7 @@ class CanFrame: def key(self): if self.sequence is None or self.pgn == 0: return None - return f"{self.pgn}-{self.sequence}" + return f"{self.pgn}-{self.sequence}-{self.src}" def getFPNum(self,bytes=False): if self.frame != 0: return None @@ -497,14 +498,38 @@ class MultiFrame: def __str__(self): return f"{self.firstFrame.ts},{self.firstFrame.prio},{self.firstFrame.pgn},{self.firstFrame.src},{self.firstFrame.dst},{self.numBytes},{dataToSep(self.bytes,self.numBytes)}" +def usage(): + print(f"usage: {sys.argv[0]} [-q] [-p pgn,pgn,...] file") + sys.exit(1) if __name__ == '__main__': - with open (sys.argv[1],"r") as fh: + try: + opts,args=getopt.getopt(sys.argv[1:],"hp:q") + except getopt.GetoptError as err: + err(err) + pgnlist=[] + quiet=False + for o,a in opts: + if o == '-h': + usage() + elif o == '-q': + quiet=True + elif o == '-p': + pgns=(int(x) for x in a.split(",")) + pgnlist.extend(pgns) + if len(args) < 1: + usage() + hasFilter=len(pgnlist) > 0 + if not quiet and hasFilter: + print(f"PGNs: {','.join(str(x) for x in pgnlist)}") + with open (args[0],"r") as fh: buffer={} lnr=0 for line in fh: lnr+=1 frame=CanFrame.fromDump(line) + if hasFilter and not frame.pgn in pgnlist: + continue if frame.sequence is None: print(frame) else: @@ -513,7 +538,8 @@ if __name__ == '__main__': mustDelete=False if mf is None: if frame.frame != 0: - print(f"floating multi frame in line {lnr}: {frame}",file=sys.stderr) + if not quiet: + print(f"floating multi frame in line {lnr}: {frame}",file=sys.stderr) continue mf=MultiFrame(frame) if not mf.finished: From e5c4f0b17958b5c640d98aa03bb2660a10efb491 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Tue, 23 Sep 2025 20:33:34 +0200 Subject: [PATCH 019/121] add actisense mode to sendN2K --- tools/sendN2K.py | 126 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 112 insertions(+), 14 deletions(-) diff --git a/tools/sendN2K.py b/tools/sendN2K.py index 0b84c96..2d3d7d3 100755 --- a/tools/sendN2K.py +++ b/tools/sendN2K.py @@ -4,6 +4,7 @@ import sys import os import datetime import getopt +import time ###generated with getPgnType.py from canboat pgns.json PGNM_Fast=0 @@ -422,6 +423,7 @@ class CanFrame: self.prio=prio self.sequence=None self.frame=None + self.len=8 if self.mode == PGNM_Fast and data is not None and len(self.data) >= 2: fb=int(data[0:2],16) self.frame=fb & 0x1f @@ -443,7 +445,7 @@ class CanFrame: return frames def __str__(self): - return f"{self.ts},{self.prio},{self.pgn},{self.src},{self.dst},{int(len(self.data)/2 if self.data else 0)},{dataToSep(self.data)}" + return f"{self.ts},{self.prio},{self.pgn},{self.src},{self.dst},{self.len},{dataToSep(self.data)}" @classmethod @@ -476,10 +478,14 @@ class CanFrame: class MultiFrame: def __init__(self,firstFrame: CanFrame): - self.bytes="" - self.firstFrame=firstFrame + self.data="" + self.prio=firstFrame.prio + self.pgn=firstFrame.pgn + self.src=firstFrame.src + self.dst=firstFrame.dst + self.ts=firstFrame.ts self.numFrames=firstFrame.getFPNum(bytes=False) - self.numBytes=firstFrame.getFPNum(bytes=True) + self.len=firstFrame.getFPNum(bytes=True) self.finished=False self.addFrame(firstFrame) def addFrame(self,frame:CanFrame): @@ -488,27 +494,102 @@ class MultiFrame: if frame.frame is None: return False if frame.frame == 0: - self.bytes+=frame.data[4:] + self.data+=frame.data[4:] else: - self.bytes+=frame.data[2:] + self.data+=frame.data[2:] if frame.frame >= (self.numFrames-1): self.finished=True return True def __str__(self): - return f"{self.firstFrame.ts},{self.firstFrame.prio},{self.firstFrame.pgn},{self.firstFrame.src},{self.firstFrame.dst},{self.numBytes},{dataToSep(self.bytes,self.numBytes)}" + return f"{self.ts},{self.prio},{self.pgn},{self.src},{self.dst},{self.len},{dataToSep(self.data,self.numBytes)}" def usage(): - print(f"usage: {sys.argv[0]} [-q] [-p pgn,pgn,...] file") + print(f"usage: {sys.argv[0]} [-q] [-p pgn,pgn,...] [-w waitsec] [ -f plain|actisense] file") sys.exit(1) +F_PLAIN=0 +F_ACT=1 +FORMATS={ + 'plain':F_PLAIN, + 'actisense':F_ACT +} + +MAX_ACT=400 +ACT_ESC=0x10 +ACT_START=0x2 +ACT_N2K=0x93 +ACT_END=0x3 + +class ActBuffer: + def __init__(self): + self.buf=bytearray(MAX_ACT) + self.sum=0 + self.idx=0 + self.clear() + def clear(self): + self.sum=0 + self.idx=2 + self.buf[0:2]=(ACT_ESC,ACT_START) + def add(self,val): + #TODO: len check? + val=val & 0xff + self.buf[self.idx]=val + self.sum = (self.sum + val) & 0xff + self.idx+=1 + if val == ACT_ESC: + self.buf[self.idx]=ACT_ESC + self.idx+=1 + def finalize(self): + self.sum=self.sum % 256 + self.sum = 256 - self.sum if self.sum != 0 else 0 + self.add(self.sum) + self.buf[self.idx]=ACT_ESC + self.idx+=1 + self.buf[self.idx]=ACT_END + self.idx+=1 + +actBuffer=ActBuffer() + +def send_act(frame_like,quiet): + try: + actBuffer.clear() + actBuffer.add(ACT_N2K) + actBuffer.add(frame_like.len+11) + actBuffer.add(frame_like.prio) + pgn=frame_like.pgn + actBuffer.add(pgn) + pgn = pgn >> 8 + actBuffer.add(pgn) + pgn = pgn >> 8; + actBuffer.add(pgn) + actBuffer.add(frame_like.dst) + actBuffer.add(frame_like.src) + #Time + actBuffer.add(0) + actBuffer.add(0) + actBuffer.add(0) + actBuffer.add(0) + + actBuffer.add(frame_like.len) + for i in range(0,frame_like.len*2,2): + actBuffer.add(int(frame_like.data[i:i+2],16)) + actBuffer.finalize() + sys.stdout.buffer.write(memoryview(actBuffer.buf)[0:actBuffer.idx]) + sys.stdout.buffer.flush() + except Exception as e: + if not quiet: + print(f"Error writing actisense for pgn {frame_like.pgn}, idx={actBuffer.idx}: {e}",file=sys.stderr) + if __name__ == '__main__': try: - opts,args=getopt.getopt(sys.argv[1:],"hp:q") - except getopt.GetoptError as err: - err(err) + opts,args=getopt.getopt(sys.argv[1:],"hp:qw:f:") + except getopt.GetoptError as e: + logError(e) pgnlist=[] quiet=False + delay=0.0 + format=F_PLAIN for o,a in opts: if o == '-h': usage() @@ -517,6 +598,12 @@ if __name__ == '__main__': elif o == '-p': pgns=(int(x) for x in a.split(",")) pgnlist.extend(pgns) + elif o == '-w': + delay=float(a) + elif o == '-f': + format=FORMATS.get(a) + if format is None: + logError(f"invalid format {a}, allowed {','.join(FORMATS.keys())}") if len(args) < 1: usage() hasFilter=len(pgnlist) > 0 @@ -531,7 +618,12 @@ if __name__ == '__main__': if hasFilter and not frame.pgn in pgnlist: continue if frame.sequence is None: - print(frame) + if format == F_PLAIN: + print(frame) + else: + send_act(frame,quiet) + if delay > 0: + time.sleep(delay) else: key=frame.key() mf=buffer.get(key) @@ -548,6 +640,12 @@ if __name__ == '__main__': mf.addFrame(frame) mustDelete=True if mf.finished: - print(mf) - del buffer[key] + if format == F_PLAIN: + print(mf) + else: + send_act(mf,quiet) + if mustDelete: + del buffer[key] + if delay > 0: + time.sleep(delay) \ No newline at end of file From e5968b84805e12c6dfedb8f86382baa195d94deb Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Wed, 24 Sep 2025 18:10:06 +0200 Subject: [PATCH 020/121] some error handling and stats to sendN2K --- tools/sendN2K.py | 119 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 87 insertions(+), 32 deletions(-) diff --git a/tools/sendN2K.py b/tools/sendN2K.py index 2d3d7d3..194bbd7 100755 --- a/tools/sendN2K.py +++ b/tools/sendN2K.py @@ -394,8 +394,10 @@ PGN_MODES={ -def logError(fmt,*args): - print("ERROR:" +fmt%(args)) +def logError(fmt,*args,keep=False): + print("ERROR:" +fmt%(args),file=sys.stderr) + if not keep: + sys.exit(1) def dataToSep(data,maxbytes=None): pd=None @@ -453,7 +455,7 @@ class CanFrame: '''(1658058069.867835) can0 09F80103#ACAF6C20B79AAC06''' match=cls.DUMP_PAT.search(line) if match is None: - logError("no dump pattern in line %s",line) + logError("no dump pattern in line %s",line,keep=True) return ts=match[1] dt=datetime.datetime.fromtimestamp(float(ts),tz=datetime.UTC) @@ -476,18 +478,16 @@ class CanFrame: pgn=(RDP << 16) + (PF << 8)+PS return CanFrame(tstr,pgn,src=src,dst=dst,prio=prio,data=data) -class MultiFrame: +class MultiFrame(CanFrame): def __init__(self,firstFrame: CanFrame): + super().__init__(firstFrame.ts,firstFrame.pgn, + src=firstFrame.src,dst=firstFrame.dst,prio=firstFrame.prio) self.data="" - self.prio=firstFrame.prio - self.pgn=firstFrame.pgn - self.src=firstFrame.src - self.dst=firstFrame.dst - self.ts=firstFrame.ts self.numFrames=firstFrame.getFPNum(bytes=False) self.len=firstFrame.getFPNum(bytes=True) self.finished=False self.addFrame(firstFrame) + def addFrame(self,frame:CanFrame): if self.finished: return False @@ -502,18 +502,27 @@ class MultiFrame: return True def __str__(self): - return f"{self.ts},{self.prio},{self.pgn},{self.src},{self.dst},{self.len},{dataToSep(self.data,self.numBytes)}" + return f"{self.ts},{self.prio},{self.pgn},{self.src},{self.dst},{self.len},{dataToSep(self.data,self.len)}" def usage(): print(f"usage: {sys.argv[0]} [-q] [-p pgn,pgn,...] [-w waitsec] [ -f plain|actisense] file") sys.exit(1) -F_PLAIN=0 -F_ACT=1 -FORMATS={ - 'plain':F_PLAIN, - 'actisense':F_ACT -} + +class Format: + F_PLAIN=0 + N_PLAIN='plain' + F_ACT=1 + N_ACT='actisense' + def __init__(self,name,key,merge=True): + self.key=key + self.name=name + self.merge=merge + +FORMATS=[ + Format(Format.N_PLAIN,Format.F_PLAIN), + Format(Format.N_ACT,Format.F_ACT) +] MAX_ACT=400 ACT_ESC=0x10 @@ -551,7 +560,7 @@ class ActBuffer: actBuffer=ActBuffer() -def send_act(frame_like,quiet): +def send_act(frame_like:CanFrame,quiet): try: actBuffer.clear() actBuffer.add(ACT_N2K) @@ -575,11 +584,59 @@ def send_act(frame_like,quiet): for i in range(0,frame_like.len*2,2): actBuffer.add(int(frame_like.data[i:i+2],16)) actBuffer.finalize() - sys.stdout.buffer.write(memoryview(actBuffer.buf)[0:actBuffer.idx]) + written=sys.stdout.buffer.write(memoryview(actBuffer.buf)[0:actBuffer.idx]) + if (written != actBuffer.idx): + if not quiet: + logError(f"actisense not all bytes written {written}/{actBuffer.idx} for pgn={frame_like.pgn} ts={frame_like.ts}",keep=True) sys.stdout.buffer.flush() + return True except Exception as e: if not quiet: print(f"Error writing actisense for pgn {frame_like.pgn}, idx={actBuffer.idx}: {e}",file=sys.stderr) + return False + +class Counters: + C_OK=1 + C_FAIL=2 + C_FRAME=3 + TITLES={ + C_OK:'OK', + C_FAIL:'FAIL', + C_FRAME:'FRAMES' + } + def __init__(self): + self.counters={} + for i in self.TITLES.keys(): + self.counters[i]=0 + def add(self,idx:int): + if idx not in self.TITLES.keys(): + return + self.counters[idx]+=1 + def __str__(self): + rt=None + for i in self.TITLES.keys(): + v=f"{self.TITLES[i]}:{self.counters[i]}" + if rt is None: + rt=v + else: + rt+=","+v + return rt + +def writeOut(frame:CanFrame,format:Format,quiet:bool,counters:Counters): + rt=False + if format.key == Format.F_ACT: + rt= send_act(frame,quiet) + elif format.key == Format.F_PLAIN: + print(frame) + rt=True + counters.add(Counters.C_OK if rt else Counters.C_FAIL) + return rt + +def findFormat(name:str)->Format: + for f in FORMATS: + if f.name == name: + return f + return None if __name__ == '__main__': try: @@ -589,7 +646,7 @@ if __name__ == '__main__': pgnlist=[] quiet=False delay=0.0 - format=F_PLAIN + format=findFormat(Format.N_PLAIN) for o,a in opts: if o == '-h': usage() @@ -601,14 +658,15 @@ if __name__ == '__main__': elif o == '-w': delay=float(a) elif o == '-f': - format=FORMATS.get(a) + format=findFormat(a) if format is None: - logError(f"invalid format {a}, allowed {','.join(FORMATS.keys())}") + logError(f"invalid format {a}, allowed {','.join(x.name for x in FORMATS)}") if len(args) < 1: usage() hasFilter=len(pgnlist) > 0 if not quiet and hasFilter: - print(f"PGNs: {','.join(str(x) for x in pgnlist)}") + print(f"PGNs: {','.join(str(x) for x in pgnlist)}",file=sys.stderr) + counters=Counters() with open (args[0],"r") as fh: buffer={} lnr=0 @@ -617,11 +675,9 @@ if __name__ == '__main__': frame=CanFrame.fromDump(line) if hasFilter and not frame.pgn in pgnlist: continue - if frame.sequence is None: - if format == F_PLAIN: - print(frame) - else: - send_act(frame,quiet) + counters.add(Counters.C_FRAME) + if frame.sequence is None or not format.merge: + writeOut(frame,format,quiet,counters=counters) if delay > 0: time.sleep(delay) else: @@ -640,12 +696,11 @@ if __name__ == '__main__': mf.addFrame(frame) mustDelete=True if mf.finished: - if format == F_PLAIN: - print(mf) - else: - send_act(mf,quiet) + writeOut(mf,format,quiet,counters=counters) if mustDelete: del buffer[key] if delay > 0: - time.sleep(delay) + time.sleep(delay) + if not quiet: + print(f"STATISTICS: {counters}",file=sys.stderr) \ No newline at end of file From b7cd8c6bdd971d885ea6575ed9648c57f8a2ad43 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Wed, 24 Sep 2025 18:29:22 +0200 Subject: [PATCH 021/121] add pass format to sendN2K --- tools/sendN2K.py | 53 +++++++++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/tools/sendN2K.py b/tools/sendN2K.py index 194bbd7..4d7c11f 100755 --- a/tools/sendN2K.py +++ b/tools/sendN2K.py @@ -411,11 +411,22 @@ def dataToSep(data,maxbytes=None): else: pd+=","+data[i:i+2] return pd +class Format: + F_PLAIN=0 + N_PLAIN='plain' + F_ACT=1 + N_ACT='actisense' + F_PASS=2 + N_PASS='pass' + def __init__(self,name,key,merge=True): + self.key=key + self.name=name + self.merge=merge class CanFrame: DUMP_PAT=re.compile(r'\(([^)]*)\) *([^ ]*) *([^#]*)#(.*)') - def __init__(self,ts,pgn,src=1,dst=255,prio=1,data=None): + def __init__(self,ts,pgn,src=1,dst=255,prio=1,dev=None,hdr=None,data=None): self.pgn=pgn self.mode=PGN_MODES.get(pgn) self.ts=ts @@ -426,6 +437,8 @@ class CanFrame: self.sequence=None self.frame=None self.len=8 + self.dev=dev + self.hdr=hdr if self.mode == PGNM_Fast and data is not None and len(self.data) >= 2: fb=int(data[0:2],16) self.frame=fb & 0x1f @@ -446,9 +459,19 @@ class CanFrame: frames=int((numbytes-6-1)/7)+1+1 if numbytes > 6 else 1 return frames + def _formatTs(self): + dt=datetime.datetime.fromtimestamp(self.ts,tz=datetime.UTC) + return dt.strftime("%F-%T.")+dt.strftime("%f")[0:3] + def __str__(self): - return f"{self.ts},{self.prio},{self.pgn},{self.src},{self.dst},{self.len},{dataToSep(self.data)}" + return f"{self._formatTs()},{self.prio},{self.pgn},{self.src},{self.dst},{self.len},{dataToSep(self.data)}" + def printOut(self,format:Format): + if format.key == Format.F_PASS: + return f"({self.ts}) {self.dev} {self.hdr}#{self.data}" + else: + return str(self) + @classmethod def fromDump(cls,line): @@ -458,8 +481,6 @@ class CanFrame: logError("no dump pattern in line %s",line,keep=True) return ts=match[1] - dt=datetime.datetime.fromtimestamp(float(ts),tz=datetime.UTC) - tstr=dt.strftime("%F-%T.")+dt.strftime("%f")[0:3] data=match[4] hdr=match[3] hdrval=int(hdr,16) @@ -476,12 +497,13 @@ class CanFrame: else: dst=0xff pgn=(RDP << 16) + (PF << 8)+PS - return CanFrame(tstr,pgn,src=src,dst=dst,prio=prio,data=data) + return CanFrame(float(ts),pgn,src=src,dst=dst,prio=prio,data=data,dev=match[2],hdr=hdr) class MultiFrame(CanFrame): def __init__(self,firstFrame: CanFrame): super().__init__(firstFrame.ts,firstFrame.pgn, - src=firstFrame.src,dst=firstFrame.dst,prio=firstFrame.prio) + src=firstFrame.src,dst=firstFrame.dst,prio=firstFrame.prio, + dev=firstFrame.dev,hdr=firstFrame.hdr) self.data="" self.numFrames=firstFrame.getFPNum(bytes=False) self.len=firstFrame.getFPNum(bytes=True) @@ -502,26 +524,19 @@ class MultiFrame(CanFrame): return True def __str__(self): - return f"{self.ts},{self.prio},{self.pgn},{self.src},{self.dst},{self.len},{dataToSep(self.data,self.len)}" + return f"{self._formatTs()},{self.prio},{self.pgn},{self.src},{self.dst},{self.len},{dataToSep(self.data,self.len)}" def usage(): print(f"usage: {sys.argv[0]} [-q] [-p pgn,pgn,...] [-w waitsec] [ -f plain|actisense] file") sys.exit(1) -class Format: - F_PLAIN=0 - N_PLAIN='plain' - F_ACT=1 - N_ACT='actisense' - def __init__(self,name,key,merge=True): - self.key=key - self.name=name - self.merge=merge + FORMATS=[ Format(Format.N_PLAIN,Format.F_PLAIN), - Format(Format.N_ACT,Format.F_ACT) + Format(Format.N_ACT,Format.F_ACT), + Format(Format.N_PASS,Format.F_PASS,False) ] MAX_ACT=400 @@ -626,8 +641,8 @@ def writeOut(frame:CanFrame,format:Format,quiet:bool,counters:Counters): rt=False if format.key == Format.F_ACT: rt= send_act(frame,quiet) - elif format.key == Format.F_PLAIN: - print(frame) + else: + print(frame.printOut(format)) rt=True counters.add(Counters.C_OK if rt else Counters.C_FAIL) return rt From 78aafd308a54753ed97da0ea9a9d264487fbf462 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Wed, 24 Sep 2025 20:30:44 +0200 Subject: [PATCH 022/121] seasmart for sendN2K --- tools/sendN2K.py | 73 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 5 deletions(-) diff --git a/tools/sendN2K.py b/tools/sendN2K.py index 4d7c11f..d3dca5b 100755 --- a/tools/sendN2K.py +++ b/tools/sendN2K.py @@ -418,6 +418,8 @@ class Format: N_ACT='actisense' F_PASS=2 N_PASS='pass' + F_SEASMART=3 + N_SEASMART="seasmart" def __init__(self,name,key,merge=True): self.key=key self.name=name @@ -536,7 +538,8 @@ def usage(): FORMATS=[ Format(Format.N_PLAIN,Format.F_PLAIN), Format(Format.N_ACT,Format.F_ACT), - Format(Format.N_PASS,Format.F_PASS,False) + Format(Format.N_PASS,Format.F_PASS,False), + Format(Format.N_SEASMART,Format.F_SEASMART) ] MAX_ACT=400 @@ -575,7 +578,41 @@ class ActBuffer: actBuffer=ActBuffer() -def send_act(frame_like:CanFrame,quiet): + +LB=b'0000000000000' +B_STAR=0x2a +class SeasmartBuffer: + def __init__(self): + self.buf=bytearray(500) + self.idx=0 + self.clear() + def clear(self): + self.idx=0 + def addB(self,bv): + l=len(bv) + self.buf[self.idx:self.idx+l]=bv + self.idx+=l + def addVal(self,val,blen=2): + hs=hex(val)[2:].encode() + if len(hs) != blen: + hs=(LB+hs)[-blen:] + self.addB(hs) + + def finalize(self): + sum=0 + self.buf[self.idx]=B_STAR + self.idx+=1 + for b in memoryview(self.buf)[1:]: + if b == B_STAR: + break + sum ^= b + sum = sum & 0xff + self.addVal(sum) + self.addB(b'\x0d\x0a') + +seasmartBuffer=SeasmartBuffer() + +def send_act(frame_like:CanFrame,quiet,stream): try: actBuffer.clear() actBuffer.add(ACT_N2K) @@ -599,17 +636,41 @@ def send_act(frame_like:CanFrame,quiet): for i in range(0,frame_like.len*2,2): actBuffer.add(int(frame_like.data[i:i+2],16)) actBuffer.finalize() - written=sys.stdout.buffer.write(memoryview(actBuffer.buf)[0:actBuffer.idx]) + written=stream.write(memoryview(actBuffer.buf)[0:actBuffer.idx]) if (written != actBuffer.idx): if not quiet: logError(f"actisense not all bytes written {written}/{actBuffer.idx} for pgn={frame_like.pgn} ts={frame_like.ts}",keep=True) - sys.stdout.buffer.flush() + stream.flush() return True except Exception as e: if not quiet: print(f"Error writing actisense for pgn {frame_like.pgn}, idx={actBuffer.idx}: {e}",file=sys.stderr) return False +BK=b',' +def send_seasmart(frame_like:CanFrame,quiet,stream): + try: + seasmartBuffer.clear() + seasmartBuffer.addB(b'$PCDIN,') + seasmartBuffer.addVal(frame_like.pgn,6) + seasmartBuffer.addB(BK) + seasmartBuffer.addVal(int(time.time()),8) + seasmartBuffer.addB(BK) + seasmartBuffer.addVal(frame_like.src) + seasmartBuffer.addB(BK) + seasmartBuffer.addB(frame_like.data.encode()) + seasmartBuffer.finalize() + written=stream.write(memoryview(seasmartBuffer.buf)[0:seasmartBuffer.idx]) + if (written != seasmartBuffer.idx): + if not quiet: + raise Exception(f"seasmart not all bytes written {written}/{seasmartBuffer.idx} for pgn={frame_like.pgn} ts={frame_like.ts}") + stream.flush() + return True + except Exception as e: + if not quiet: + logError(f"writing seasmart for pgn {frame_like.pgn}, idx={seasmartBuffer.idx}: {e}",keep=True) + return False + class Counters: C_OK=1 C_FAIL=2 @@ -640,7 +701,9 @@ class Counters: def writeOut(frame:CanFrame,format:Format,quiet:bool,counters:Counters): rt=False if format.key == Format.F_ACT: - rt= send_act(frame,quiet) + rt= send_act(frame,quiet,sys.stdout.buffer) + elif format.key == Format.F_SEASMART: + rt= send_seasmart(frame,quiet,sys.stdout.buffer) else: print(frame.printOut(format)) rt=True From 448af708d4f1a58d987e286460955dd0ee8a031f Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Thu, 25 Sep 2025 17:37:59 +0200 Subject: [PATCH 023/121] fill timestamp for actisense with frame timestamp in sendN2K --- tools/sendN2K.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tools/sendN2K.py b/tools/sendN2K.py index d3dca5b..8921348 100755 --- a/tools/sendN2K.py +++ b/tools/sendN2K.py @@ -529,7 +529,7 @@ class MultiFrame(CanFrame): return f"{self._formatTs()},{self.prio},{self.pgn},{self.src},{self.dst},{self.len},{dataToSep(self.data,self.len)}" def usage(): - print(f"usage: {sys.argv[0]} [-q] [-p pgn,pgn,...] [-w waitsec] [ -f plain|actisense] file") + print(f"usage: {sys.argv[0]} [-q] [-p pgn,pgn,...] [-w waitsec] [ -f plain|actisense] file",file=sys.stderr) sys.exit(1) @@ -627,10 +627,11 @@ def send_act(frame_like:CanFrame,quiet,stream): actBuffer.add(frame_like.dst) actBuffer.add(frame_like.src) #Time - actBuffer.add(0) - actBuffer.add(0) - actBuffer.add(0) - actBuffer.add(0) + ts=int(frame_like.ts) + actBuffer.add(ts>>24) + actBuffer.add(ts>>16) + actBuffer.add(ts>>8) + actBuffer.add(ts) actBuffer.add(frame_like.len) for i in range(0,frame_like.len*2,2): @@ -654,7 +655,7 @@ def send_seasmart(frame_like:CanFrame,quiet,stream): seasmartBuffer.addB(b'$PCDIN,') seasmartBuffer.addVal(frame_like.pgn,6) seasmartBuffer.addB(BK) - seasmartBuffer.addVal(int(time.time()),8) + seasmartBuffer.addVal(int(frame_like.ts),8) seasmartBuffer.addB(BK) seasmartBuffer.addVal(frame_like.src) seasmartBuffer.addB(BK) From 24502e423eb2dc547fe246c4ed5f108726adab29 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Thu, 25 Sep 2025 19:25:54 +0200 Subject: [PATCH 024/121] set talker/channel when converting AIS from N2K, new lib version n2ktoais --- lib/nmea2kto0183/N2kDataToNMEA0183.cpp | 43 +++++++++++++++++++++----- platformio.ini | 2 +- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp index ade2c3f..1a865e2 100644 --- a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp +++ b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp @@ -708,6 +708,32 @@ private: } } + //helper for converting the AIS transceiver info to talker/channel + + void setTalkerChannel(tNMEA0183AISMsg &msg, tN2kAISTransceiverInformation &transceiver){ + bool channelA=true; + bool own=false; + switch (transceiver){ + case tN2kAISTransceiverInformation::N2kaischannel_A_VDL_reception: + channelA=true; + own=false; + break; + case tN2kAISTransceiverInformation::N2kaischannel_B_VDL_reception: + channelA=false; + own=false; + break; + case tN2kAISTransceiverInformation::N2kaischannel_A_VDL_transmission: + channelA=true; + own=true; + break; + case tN2kAISTransceiverInformation::N2kaischannel_B_VDL_transmission: + channelA=false; + own=true; + break; + } + msg.SetChannelAndTalker(channelA,own); + } + //***************************************************************************** // 129038 AIS Class A Position Report (Message 1, 2, 3) void HandleAISClassAPosReport(const tN2kMsg &N2kMsg) @@ -736,7 +762,7 @@ private: { - + setTalkerChannel(NMEA0183AISMsg,_AISTransceiverInformation); if (_MessageType < 1 || _MessageType > 3) _MessageType=1; //only allow type 1...3 for 129038 if (SetAISClassABMessage1(NMEA0183AISMsg, _MessageType, _Repeat, _UserID, _Latitude, _Longitude, _Accuracy, _RAIM, _Seconds, _COG, _SOG, _Heading, _ROT, _NavStatus)) @@ -779,11 +805,10 @@ private: _Length, _Beam, _PosRefStbd, _PosRefBow, _ETAdate, _ETAtime, _Draught, _Destination,21, _AISversion, _GNSStype, _DTE, _AISinfo,_SID)) { - - + setTalkerChannel(NMEA0183AISMsg,_AISinfo); if (SetAISClassAMessage5(NMEA0183AISMsg, _MessageID, _Repeat, _UserID, _IMONumber, _Callsign, _Name, _VesselType, _Length, _Beam, _PosRefStbd, _PosRefBow, _ETAdate, _ETAtime, _Draught, _Destination, - _GNSStype, _DTE)) + _GNSStype, _DTE,_AISversion)) { if (NMEA0183AISMsg.BuildMsg5Part1()){ SendMessage(NMEA0183AISMsg); @@ -815,15 +840,15 @@ private: tN2kAISUnit _Unit; bool _Display, _DSC, _Band, _Msg22, _State; tN2kAISMode _Mode; - tN2kAISTransceiverInformation _AISTranceiverInformation; + tN2kAISTransceiverInformation _AISTransceiverInformation; uint8_t _SID; if (ParseN2kPGN129039(N2kMsg, _MessageID, _Repeat, _UserID, _Latitude, _Longitude, _Accuracy, _RAIM, - _Seconds, _COG, _SOG, _AISTranceiverInformation, _Heading, _Unit, _Display, _DSC, _Band, _Msg22, _Mode, _State,_SID)) + _Seconds, _COG, _SOG, _AISTransceiverInformation, _Heading, _Unit, _Display, _DSC, _Band, _Msg22, _Mode, _State,_SID)) { tNMEA0183AISMsg NMEA0183AISMsg; - + setTalkerChannel(NMEA0183AISMsg,_AISTransceiverInformation); if (SetAISClassBMessage18(NMEA0183AISMsg, _MessageID, _Repeat, _UserID, _Latitude, _Longitude, _Accuracy, _RAIM, _Seconds, _COG, _SOG, _Heading, _Unit, _Display, _DSC, _Band, _Msg22, _Mode, _State)) { @@ -851,6 +876,7 @@ private: { tNMEA0183AISMsg NMEA0183AISMsg; + setTalkerChannel(NMEA0183AISMsg,_AISInfo); if (SetAISClassBMessage24PartA(NMEA0183AISMsg, _MessageID, _Repeat, _UserID, _Name)) { } @@ -881,7 +907,7 @@ private: { tNMEA0183AISMsg NMEA0183AISMsg; - + setTalkerChannel(NMEA0183AISMsg,_AISInfo); if (SetAISClassBMessage24(NMEA0183AISMsg, _MessageID, _Repeat, _UserID, _VesselType, _Vendor, _Callsign, _Length, _Beam, _PosRefStbd, _PosRefBow, _MothershipID)) { @@ -905,6 +931,7 @@ private: tN2kAISAtoNReportData data; if (ParseN2kPGN129041(N2kMsg,data)){ tNMEA0183AISMsg nmea0183Msg; + setTalkerChannel(nmea0183Msg,data.AISTransceiverInformation); if (SetAISMessage21( nmea0183Msg, data.Repeat, diff --git a/platformio.ini b/platformio.ini index 4e0773a..89187e0 100644 --- a/platformio.ini +++ b/platformio.ini @@ -28,7 +28,7 @@ lib_deps = ESPmDNS WiFi Update - nmea2kto183ais=https://github.com/ronzeiller/NMEA0183-AIS#7d2bfab54e3e5bfaab36fe6aa356241baa7251c2 + nmea2kto183ais=https://github.com/wellenvogel/esp32n2kto183ais.git#20250925 [devdeps] lib_deps= From 9633abc4811ad6dc3952b6f1b5dccbe83168d4d5 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Fri, 26 Sep 2025 19:56:44 +0200 Subject: [PATCH 025/121] correct timestamp for pass format --- tools/sendN2K.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/tools/sendN2K.py b/tools/sendN2K.py index 8921348..e5c55db 100755 --- a/tools/sendN2K.py +++ b/tools/sendN2K.py @@ -470,14 +470,16 @@ class CanFrame: def printOut(self,format:Format): if format.key == Format.F_PASS: - return f"({self.ts}) {self.dev} {self.hdr}#{self.data}" + return f"({self.ts:.6f}) {self.dev} {self.hdr}#{self.data}" else: return str(self) @classmethod - def fromDump(cls,line): + def fromDump(cls,line:str): '''(1658058069.867835) can0 09F80103#ACAF6C20B79AAC06''' + if line is None or line == '': + return None match=cls.DUMP_PAT.search(line) if match is None: logError("no dump pattern in line %s",line,keep=True) @@ -588,9 +590,13 @@ class SeasmartBuffer: self.clear() def clear(self): self.idx=0 - def addB(self,bv): + def addB(self,bv,mlen=None): l=len(bv) - self.buf[self.idx:self.idx+l]=bv + if mlen is not None and mlen < l: + l=mlen + self.buf[self.idx:self.idx+l]=memoryview(bv)[0:l] + else: + self.buf[self.idx:self.idx+l]=bv self.idx+=l def addVal(self,val,blen=2): hs=hex(val)[2:].encode() @@ -659,7 +665,7 @@ def send_seasmart(frame_like:CanFrame,quiet,stream): seasmartBuffer.addB(BK) seasmartBuffer.addVal(frame_like.src) seasmartBuffer.addB(BK) - seasmartBuffer.addB(frame_like.data.encode()) + seasmartBuffer.addB(frame_like.data.encode(),mlen=frame_like.len*2) seasmartBuffer.finalize() written=stream.write(memoryview(seasmartBuffer.buf)[0:seasmartBuffer.idx]) if (written != seasmartBuffer.idx): @@ -752,6 +758,8 @@ if __name__ == '__main__': for line in fh: lnr+=1 frame=CanFrame.fromDump(line) + if frame is None: + continue if hasFilter and not frame.pgn in pgnlist: continue counters.add(Counters.C_FRAME) From 60d06cd9eea8a863fdccb3d21b2beeec96b751d3 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Fri, 26 Sep 2025 19:57:08 +0200 Subject: [PATCH 026/121] remove wrong addEmptyField --- lib/nmea2kto0183/N2kDataToNMEA0183.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp index 1a865e2..eeeea60 100644 --- a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp +++ b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp @@ -951,7 +951,6 @@ private: data.AssignedModeFlag, data.GNSSType, data.AtoNStatus, - data.AISTransceiverInformation, data.AtoNName )){ SendMessage(nmea0183Msg); From 3f22164b1d435095fcaedac23dab8fbdc904f353 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Fri, 26 Sep 2025 20:00:30 +0200 Subject: [PATCH 027/121] use forked NMEA2000 lib --- platformio.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/platformio.ini b/platformio.ini index 89187e0..53bdb61 100644 --- a/platformio.ini +++ b/platformio.ini @@ -18,7 +18,7 @@ extra_configs= [basedeps] lib_deps = - ttlappalainen/NMEA2000-library @ 4.22.0 + ttlappalainen_NMEA2000=https://github.com/wellenvogel/NMEA2000.git#20250926 ttlappalainen/NMEA0183 @ 1.10.1 ArduinoJson @ 6.18.5 AsyncTCP-esphome @ 2.0.1 @@ -28,11 +28,11 @@ lib_deps = ESPmDNS WiFi Update - nmea2kto183ais=https://github.com/wellenvogel/esp32n2kto183ais.git#20250925 + nmea2kto183ais=https://github.com/wellenvogel/esp32n2kto183ais.git#20250926 [devdeps] lib_deps= - ttlappalainen/NMEA2000-library @ 4.22.0 + ttlappalainen_NMEA2000=symlink://../NMEA2000 ttlappalainen/NMEA0183 @ 1.10.1 ArduinoJson @ 6.18.5 AsyncTCP-esphome @ 2.0.1 From d0966159c02e1ee946572e029734ff333b3ce793 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Sun, 28 Sep 2025 18:37:26 +0200 Subject: [PATCH 028/121] separate building AIS class 24 --- lib/nmea2kto0183/N2kDataToNMEA0183.cpp | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp index eeeea60..e2b61fd 100644 --- a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp +++ b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp @@ -879,6 +879,7 @@ private: setTalkerChannel(NMEA0183AISMsg,_AISInfo); if (SetAISClassBMessage24PartA(NMEA0183AISMsg, _MessageID, _Repeat, _UserID, _Name)) { + SendMessage(NMEA0183AISMsg); } } return; @@ -908,17 +909,10 @@ private: tNMEA0183AISMsg NMEA0183AISMsg; setTalkerChannel(NMEA0183AISMsg,_AISInfo); - if (SetAISClassBMessage24(NMEA0183AISMsg, _MessageID, _Repeat, _UserID, _VesselType, _Vendor, _Callsign, + if (SetAISClassBMessage24PartB(NMEA0183AISMsg, _MessageID, _Repeat, _UserID, _VesselType, _Vendor, _Callsign, _Length, _Beam, _PosRefStbd, _PosRefBow, _MothershipID)) { - if (NMEA0183AISMsg.BuildMsg24PartA()){ - SendMessage(NMEA0183AISMsg); - } - - if (NMEA0183AISMsg.BuildMsg24PartB()){ - SendMessage(NMEA0183AISMsg); - } - + SendMessage(NMEA0183AISMsg); } } return; From df9b377b316603902c78de0d72bc077f6a35a5b3 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Sun, 28 Sep 2025 18:47:17 +0200 Subject: [PATCH 029/121] move the nmea2ktoais functions back into our code base. --- lib/nmea2ktoais/NMEA0183AISMessages.cpp | 601 ++++++++++++++++++++++++ lib/nmea2ktoais/NMEA0183AISMessages.h | 87 ++++ lib/nmea2ktoais/NMEA0183AISMsg.cpp | 201 ++++++++ lib/nmea2ktoais/NMEA0183AISMsg.h | 97 ++++ lib/nmea2ktoais/README.md | 73 +++ 5 files changed, 1059 insertions(+) create mode 100644 lib/nmea2ktoais/NMEA0183AISMessages.cpp create mode 100644 lib/nmea2ktoais/NMEA0183AISMessages.h create mode 100644 lib/nmea2ktoais/NMEA0183AISMsg.cpp create mode 100644 lib/nmea2ktoais/NMEA0183AISMsg.h create mode 100644 lib/nmea2ktoais/README.md diff --git a/lib/nmea2ktoais/NMEA0183AISMessages.cpp b/lib/nmea2ktoais/NMEA0183AISMessages.cpp new file mode 100644 index 0000000..7a64d70 --- /dev/null +++ b/lib/nmea2ktoais/NMEA0183AISMessages.cpp @@ -0,0 +1,601 @@ +/* +NMEA0183AISMessages.cpp + +Copyright (c) 2019 Ronnie Zeiller + +Based on the works of Timo Lappalainen NMEA2000 and NMEA0183 Library +Thanks to Eric S. Raymond (https://gpsd.gitlab.io/gpsd/AIVDM.html) +and Kurt Schwehr for their informations on AIS encoding. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + +#include "NMEA0183AISMessages.h" +#include +#include +#include +//#include +//#include +#include +#include +#include "NMEA0183AISMsg.h" + +const double pi=3.1415926535897932384626433832795; +const double kmhToms=1000.0/3600.0; +const double knToms=1852.0/3600.0; +const double degToRad=pi/180.0; +const double radToDeg=180.0/pi; +const double msTokmh=3600.0/1000.0; +const double msTokn=3600.0/1852.0; +const double nmTom=1.852*1000; +const double mToFathoms=0.546806649; +const double mToFeet=3.2808398950131; +const double radsToDegMin = 60 * 360.0 / (2 * pi); // [rad/s -> degree/minute] + + +// ************************ Helper for AIS *********************************** +static bool AddMessageType(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageType); +static bool AddRepeat(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t Repeat); +static bool AddUserID(tNMEA0183AISMsg &NMEA0183AISMsg, uint32_t UserID); +static bool AddIMONumber(tNMEA0183AISMsg &NMEA0183AISMsg, uint32_t &IMONumber); +static bool AddText(tNMEA0183AISMsg &NMEA0183AISMsg, char *FieldVal, uint8_t length); +//static bool AddVesselType(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t VesselType); +static bool AddDimensions(tNMEA0183AISMsg &NMEA0183AISMsg, double Length, double Beam, double PosRefStbd, double PosRefBow); +static bool AddNavStatus(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t &NavStatus); +static bool AddROT(tNMEA0183AISMsg &NMEA0183AISMsg, double &rot); +static bool AddSOG (tNMEA0183AISMsg &NMEA0183AISMsg, double &sog); +static bool AddLongitude(tNMEA0183AISMsg &NMEA0183AISMsg, double &Longitude); +static bool AddLatitude(tNMEA0183AISMsg &NMEA0183AISMsg, double &Latitude); +static bool AddHeading (tNMEA0183AISMsg &NMEA0183AISMsg, double &heading); +static bool AddCOG(tNMEA0183AISMsg &NMEA0183AISMsg, double cog); +static bool AddSeconds (tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t &Seconds); +static bool AddEPFDFixType(tNMEA0183AISMsg &NMEA0183AISMsg, tN2kGNSStype &GNSStype); +static bool AddStaticDraught(tNMEA0183AISMsg &NMEA0183AISMsg, double &Draught); +static bool AddETADateTime(tNMEA0183AISMsg &NMEA0183AISMsg, uint16_t &ETAdate, double &ETAtime); + +//***************************************************************************** +// Types 1, 2 and 3: Position Report Class A or B -> https://gpsd.gitlab.io/gpsd/AIVDM.html +// total of 168 bits, occupying one AIVDM sentence +// Example: !AIVDM,1,1,,A,133m@ogP00PD;88MD5MTDww@2D7k,0*46 +// Payload: Payload: 133m@ogP00PD;88MD5MTDww@2D7k +// Message type 1 has a payload length of 168 bits. +// because AIS encodes messages using a 6-bits ASCII mechanism and 168 divided by 6 is 28. +// +// Got values from: ParseN2kPGN129038() +bool SetAISClassABMessage1( tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageType, uint8_t Repeat, + uint32_t UserID, double Latitude, double Longitude, bool Accuracy, bool RAIM, uint8_t Seconds, + double COG, double SOG, double Heading, double ROT, uint8_t NavStatus ) { + + NMEA0183AISMsg.ClearAIS(); + if ( !AddMessageType(NMEA0183AISMsg, MessageType) ) return false; // 0 - 5 | 6 Message Type -> Constant: 1 + if ( !AddRepeat(NMEA0183AISMsg, Repeat) ) return false; // 6 - 7 | 2 Repeat Indicator: 0 = default; 3 = do not repeat any more + if ( !AddUserID(NMEA0183AISMsg, UserID) ) return false; // 8 - 37 | 30 MMSI + if ( !AddNavStatus(NMEA0183AISMsg, NavStatus) ) return false; // 38-41 | 4 Navigational Status e.g.: "Under way sailing" + if ( !AddROT(NMEA0183AISMsg, ROT) ) return false; // 42-49 | 8 Rate of Turn (ROT) + if ( !AddSOG(NMEA0183AISMsg, SOG) ) return false; // 50-59 | 10 [m/s -> kts] SOG with one digit x10, 1023 = N/A + if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Accuracy) ) return false;// 60 | 1 GPS Accuracy 1 oder 0, Default 0 + if ( !AddLongitude(NMEA0183AISMsg, Longitude) ) return false; // 61-88 | 28 Longitude in Minutes / 10000 + if ( !AddLatitude(NMEA0183AISMsg, Latitude) ) return false; // 89-115 | 27 Latitude in Minutes / 10000 + if ( !AddCOG(NMEA0183AISMsg, COG) ) return false; // 116-127 | 12 Course over ground will be 3600 (0xE10) if that data is not available. + if ( !AddHeading (NMEA0183AISMsg, Heading) ) return false; // 128-136 | 9 True Heading (HDG) + if ( !AddSeconds(NMEA0183AISMsg, Seconds) ) return false; // 137-142 | 6 Seconds in UTC timestamp) + if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 2) ) return false; // 143-144 | 2 Maneuver Indicator: 0 (default) 1, 2 (not delivered within this PGN) + if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 3) ) return false; // 145-147 | 3 Spare + if ( !NMEA0183AISMsg.AddBoolToPayloadBin(RAIM) ) return false; // 148-148 | 1 RAIM flag 0 = RAIM not in use (default), 1 = RAIM in use + if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 19) ) return false; // 149-167 | 19 Radio Status (-> 0 NOT SENT WITH THIS PGN!!!!!) + if ( !NMEA0183AISMsg.InitAis()) return false; + int padBits=0; + if ( !NMEA0183AISMsg.AddStrField( NMEA0183AISMsg.GetPayloadFix(padBits) ) ) return false; + if ( !NMEA0183AISMsg.AddUInt32Field(padBits) ) return false; + return true; +} + +// ***************************************************************************** +// https://www.navcen.uscg.gov/?pageName=AISMessagesAStatic# +// AIS class A Static and Voyage Related Data +// Values derived from ParseN2kPGN129794(); +bool SetAISClassAMessage5(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, + uint32_t UserID, uint32_t IMONumber, char *Callsign, char *Name, + uint8_t VesselType, double Length, double Beam, double PosRefStbd, + double PosRefBow, uint16_t ETAdate, double ETAtime, double Draught, + char *Destination, tN2kGNSStype GNSStype, uint8_t DTE, + tN2kAISVersion AISversion) { + + // AIS Type 5 Message + NMEA0183AISMsg.ClearAIS(); + if ( !AddMessageType(NMEA0183AISMsg, 5) ) return false; // 0 - 5 | 6 Message Type -> Constant: 5 + if ( !AddRepeat(NMEA0183AISMsg, Repeat) ) return false; // 6 - 7 | 2 Repeat Indicator: 0 = default; 3 = do not repeat any more + if ( !AddUserID(NMEA0183AISMsg, UserID) ) return false; // 8 - 37 | 30 MMSI + if ( !NMEA0183AISMsg.AddIntToPayloadBin((uint32_t)AISversion, 2) ) + return false; // 38 - 39 | 2 AIS Version -> 0 oder 1 NOT DERIVED FROM N2k, Always 1!!!! + if ( !AddIMONumber(NMEA0183AISMsg, IMONumber) ) return false; // 40 - 69 | 30 IMO Number unisgned + if ( !AddText(NMEA0183AISMsg, Callsign, 42) ) return false; // 70 - 111 | 42 Call Sign WDE4178 -> 7 6-bit characters -> Ascii lt. Table) + if ( !AddText(NMEA0183AISMsg, Name, 120) ) return false; // 112-231 | 120 Vessel Name POINT FERMIN -> 20 6-bit characters -> Ascii lt. Table + if ( !NMEA0183AISMsg.AddIntToPayloadBin(VesselType, 8) ) return false; // 232-239 | 8 Ship Type 0....255 e.g. 31 Towing + if ( !AddDimensions(NMEA0183AISMsg, Length, Beam, PosRefStbd, PosRefBow) ) return false; // 240 - 269 | 30 Dimensions + if ( !AddEPFDFixType(NMEA0183AISMsg, GNSStype) ) return false; // 270-273 | 4 Position Fix Type, 0 (default) + if ( !AddETADateTime(NMEA0183AISMsg, ETAdate, ETAtime) ) return false; // 274 -293 | 20 Estimated time of arrival; MMDDHHMM UTC + if ( !AddStaticDraught(NMEA0183AISMsg, Draught) ) return false; // 294-301 | 8 Maximum Present Static Draught + if ( !AddText(NMEA0183AISMsg, Destination, 120) ) return false; // 302-421 | 120 | 20 Destination 20 6-bit characters + if ( !NMEA0183AISMsg.AddIntToPayloadBin(DTE, 1) ) return false; // 422 | 1 | Data terminal equipment (DTE) ready (0 = available, 1 = not available = default) + if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 1) ) return false; // 423 | 1 | spare + + return true; +} + +// **************************************************************************** +// AIS position report (class B 129039) -> Type 18: Standard Class B CS Position Report +// PGN129039 +// ParseN2kAISClassBPosition(const tN2kMsg &N2kMsg, uint8_t &MessageID, tN2kAISRepeat &Repeat, uint32_t &UserID, +// double &Latitude, double &Longitude, bool &Accuracy, bool &RAIM, +// uint8_t &Seconds, double &COG, double &SOG, tN2kAISTransceiverInformation &AISTransceiverInformation, +// double &Heading, tN2kAISUnit &Unit, bool &Display, bool &DSC, bool &Band, bool &Msg22, tN2kAISMode &Mode, +// bool &State) +// VDM, VDO (AIS VHF Data-link message 18) +bool SetAISClassBMessage18(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, uint32_t UserID, + double Latitude, double Longitude, bool Accuracy, bool RAIM, + uint8_t Seconds, double COG, double SOG, double Heading, tN2kAISUnit Unit, + bool Display, bool DSC, bool Band, bool Msg22, bool Mode, bool State) { + // + NMEA0183AISMsg.ClearAIS(); + if ( !AddMessageType(NMEA0183AISMsg, MessageID) ) return false; // 0 - 5 | 6 Message Type -> Constant: 18 + if ( !AddRepeat(NMEA0183AISMsg, Repeat) ) return false; // 6 - 7 | 2 Repeat Indicator: 0 = default; 3 = do not repeat any more + if ( !AddUserID(NMEA0183AISMsg, UserID) ) return false; // 8 - 37 | 30 MMSI + if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 8) ) return false; // 38-45 | 8 Regional Reserved + if ( !AddSOG(NMEA0183AISMsg, SOG) ) return false; // 46-55 | 10 [m/s -> kts] SOG with one digit x10, 1023 = N/A + if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Accuracy)) return false; // 56 | 1 GPS Accuracy 1 oder 0, Default 0 + if ( !AddLongitude(NMEA0183AISMsg, Longitude) ) return false; // 57-84 | 28 Longitude in Minutes / 10000 + if ( !AddLatitude(NMEA0183AISMsg, Latitude) ) return false; // 85-111 | 27 Latitude in Minutes / 10000 + if ( !AddCOG(NMEA0183AISMsg, COG) ) return false; // 112-123 | 12 Course over ground will be 3600 (0xE10) if that data is not available. + if ( !AddHeading (NMEA0183AISMsg, Heading) ) return false; // 124-132 | 9 True Heading (HDG) + if ( !AddSeconds(NMEA0183AISMsg, Seconds) ) return false; // 133-138 | 6 Seconds in UTC timestamp) + if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 2) ) return false; // 139-140 | 2 Regional Reserved + if ( !NMEA0183AISMsg.AddIntToPayloadBin(Unit, 1) ) return false; // 141 | 1 0=Class B SOTDMA unit 1=Class B CS (Carrier Sense) unit + if ( !NMEA0183AISMsg.AddIntToPayloadBin(Display, 1) ) return false; // 142 | 1 0=No visual display, 1=Has display, (Probably not reliable). + if ( !NMEA0183AISMsg.AddBoolToPayloadBin(DSC) ) return false; // 143 | 1 If 1, unit is attached to a VHF voice radio with DSC capability. + if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Band) ) return false; // 144 | 1 If this flag is 1, the unit can use any part of the marine channel. + if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Msg22)) return false; // 145 | 1 If 1, unit can accept a channel assignment via Message Type 22. + if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Mode) ) return false; // 146 | 1 Assigned-mode flag: 0 = autonomous mode (default), 1 = assigned mode + if ( !NMEA0183AISMsg.AddBoolToPayloadBin(RAIM) ) return false; // 147 | 1 as for Message Type 1,2,3 + if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 20) ) return false; // 148-167 | 20 Radio Status not in PGN 129039 + if ( !NMEA0183AISMsg.InitAis()) return false; + int padBits=0; + if ( !NMEA0183AISMsg.AddStrField( NMEA0183AISMsg.GetPayloadFix(padBits) ) ) return false; + if ( !NMEA0183AISMsg.AddUInt32Field(padBits) ) return false; + + return true; +} + +// **************************************************************************** +// Type 24: Static Data Report +// Equivalent of a Type 5 message for ships using Class B equipment. Also used to associate an MMSI +// with a name on either class A or class B equipment. +// +// A "Type 24" may be in part A or part B format; According to the standard, parts A and B are expected +// to be broadcast in adjacent pairs; in the real world they may (due to quirks in various aggregation methods) +// be separated by other sentences or even interleaved with different Type 24 pairs; decoders must cope with this. +// The interpretation of some fields in Type B format changes depending on the range of the Type B MMSI field. +// +// 160 bits for part A, 168 bits for part B. +// According to the standard, both the A and B parts are supposed to be 168 bits. +// However, in the wild, A parts are often transmitted with only 160 bits, omitting the spare 7 bits at the end. +// Implementers should be permissive about this. +// +// If the Part Number field is 0, the rest of the message is interpreted as a Part A; +// If it is 1, the rest of the message is interpreted as a Part B; values 2 and 3 are not allowed. +// +// PGN 129809 AIS Class B "CS" Static Data Report, Part A -> AIS VHF Data-link message 24 +// PGN 129810 AIS Class B "CS" Static Data Report, Part B -> AIS VHF Data-link message 24 +// ParseN2kPGN129809 (const tN2kMsg &N2kMsg, uint8_t &MessageID, tN2kAISRepeat &Repeat, uint32_t &UserID, char *Name) -> store to vector +// ParseN2kPGN129810(const tN2kMsg &N2kMsg, uint8_t &MessageID, tN2kAISRepeat &Repeat, uint32_t &UserID, +// uint8_t &VesselType, char *Vendor, char *Callsign, double &Length, double &Beam, +// double &PosRefStbd, double &PosRefBow, uint32_t &MothershipID); +// +// Part A: MessageID, Repeat, UserID, ShipName -> store in vector to call on Part B arrivals!!! +// Part B: MessageID, Repeat, UserID, VesselType (5), Callsign (5), Length & Beam, PosRefBow,.. (5) +bool SetAISClassBMessage24PartA(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, uint32_t UserID, char *Name) { + // AIS Type 24 Message + NMEA0183AISMsg.ClearAIS(); + // Common for PART A AND Part B Bit 0 - 39 / len 40 + if ( !AddMessageType(NMEA0183AISMsg, 24) ) return false; // 0 - 5 | 6 Message Type -> Constant: 24 + if ( !AddRepeat(NMEA0183AISMsg, Repeat) ) return false; // 6 - 7 | 2 Repeat Indicator: 0 = default; 3 = do not repeat any more + if ( !AddUserID(NMEA0183AISMsg, UserID) ) return false; // 8 - 37 | 30 MMSI + if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 2) ) return false; // 38-39 | 2 Part Number 0-1 -> + // Part A: 40 + 128 = len 168 + if ( !AddText(NMEA0183AISMsg, Name, 120) ) return false; // 40-159 | 120 Vessel Name 20 6-bit characters -> Ascii Table + if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 8) ) return false; // 160-167 | 8 Spare + if ( !NMEA0183AISMsg.InitAis() ) return false; + int padBits=0; + if ( !NMEA0183AISMsg.AddStrField( NMEA0183AISMsg.GetPayloadFix(padBits) ) ) return false; + if ( !NMEA0183AISMsg.AddUInt32Field(padBits) ) return false; + return true; +} + +// *************************************************************************************************************** +bool SetAISClassBMessage24PartB(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, + uint32_t UserID, uint8_t VesselType, char *VendorID, char *Callsign, + double Length, double Beam, double PosRefStbd, double PosRefBow, uint32_t MothershipID ) { + + + // AIS Type 24 Message + NMEA0183AISMsg.ClearAIS(); + // Common for PART A AND Part B Bit 0 - 39 / len 40 + if ( !AddMessageType(NMEA0183AISMsg, 24) ) return false; // 0 - 5 | 6 Message Type -> Constant: 24 + if ( !AddRepeat(NMEA0183AISMsg, Repeat) ) return false; // 6 - 7 | 2 Repeat Indicator: 0 = default; 3 = do not repeat any more + if ( !AddUserID(NMEA0183AISMsg, UserID) ) return false; // 8 - 37 | 30 MMSI + if ( !NMEA0183AISMsg.AddIntToPayloadBin(1, 2) ) return false; // 38-39 | 2 Part Number 0-1 -> + + // https://www.navcen.uscg.gov/?pageName=AISMessagesB + // PART B: 40 + 128 = len 168 + if ( !NMEA0183AISMsg.AddIntToPayloadBin(VesselType, 8) ) return false; // 168-175 | 40-47 | 8 Ship Type 0....99 + if ( !AddText(NMEA0183AISMsg, VendorID, 42) ) return false; // 176-217 | 48-89 | 42 Vendor ID + Unit Model Code + Serial Number + if ( !AddText(NMEA0183AISMsg, Callsign, 42) ) return false; // 218-259 | 90-131 | 42 Call Sign WDE4178 -> 7 6-bit characters, as in Msg Type 5 + if ( !AddDimensions(NMEA0183AISMsg, Length, Beam, PosRefStbd, PosRefBow) ) return false; // 260-289 | 132-161 | 30 Dimensions + if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 6) ) return false; // 290-295 | 162-167 | 6 Spare + if ( !NMEA0183AISMsg.InitAis() ) return false; + int padBits=0; + if ( !NMEA0183AISMsg.AddStrField( NMEA0183AISMsg.GetPayloadFix(padBits) ) ) return false; + if ( !NMEA0183AISMsg.AddUInt32Field(padBits) ) return false; + return true; +} + +// **************************************************************************** +// AIS ATON report (129041) -> Type 21: Position and status report for aids-to-navigation +// PGN129041 + +bool SetAISMessage21(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t Repeat, uint32_t UserID, + double Latitude, double Longitude, bool Accuracy, bool RAIM, + uint8_t Seconds, double Length, double Beam, double PositionReferenceStarboard, + double PositionReferenceTrueNord, tN2kAISAtoNType Type, bool OffPositionIndicator, + bool VirtualAtoNFlag, bool AssignedModeFlag, tN2kGNSStype GNSSType, uint8_t AtoNStatus, + char * atonName ) { + // + NMEA0183AISMsg.ClearAIS(); + if ( !AddMessageType(NMEA0183AISMsg, 21) ) return false; // 0 - 5 | 6 Message Type -> Constant: 18 + if ( !AddRepeat(NMEA0183AISMsg, Repeat) ) return false; // 6 - 7 | 2 Repeat Indicator: 0 = default; 3 = do not repeat any more + if ( !AddUserID(NMEA0183AISMsg, UserID) ) return false; // 8 - 37 | 30 MMSI + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(Type,5)) return false; // | 5 aid type + //the name must be split: + //if it's > 120 bits the rest goes to the last parameter + if ( !NMEA0183AISMsg.AddEncodedCharToPayloadBin(atonName,120)) + return false; // | 120 name + if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Accuracy) ) return false; // | 1 accuracy + if ( !AddLongitude(NMEA0183AISMsg,Longitude)) return false; // | 28 lon + if ( !AddLatitude(NMEA0183AISMsg,Latitude)) return false; // | 27 lat + if ( !AddDimensions(NMEA0183AISMsg, Length, Beam, + PositionReferenceStarboard, PositionReferenceTrueNord)) return false; // | 30 dim + if ( !AddEPFDFixType(NMEA0183AISMsg,GNSSType)) return false; // | 4 fix type + if ( !AddSeconds(NMEA0183AISMsg,Seconds)) return false; // | 6 second + if ( !NMEA0183AISMsg.AddBoolToPayloadBin(OffPositionIndicator)) + return false; // | 1 off + if ( !NMEA0183AISMsg.AddIntToPayloadBin(0,8)) return false; // | 8 reserverd + if ( !NMEA0183AISMsg.AddBoolToPayloadBin(RAIM)) return false; // | 1 raim + if ( !NMEA0183AISMsg.AddBoolToPayloadBin(VirtualAtoNFlag)) + return false; // | 1 virt + if ( !NMEA0183AISMsg.AddBoolToPayloadBin(AssignedModeFlag)) + return false; // | 1 assigned + if ( !NMEA0183AISMsg.AddIntToPayloadBin(0,1)) return false; // | 1 spare + size_t l=strlen(atonName); + if (l >=20){ + uint8_t bitlen=(l-20)*6; + if (bitlen > 88) bitlen=88; + if ( !NMEA0183AISMsg.AddEncodedCharToPayloadBin(atonName+20,bitlen)) return false; // | name + } + if ( !NMEA0183AISMsg.InitAis() ) return false; + int padBits=0; + if ( !NMEA0183AISMsg.AddStrField( NMEA0183AISMsg.GetPayload(padBits) ) ) return false; + if ( !NMEA0183AISMsg.AddUInt32Field(padBits) ) return false; + + return true; +} + +//****************************************************************************** +// Validations and Unit Transformations +//****************************************************************************** + +// ***************************************************************************** +// 6bit Message Type -> Constant: 1 or 3, 5, 24 etc. +bool AddMessageType(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageType) { + + if (MessageType < 0 || MessageType > 24 ) MessageType = 1; + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(MessageType, 6) ) return false; + return true; +} + +// ***************************************************************************** +// 2bit Repeat Indicator: 0 = default; 3 = do not repeat any more +bool AddRepeat(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t Repeat) { + + if (Repeat < 0 || Repeat > 3) Repeat = 0; + if ( !NMEA0183AISMsg.AddIntToPayloadBin(Repeat, 2) ) return false; + return true; +} + +// ***************************************************************************** +// 30bit UserID = MMSI (9 decimal digits) +bool AddUserID(tNMEA0183AISMsg &NMEA0183AISMsg, uint32_t UserID) { + + if (UserID < 0||UserID > 999999999) UserID = 0; + if ( !NMEA0183AISMsg.AddIntToPayloadBin(UserID, 30) ) return false; + return true; +} + +// ***************************************************************************** +// 30 bit IMO Number +// 0 = not available = default – Not applicable to SAR aircraft +// 0000000001-0000999999 not used +// 0001000000-0009999999 = valid IMO number; +// 0010000000-1073741823 = official flag state number. +bool AddIMONumber(tNMEA0183AISMsg &NMEA0183AISMsg, uint32_t &IMONumber) { + uint32_t iTemp; + ( (IMONumber >= 999999 && IMONumber <= 9999999)||(IMONumber >= 10000000 && IMONumber <= 1073741823) )? iTemp = IMONumber : iTemp = 0; + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 30) ) return false; + return true; +} + +// ***************************************************************************** +// 42bit Callsign alphanumeric value, max 7 six-bit characters +// 120bit Name or Destination +bool AddText(tNMEA0183AISMsg &NMEA0183AISMsg, char *FieldVal, uint8_t length) { + uint8_t len = length/6; + if ( strlen(FieldVal) > len ) FieldVal[len] = 0; + if ( !NMEA0183AISMsg.AddEncodedCharToPayloadBin(FieldVal, length) ) return false; + return true; +} + +// ***************************************************************************** +// Calculate Dimension A, B, C, D +// double PosRefBow 240-248 | 9 [m] Dimension to Bow, reference for pos. A +// Length - PosRefBow 249-257 | 9 [m] Dimension to Stern, reference for pos. B +// Beam - PosRefStbd 258-263 | 6 [m] Dimension to Port, reference for pos. C +// PosRefStbd 264-269 | 6 [m] Dimension to Starboard, reference for pos. D +// Ship dimensions will be 0 if not available. For the dimensions to bow and stern, +// the special value 511 indicates 511 meters or greater; +// for the dimensions to port and starboard, the special value 63 indicates 63 meters or greater. +// 30 Bit +bool AddDimensions(tNMEA0183AISMsg &NMEA0183AISMsg, double Length, double Beam, double PosRefStbd, double PosRefBow) { + uint16_t _PosRefBow = 0; + uint16_t _PosRefStern = 0; + uint16_t _PosRefStbd = 0; + uint16_t _PosRefPort = 0; + + if ( PosRefBow >= 0.0 && PosRefBow <= 511.0 ) { + _PosRefBow = ceil(PosRefBow); + } else { + _PosRefBow = 511; + } + + if ( PosRefStbd >= 0.0 && PosRefStbd <= 63.0 ) { + _PosRefStbd = ceil(PosRefStbd); + } else { + _PosRefStbd = 63; + } + + if ( !N2kIsNA(Length) ) { + _PosRefStern = ceil( Length ) - _PosRefBow; + if ( _PosRefStern < 0 ) _PosRefStern = 0; + if ( _PosRefStern > 511 ) _PosRefStern = 511; + } + if ( !N2kIsNA(Beam) ) { + _PosRefPort = ceil( Beam ) - _PosRefStbd; + if ( _PosRefPort < 0 ) _PosRefPort = 0; + if ( _PosRefPort > 63 ) _PosRefPort = 63; + } + + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(_PosRefBow, 9) ) return false; + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(_PosRefStern, 9) ) return false; + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(_PosRefPort, 6) ) return false; + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(_PosRefStbd, 6) ) return false; + return true; +} + +// ***************************************************************************** +// 4 Bit Navigational Status e.g.: "Under way sailing" +// Same values used as in tN2kAISNavStatus, so we can use direct numbers +bool AddNavStatus(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t &NavStatus) { + uint8_t iTemp; + (NavStatus >= 0 && NavStatus <= 15 )? iTemp = NavStatus : iTemp = 15; + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 4) ) return false; + return true; +} + +// ***************************************************************************** +// 8bit [rad/s -> degree/minute] Rate of Turn ROT 128 = N/A +// 0 = not turning +// 1…126 = turning right at up to 708 degrees per minute or higher +// 1…-126 = turning left at up to 708 degrees per minute or higher +// 127 = turning right at more than 5deg/30s (No TI available) +// -127 = turning left at more than 5deg/30s (No TI available) +// 128 (80 hex) indicates no turn information available (default) +bool AddROT(tNMEA0183AISMsg &NMEA0183AISMsg, double &rot) { + int8_t iTemp; + if ( N2kIsNA(rot)) iTemp = 128; + else { + rot *= radsToDegMin; + (rot > -128.0 && rot < 128.0)? iTemp = aRoundToInt(rot) : iTemp = 128; + } + + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 8) ) return false; + return true; +} + +// ***************************************************************************** +// 10 bit [m/s -> kts] SOG x10, 1023 = N/A +// Speed over ground is in 0.1-knot resolution from 0 to 102 knots. +// Value 1023 indicates speed is not available, value 1022 indicates 102.2 knots or higher. +bool AddSOG (tNMEA0183AISMsg &NMEA0183AISMsg, double &sog) { + int16_t iTemp; + if ( sog < 0.0 ) iTemp = 1023; + else { + sog *= msTokn; + if (sog > 102.2) iTemp = 1023; + else iTemp = aRoundToInt( 10 * sog ); + } + + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 10) ) return false; + return true; +} + +// ***************************************************************************** +// 28 bit @TODO check negative values +// Values up to plus or minus 180 degrees, East = positive, West = negative. +// A value of 181 degrees (0x6791AC0 hex) indicates that longitude is not available and is the default. +// AIS Longitude is given in in 1/10000 min; divide by 600000.0 to obtain degrees. +bool AddLongitude(tNMEA0183AISMsg &NMEA0183AISMsg, double &Longitude) { + int32_t iTemp; + (Longitude >= -180.0 && Longitude <= 180.0)? iTemp = (int) (Longitude * 600000) : iTemp = 181 * 600000; + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 28) ) return false; + return true; +} + +// ***************************************************************************** +// 27 bit +// Values up to plus or minus 90 degrees, North = positive, South = negative. +// A value of 91 degrees (0x3412140 hex) indicates latitude is not available and is the default. +bool AddLatitude(tNMEA0183AISMsg &NMEA0183AISMsg, double &Latitude) { + int32_t iTemp; + (Latitude >= -90.0 && Latitude <= 90.0)? iTemp = (int) (Latitude * 600000) : iTemp = 91 * 600000; + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 27) ) return false; + return true; +} + +// **************************************************************************** +// 9 bit True Heading (HDG) 0 to 359 degrees, 511 = not available. +bool AddHeading (tNMEA0183AISMsg &NMEA0183AISMsg, double &heading) { + uint16_t iTemp; + if ( N2kIsNA(heading) ) iTemp = 511; + else { + heading *= radToDeg; + (heading >= 0.0 && heading <= 359.0 )? iTemp = aRoundToInt( heading ) : iTemp = 511; + } + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 9) ) return false; + return true; +} + +// ***************************************************************************** +// 12bit Relative to true north, to 0.1 degree precision +bool AddCOG(tNMEA0183AISMsg &NMEA0183AISMsg, double cog) { + int16_t iTemp; + cog *= radToDeg; + if ( cog >= 0.0 && cog < 360.0 ) { iTemp = aRoundToInt( cog * 10 ); } else { iTemp = 3600; } + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 12) ) return false; + return true; +} + +// ***************************************************************************** +// 6bit Seconds in UTC timestamp should be 0-59, except for these special values: +// 60 if time stamp is not available (default) +// 61 if positioning system is in manual input mode +// 62 if Electronic Position Fixing System operates in estimated (dead reckoning) mode, +// 63 if the positioning system is inoperative. +bool AddSeconds (tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t &Seconds) { + uint8_t iTemp; + (Seconds >= 0 && Seconds <= 63 )? iTemp = Seconds : iTemp = 60; + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 6) ) return false; + return true; +} + +// ***************************************************************************** +// 4 bit Position Fix Type, See "EPFD Fix Types" 0 (default) +bool AddEPFDFixType(tNMEA0183AISMsg &NMEA0183AISMsg, tN2kGNSStype &GNSStype) { + // Translate tN2kGNSStype to AIS conventions + // 3 & 4 not defined in AIS -> we take 1 for GPS + uint8_t fixType = 0; + switch (GNSStype) { + case 0: // GPS + case 3: // GPS+SBAS/WAAS + case 4: // GPS+SBAS/WAAS+GLONASS + fixType = 1; break; + case 1: // GLONASS + fixType = 2; break; + case 2: // GPS+GLONASS + fixType = 3; break; + case 5: // Chayka + fixType = 5; break; + case 6: // integrated + fixType = 6; break; + case 7: // surveyed + fixType = 7; break; + case 8: // Galileo + fixType = 8; break; + default: + fixType = 0; + } + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(fixType, 4) ) return false; + return true; +} + +// ***************************************************************************** +// 8 bit Maxiumum present static draught +// In 1/10 m, 255 = draught 25.5 m or greater, 0 = not available = default; in accordance with IMO Resolution A.851 +bool AddStaticDraught(tNMEA0183AISMsg &NMEA0183AISMsg, double &Draught) { + uint8_t staticDraught; + if ( N2kIsNA(Draught) ) staticDraught = 0; + else if (Draught < 0.0) staticDraught = 0; + else if (Draught>25.5) staticDraught = 255; + else staticDraught = (int) ceil( 10.0 * Draught); + + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(staticDraught, 8) ) return false; + return true; +} + +// ***************************************************************************** +// 20bit Estimated time of arrival; MMDDHHMM UTC +// 4 Bits 19-16: month; 1-12; 0 = not available = default +// 5 Bits 15-11: day; 1-31; 0 = not available = default +// 5 Bits 10-6: hour; 0-23; 24 = not available = default +// 6 Bits 5-0: minute; 0-59; 60 = not available = default +// N2k Field #7: ETA Time - Seconds since midnight Bits: 32 Units: s +// Type: Time Resolution: 0.0001 Signed: false e.g. 36000.00 +// N2k Field #8: ETA Date - Days since January 1, 1970 Bits: 16 +// Units: days Type: Date Resolution: 1 Signed: false e.g. 18184 +bool AddETADateTime(tNMEA0183AISMsg &NMEA0183AISMsg, uint16_t &ETAdate, double &ETAtime) { + + uint8_t month = 0; + uint8_t day = 0; + uint8_t hour = 24; + uint8_t minute = 60; + + if (!N2kIsNA(ETAdate) && ETAdate > 0 ) { + tmElements_t tm; + #ifndef _Time_h + time_t t=NMEA0183AISMsg.daysToTime_t(ETAdate); + #else + time_t t=ETAdate*86400; + #endif + NMEA0183AISMsg.breakTime(t, tm); + month = (uint8_t) NMEA0183AISMsg.GetMonth(tm); + day = (uint8_t) NMEA0183AISMsg.GetDay(tm); + } + if ( !N2kIsNA(ETAtime) && ETAtime >= 0 ) { + double temp = ETAtime / 3600; + hour = (int) temp; + minute = (int) ((temp - hour) * 60); + } else { + hour = 24; + minute = 60; + } + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(month, 4) ) return false; + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(day, 5) ) return false; + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(hour, 5) ) return false; + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(minute, 6) ) return false; + return true; +} + + diff --git a/lib/nmea2ktoais/NMEA0183AISMessages.h b/lib/nmea2ktoais/NMEA0183AISMessages.h new file mode 100644 index 0000000..d818a2b --- /dev/null +++ b/lib/nmea2ktoais/NMEA0183AISMessages.h @@ -0,0 +1,87 @@ +/* +NMEA0183AISMessages.h + +Copyright (c) 2019 Ronnie Zeiller, www.zeiller.eu + +Based on the works of Timo Lappalainen and Eric S. Raymond and Kurt Schwehr https://gpsd.gitlab.io/gpsd/AIVDM.html + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + +#ifndef _tNMEA0183AISMessages_H_ +#define _tNMEA0183AISMessages_H_ + + +#include +#include +#include +#include +#include "NMEA0183AISMsg.h" +#include +#include +#include + + +// Types 1, 2 and 3: Position Report Class A or B +bool SetAISClassABMessage1(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageType, uint8_t Repeat, + uint32_t UserID, double Latitude, double Longitude, bool Accuracy, bool RAIM, uint8_t Seconds, + double COG, double SOG, double Heading, double ROT, uint8_t NavStatus); + +//***************************************************************************** +// AIS Class A Static and Voyage Related Data Message Type 5 +bool SetAISClassAMessage5(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, + uint32_t UserID, uint32_t IMONumber, char *Callsign, char *Name, + uint8_t VesselType, double Length, double Beam, double PosRefStbd, + double PosRefBow, uint16_t ETAdate, double ETAtime, double Draught, + char *Destination, tN2kGNSStype GNSStype, uint8_t DTE, + tN2kAISVersion AISversion); + +//***************************************************************************** +// AIS position report (class B 129039) -> Standard Class B CS Position Report Message Type 18 Part B +bool SetAISClassBMessage18(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, uint32_t UserID, + double Latitude, double Longitude, bool Accuracy, bool RAIM, + uint8_t Seconds, double COG, double SOG, double Heading, tN2kAISUnit Unit, + bool Display, bool DSC, bool Band, bool Msg22, bool Mode, bool State); + +//***************************************************************************** +// Static Data Report Class B, Message Type 24 +// PGN 129809 Handle AIS Class B "CS" Static Data Report, Part A +bool SetAISClassBMessage24PartA(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, uint32_t UserID, char *Name); + +//***************************************************************************** +// Static Data Report Class B, Message Type 24 +bool SetAISClassBMessage24PartB(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, + uint32_t UserID, uint8_t VesselType, char *VendorID, char *Callsign, + double Length, double Beam, double PosRefStbd, double PosRefBow, uint32_t MothershipID ); + +//***************************************************************************** +// Aton class 21 +bool SetAISMessage21(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t Repeat, uint32_t UserID, + double Latitude, double Longitude, bool Accuracy, bool RAIM, + uint8_t Seconds, double Length, double Beam, double PositionReferenceStarboard, + double PositionReferenceTrueNord, tN2kAISAtoNType Type, bool OffPositionIndicator, + bool VirtualAtoNFlag, bool AssignedModeFlag, tN2kGNSStype GNSSType, uint8_t AtoNStatus, + char * atonName ); + +inline int32_t aRoundToInt(double x) { + return x >= 0 + ? (int32_t) floor(x + 0.5) + : (int32_t) ceil(x - 0.5); +} +#endif diff --git a/lib/nmea2ktoais/NMEA0183AISMsg.cpp b/lib/nmea2ktoais/NMEA0183AISMsg.cpp new file mode 100644 index 0000000..04118df --- /dev/null +++ b/lib/nmea2ktoais/NMEA0183AISMsg.cpp @@ -0,0 +1,201 @@ +/* +NMEA0183AISMsg.cpp + +Copyright (c) 2019 Ronnie Zeiller, www.zeiller.eu +Based on the works of Timo Lappalainen NMEA2000 and NMEA0183 Library + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + +#include "NMEA0183AISMsg.h" +#include +//#include +#include +#include +#include +#include +#include + +const char AsciiChar[] = "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_ !\"#$%&\'()*+,-./0123456789:;<=>?"; +const char *tNMEA0183AISMsg::EmptyAISField = "000000"; + +//***************************************************************************** +tNMEA0183AISMsg::tNMEA0183AISMsg() { + ClearAIS(); +} + +//***************************************************************************** +void tNMEA0183AISMsg::ClearAIS() { + + Payload[0]=0; + PayloadBin.reset(); + iAddPldBin=0; + iAddPld=0; +} + + +//***************************************************************************** +bool tNMEA0183AISMsg::AddIntToPayloadBin(int32_t ival, uint16_t countBits) { + + if ( (iAddPldBin + countBits ) >= AIS_BIN_MAX_LEN ) return false; // Is there room for any data + + bset = ival; + + uint16_t iAdd=iAddPldBin; + + for(int i = countBits-1; i >= 0 ; i--) { + PayloadBin[iAdd]=bset [i]; + iAdd++; + } + + iAddPldBin += countBits; + + return true; +} + +//**************************************************************************** +bool tNMEA0183AISMsg::AddBoolToPayloadBin(bool &bval) { + if ( (iAddPldBin + 1 ) >= AIS_BIN_MAX_LEN ) return false; + PayloadBin[iAddPldBin]=bval; + iAddPldBin++; + return true; +} + +// ***************************************************************************** +// converts sval into binary 6-bit AScii encoded string and appends it to PayloadBin +// filled up with "@" == "000000" to given bit-size +bool tNMEA0183AISMsg::AddEncodedCharToPayloadBin(char *sval, size_t countBits) { + + if ( (iAddPldBin + countBits ) >= AIS_BIN_MAX_LEN ) return false; // Is there room for any data + + const char * ptr; + size_t len = strlen(sval); // e.g.: should be 7 for Callsign + if ( len * 6 > countBits ) len = countBits / 6; + + for (size_t i = 0; i= 0){ + AddIntToPayloadBin(index, 6); + } + } else { + AddIntToPayloadBin(0, 6); + } + } + // fill up with "@", also covers empty sval + if ( len * 6 < countBits ) { + for (size_t i=0;i<(countBits/6-len);i++) { + AddIntToPayloadBin(0, 6); + } + } + return true; +} + +//***************************************************************************** +template +int tNMEA0183AISMsg::ConvertBinaryAISPayloadBinToAscii(std::bitset &src,uint16_t maxSize,uint16_t bitSize,uint16_t stoffset) { + Payload[0]='\0'; + uint16_t slen=maxSize; + if (stoffset >= slen) return 0; + slen-=stoffset; + uint16_t bitLen=bitSize > 0?bitSize:slen; + uint16_t len= bitLen / 6; + if ((len * 6) < bitLen) len+=1; + uint16_t padBits=0; + uint32_t offset; + std::bitset<6> s; + uint8_t dec; + int i; + for ( i=0; i= 0){ + if ( !AddUInt32Field(sequence) ) return false; + } + else{ + if ( !AddEmptyField() ) return false; + } + if ( !AddStrField(channel) ) return false; + return true; +} +bool tNMEA0183AISMsg::BuildMsg5Part1() { + if ( iAddPldBin != 424 ) return false; + InitAis(2,1,5); + int padBits=0; + AddStrField( GetPayload(padBits,0,336)); + AddUInt32Field(padBits); + return true; +} + +bool tNMEA0183AISMsg::BuildMsg5Part2() { + if ( iAddPldBin != 424 ) return false; + InitAis(2,2,5); + int padBits=0; + AddStrField( GetPayload(padBits,336,88) ); + AddUInt32Field(padBits); + return true; +} + + +//******************************* AIS PAYLOADS ********************************* +// get converted Payload for Message 1, 2, 3 & 18, always Length 168 +const char *tNMEA0183AISMsg::GetPayloadFix(int &padBits,uint16_t fixLen){ + uint16_t lenbin = iAddPldBin; + if ( lenbin != fixLen ) return nullptr; + return GetPayload(padBits,0,0); +} +const char *tNMEA0183AISMsg::GetPayload(int &padBits,uint16_t offset,uint16_t bitLen) { + padBits=ConvertBinaryAISPayloadBinToAscii(PayloadBin,iAddPldBin, bitLen,offset ); + return Payload; +} + diff --git a/lib/nmea2ktoais/NMEA0183AISMsg.h b/lib/nmea2ktoais/NMEA0183AISMsg.h new file mode 100644 index 0000000..ae9f656 --- /dev/null +++ b/lib/nmea2ktoais/NMEA0183AISMsg.h @@ -0,0 +1,97 @@ +/* +NMEA0183AISMsg.h + +Copyright (c) 2019 Ronnie Zeiller, www.zeiller.eu +Based on the works of Timo Lappalainen NMEA2000 and NMEA0183 Library + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + +#ifndef _tNMEA0183AISMsg_H_ +#define _tNMEA0183AISMsg_H_ + +#include +#include +#include +#include +#include +#include +#include + + +#ifndef AIS_MSG_MAX_LEN +#define AIS_MSG_MAX_LEN 100 // maximum length of AIS Payload +#endif + +#ifndef AIS_BIN_MAX_LEN +#define AIS_BIN_MAX_LEN 500 // maximum length of AIS Binary Payload (before encoding to Ascii) +#endif + +#define BITSET_LENGTH 120 + +class tNMEA0183AISMsg : public tNMEA0183Msg { + + protected: // AIS-NMEA + std::bitset bset; + static const char *EmptyAISField; // 6bits 0 not used yet..... + static const char *AsciChar; + + uint16_t iAddPldBin; + char Payload[AIS_MSG_MAX_LEN]; + uint8_t iAddPld; + char talker[4]="VDM"; + char channel[2]="A"; + std::bitset PayloadBin; + public: + // Clear message + void ClearAIS(); + + public: + tNMEA0183AISMsg(); + const char *GetPayloadFix(int &padBits,uint16_t fixLen=168); + const char *GetPayload(int &padBits,uint16_t offset=0,uint16_t bitLen=0); + + bool BuildMsg5Part1(); + bool BuildMsg5Part2(); + bool InitAis(int max=1,int number=1,int sequence=-1); + + // Generally Used + bool AddIntToPayloadBin(int32_t ival, uint16_t countBits); + bool AddBoolToPayloadBin(bool &bval); + bool AddEncodedCharToPayloadBin(char *sval, size_t Length); + /** + * @param channelA - if set A, otherwise B + * @param own - if set VDO, else VDM + */ + void SetChannelAndTalker(bool channelA,bool own=false); + /** + * convert the payload to ascii + * return the number of padding bits + * @param bitSize the number of bits to be used, 0 - use all bits + */ + template + int ConvertBinaryAISPayloadBinToAscii(std::bitset &src,uint16_t maxSize, uint16_t bitSize,uint16_t offset=0); + + // AIS Helper functions + protected: + inline int32_t aRoundToInt(double x) { + return (x >= 0) ? (int32_t) floor(x + 0.5) : (int32_t) ceil(x - 0.5); + } +}; +#endif diff --git a/lib/nmea2ktoais/README.md b/lib/nmea2ktoais/README.md new file mode 100644 index 0000000..1d55424 --- /dev/null +++ b/lib/nmea2ktoais/README.md @@ -0,0 +1,73 @@ +# NMEA2000 to NMEA0183 AIS Converter + + +NMEA0183 AIS library © Ronnie Zeiller, www.zeiller.eu + +Addendum for NMEA2000 and NMEA0183 Library from Timo Lappalainen https://github.com/ttlappalainen + +to get NMEA0183 AIS data from N2k-bus + +## Conversions: + +- NMEA2000 PGN 129038 => AIS CLASS A Position Report (Message Type 1) 1.) 2.) 3.) +- NMEA2000 PGN 129039 => AIS Class B Position Report, Message Type 18 +- NMEA2000 PGN 129794 => AIS Class A Ship Static and Voyage related data, Message Type 5 4.) +- NMEA2000 PGN 129809 => AIS Class B "CS" Static Data Report, making a list of UserID (MMSI) and Ship Names used for Message 24 Part A +- NMEA2000 PGN 129810 => AIS Class B "CS" Static Data Report, Message 24 Part A+B + +### Versions +1.0.6 2024-03-25 +- fixed to work with Timo´s NMEA2000 v4.21.3 + +1.0.5 2023-12-02 +- removed VDO remote print statements + +1.0.4 2023-12-02 +- merged @Isoltero master with fixed memory over run, added VDO remote print statements Thanks to Luis Soltero +- fixed example, thanks to @arduinomnomnom + +1.0.3 2022-05-01 +- Update Examples: AISTransceiverInformation in ParseN2kPGN129039 for changes in NMEA2000 library: https://github.com/ttlappalainen/NMEA2000 + + +1.0.2 2022-04-30 +- bugfix: malloc without free. Thanks to Luis Soltero (Issue https://github.com/ronzeiller/NMEA0183-AIS/issues/3) + +1.0.1 2022-03-15 +- bugfix: buffer overrun missing space for termination. Thanks to Luis Soltero (Issue https://github.com/ronzeiller/NMEA0183-AIS/issues/2) + +2020-12-25 +- corrected Navigational Status 0. Thanks to Li-Ren (Issue https://github.com/ronzeiller/NMEA0183-AIS/issues/1) + +1.0.0 2019-11-24 +- initial upload + +### Remarks +1. Message Type could be set to 1 or 3 (identical messages) on demand +2. Maneuver Indicator (not part of NMEA2000 PGN 129038) => will be set to 0 (default) +3. Radio Status (not part of NMEA2000 PGN 129038) => will be set to 0 +4. AIS Version (not part of NMEA2000 PGN 129794) => will be set to 1 + +## Dependencies + +To use this library you need also: + + - NMEA2000 library + + - NMEA0183 library + + - Related CAN libraries. + +## License + +MIT license + +Copyright (c) 2019-2022 Ronnie Zeiller, www.zeiller.eu + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. From 70ad5cc9032d741403f9232a0c09bc2c5e8fbcf4 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Sun, 28 Sep 2025 19:28:30 +0200 Subject: [PATCH 030/121] omit external nmea2ktoais --- platformio.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/platformio.ini b/platformio.ini index 53bdb61..0025382 100644 --- a/platformio.ini +++ b/platformio.ini @@ -28,7 +28,6 @@ lib_deps = ESPmDNS WiFi Update - nmea2kto183ais=https://github.com/wellenvogel/esp32n2kto183ais.git#20250926 [devdeps] lib_deps= @@ -42,7 +41,6 @@ lib_deps= ESPmDNS WiFi Update - nmea2kto183ais=symlink://../esp32n2kto183ais [env] platform = espressif32 @ 6.8.1 framework = arduino From 6266f85db6ab7c856340fa047dce4d8b35fa90ed Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Sun, 28 Sep 2025 19:58:54 +0200 Subject: [PATCH 031/121] #116: sda and scl are swapped when building with online service --- lib/iictask/GwIicTask.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/iictask/GwIicTask.cpp b/lib/iictask/GwIicTask.cpp index 221a420..53ba9bd 100644 --- a/lib/iictask/GwIicTask.cpp +++ b/lib/iictask/GwIicTask.cpp @@ -147,13 +147,13 @@ bool initWire(GwLog *logger, TwoWire &wire, int num){ #ifdef _GWI_IIC1 return initWireDo(logger,wire,num,_GWI_IIC1); #endif - return initWireDo(logger,wire,num,"",GWIIC_SDA,GWIIC_SCL); + return initWireDo(logger,wire,num,"",GWIIC_SCL,GWIIC_SDA); } if (num == 2){ #ifdef _GWI_IIC2 return initWireDo(logger,wire,num,_GWI_IIC2); #endif - return initWireDo(logger,wire,num,"",GWIIC_SDA2,GWIIC_SCL2); + return initWireDo(logger,wire,num,"",GWIIC_SCL2,GWIIC_SDA2); } return false; } From 8bf8ada30e7c224c13a25cf3a5072e445e0d2037 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Mon, 29 Sep 2025 09:35:14 +0200 Subject: [PATCH 032/121] #111: allow to add extra scripts with custom_script --- extra_script.py | 13 ++++++++++++ lib/exampletask/Readme.md | 38 ++++++++++++++++++++++++++++++++++ lib/exampletask/platformio.ini | 1 + lib/exampletask/script.py | 4 ++++ 4 files changed, 56 insertions(+) create mode 100644 lib/exampletask/script.py diff --git a/extra_script.py b/extra_script.py index 218782f..3f41463 100644 --- a/extra_script.py +++ b/extra_script.py @@ -547,3 +547,16 @@ env.Append( ) #script does not run on clean yet - maybe in the future env.AddPostAction("clean",cleangenerated) +extraScripts=getFileList(getOption(env,'custom_script',toArray=True)) +for script in extraScripts: + if os.path.isfile(script): + print(f"#extra {script}") + with open(script) as fh: + try: + code = compile(fh.read(), script, 'exec') + except SyntaxError as e: + print(f"#ERROR: script {script} does not compile: {e}") + continue + exec(code) + else: + print(f"#ERROR: script {script} not found") diff --git a/lib/exampletask/Readme.md b/lib/exampletask/Readme.md index 7637ecf..7c701fb 100644 --- a/lib/exampletask/Readme.md +++ b/lib/exampletask/Readme.md @@ -57,6 +57,44 @@ Files Starting from Version 20250305 you should normally not use this file name any more as those styles would be added for all build environments. Instead define a parameter _custom_css_ in your [platformio.ini](platformio.ini) for the environments you would like to add some styles for. This parameter accepts a list of file names (relative to the project root, separated by , or as multi line entry) + * [script.py](script.py)
+ Starting from version 202509xx you can define a parameter "custom_script" in your [platformio.ini](platformio.ini). + This parameter can contain a list of file names (relative to the project root) that will be added as a [platformio extra script](https://docs.platformio.org/en/latest/scripting/index.html#scripting). The scripts will be loaded at the end of the main [extra_script](../../extra_script.py). + You can add code there that is specific for your build. + Example: + ``` + # PlatformIO extra script for obp60task + epdtype = "unknown" + pcbvers = "unknown" + for x in env["BUILD_FLAGS"]: + if x.startswith("-D HARDWARE_"): + pcbvers = x.split('_')[1] + if x.startswith("-D DISPLAY_"): + epdtype = x.split('_')[1] + + propfilename = os.path.join(env["PROJECT_LIBDEPS_DIR"], env ["PIOENV"], "GxEPD2/library.properties") + properties = {} + with open(propfilename, 'r') as file: + for line in file: + match = re.match(r'^([^=]+)=(.*)$', line) + if match: + key = match.group(1).strip() + value = match.group(2).strip() + properties[key] = value + + gxepd2vers = "unknown" + try: + if properties["name"] == "GxEPD2": + gxepd2vers = properties["version"] + except: + pass + + env["CPPDEFINES"].extend([("BOARD", env["BOARD"]), ("EPDTYPE", epdtype), ("PCBVERS", pcbvers), ("GXEPD2VERS", gxepd2vers)]) + + print("added hardware info to CPPDEFINES") + print("friendly board name is '{}'".format(env.GetProjectOption ("board_name"))) + ``` + Interfaces ---------- diff --git a/lib/exampletask/platformio.ini b/lib/exampletask/platformio.ini index 348b36c..74363a9 100644 --- a/lib/exampletask/platformio.ini +++ b/lib/exampletask/platformio.ini @@ -14,5 +14,6 @@ custom_config= lib/exampletask/exampleConfig.json custom_js=lib/exampletask/example.js custom_css=lib/exampletask/example.css +custom_script=lib/exampletask/script.py upload_port = /dev/esp32 upload_protocol = esptool \ No newline at end of file diff --git a/lib/exampletask/script.py b/lib/exampletask/script.py new file mode 100644 index 0000000..fb53d6f --- /dev/null +++ b/lib/exampletask/script.py @@ -0,0 +1,4 @@ +Import("env") + +print("exampletask extra script running") +syntax error here \ No newline at end of file From 9831f8da853a85b23bb2718fe0cd049c15d07ae2 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Mon, 29 Sep 2025 12:43:21 +0200 Subject: [PATCH 033/121] add code for SHT4X, ENV4 --- lib/hardware/GwM5Grove.in | 37 +++++-- lib/iictask/GwIicTask.cpp | 2 +- lib/iictask/GwSHT3X.cpp | 138 -------------------------- lib/iictask/GwSHT3X.h | 20 ---- lib/iictask/SHT3X.cpp | 2 +- lib/iictask/SHT4X.cpp | 128 ++++++++++++++++++++++++ lib/iictask/SHT4X.h | 76 ++++++++++++++ lib/iictask/config.json | 197 +++++++++++++++++++++++++++++++++++++ lib/iictask/platformio.ini | 11 +++ lib/sensors/GwSensor.h | 1 + 10 files changed, 443 insertions(+), 169 deletions(-) delete mode 100644 lib/iictask/GwSHT3X.cpp delete mode 100644 lib/iictask/GwSHT3X.h create mode 100644 lib/iictask/SHT4X.cpp create mode 100644 lib/iictask/SHT4X.h diff --git a/lib/hardware/GwM5Grove.in b/lib/hardware/GwM5Grove.in index c98d4d1..a3c8c06 100644 --- a/lib/hardware/GwM5Grove.in +++ b/lib/hardware/GwM5Grove.in @@ -71,15 +71,15 @@ #endif #GROVE -//#ifdef M5_ENV4$GS$ -// #ifndef M5_GROOVEIIC$GS$ -// #define M5_GROOVEIIC$GS$ -// #endif -// GROOVE_IIC(SHT3X,$Z$,1) -// GROOVE_IIC(BMP280,$Z$,1) -// #define _GWSHT3X -// #define _GWBMP280 -//#endif +#ifdef M5_ENV4$GS$ + #ifndef M5_GROOVEIIC$GS$ + #define M5_GROOVEIIC$GS$ + #endif + GROOVE_IIC(SHT4X,$Z$,1) + GROOVE_IIC(BMP280,$Z$,1) + #define _GWSHT4X + #define _GWBMP280 +#endif #GROVE //example: -DSHT3XG1_A : defines STH3Xn1 on grove A - x depends on the other devices @@ -100,6 +100,25 @@ #define _GWSHT3X #endif +#GROVE +//example: -DSHT4XG1_A : defines STH4Xn1 on grove A - x depends on the other devices +#ifdef GWSHT4XG1$GS$ + #ifndef M5_GROOVEIIC$GS$ + #define M5_GROOVEIIC$GS$ + #endif + GROOVE_IIC(SHT4X,$Z$,1) + #define _GWSHT4X +#endif + +#GROVE +#ifdef GWSHT4XG2$GS$ + #ifndef M5_GROOVEIIC$GS$ + #define M5_GROOVEIIC$GS$ + #endif + GROOVE_IIC(SHT4X,$Z$,2) + #define _GWSHT4X +#endif + #GROVE #ifdef GWQMP6988G1$GS$ #ifndef M5_GROOVEIIC$GS$ diff --git a/lib/iictask/GwIicTask.cpp b/lib/iictask/GwIicTask.cpp index 53ba9bd..3de396b 100644 --- a/lib/iictask/GwIicTask.cpp +++ b/lib/iictask/GwIicTask.cpp @@ -23,7 +23,7 @@ static std::vector iicGroveList; #include "GwBME280.h" #include "GwBMP280.h" #include "GwQMP6988.h" -#include "GwSHT3X.h" +#include "GwSHTXX.h" #include #include "GwTimer.h" diff --git a/lib/iictask/GwSHT3X.cpp b/lib/iictask/GwSHT3X.cpp deleted file mode 100644 index c93486f..0000000 --- a/lib/iictask/GwSHT3X.cpp +++ /dev/null @@ -1,138 +0,0 @@ -#include "GwSHT3X.h" -#ifdef _GWSHT3X -class SHT3XConfig; -static GwSensorConfigInitializerList configs; -class SHT3XConfig : public IICSensorBase{ - public: - String tmNam; - String huNam; - bool tmAct=false; - bool huAct=false; - tN2kHumiditySource huSrc; - tN2kTempSource tmSrc; - SHT3X *device=nullptr; - using IICSensorBase::IICSensorBase; - virtual bool isActive(){ - return tmAct || huAct; - } - virtual bool initDevice(GwApi * api,TwoWire *wire){ - if (! isActive()) return false; - device=new SHT3X(); - device->init(addr,wire); - GwLog *logger=api->getLogger(); - LOG_DEBUG(GwLog::LOG,"initialized %s at address %d, intv %ld",prefix.c_str(),(int)addr,intv); - return true; - } - virtual bool preinit(GwApi * api){ - GwLog *logger=api->getLogger(); - LOG_DEBUG(GwLog::LOG,"%s configured",prefix.c_str()); - addHumidXdr(api,*this); - addTempXdr(api,*this); - return isActive(); - } - virtual void measure(GwApi * api,TwoWire *wire, int counterId) - { - if (!device) - return; - GwLog *logger=api->getLogger(); - int rt = 0; - if ((rt = device->get()) == 0) - { - double temp = device->cTemp; - temp = CToKelvin(temp); - double humid = device->humidity; - LOG_DEBUG(GwLog::DEBUG, "%s measure temp=%2.1f, humid=%2.0f",prefix.c_str(), (float)temp, (float)humid); - if (huAct) - { - sendN2kHumidity(api, *this, humid, counterId); - } - if (tmAct) - { - sendN2kTemperature(api, *this, temp, counterId); - } - } - else - { - LOG_DEBUG(GwLog::DEBUG, "unable to query %s: %d",prefix.c_str(), rt); - } - } - - virtual void readConfig(GwConfigHandler *cfg){ - if (ok) return; - configs.readConfig(this,cfg); - return; - } -}; -SensorBase::Creator creator=[](GwApi *api,const String &prfx)-> SensorBase*{ - if (! configs.knowsPrefix(prfx)) return nullptr; - return new SHT3XConfig(api,prfx); -}; -SensorBase::Creator registerSHT3X(GwApi *api){ - GwLog *logger=api->getLogger(); - #if defined(GWSHT3X) || defined (GWSHT3X11) - { - api->addSensor(creator(api,"SHT3X11")); - CHECK_IIC1(); - #pragma message "GWSHT3X11 defined" - } - #endif - #if defined(GWSHT3X12) - { - api->addSensor(creator(api,"SHT3X12")); - CHECK_IIC1(); - #pragma message "GWSHT3X12 defined" - } - #endif - #if defined(GWSHT3X21) - { - api->addSensor(creator(api,"SHT3X21")); - CHECK_IIC2(); - #pragma message "GWSHT3X21 defined" - } - #endif - #if defined(GWSHT3X22) - { - api->addSensor(creator(api,"SHT3X22")); - CHECK_IIC2(); - #pragma message "GWSHT3X22 defined" - } - #endif - return creator; -}; - -/** - * we do not dynamically compute the config names - * just to get compile time errors if something does not fit - * correctly - */ -#define CFGSHT3X(s, prefix, bus, baddr) \ - CFG_SGET(s, tmNam, prefix); \ - CFG_SGET(s, huNam, prefix); \ - CFG_SGET(s, iid, prefix); \ - CFG_SGET(s, tmAct, prefix); \ - CFG_SGET(s, huAct, prefix); \ - CFG_SGET(s, intv, prefix); \ - CFG_SGET(s, huSrc, prefix); \ - CFG_SGET(s, tmSrc, prefix); \ - s->busId = bus; \ - s->addr = baddr; \ - s->ok = true; \ - s->intv *= 1000; - -#define SCSHT3X(prefix, bus, addr) \ - GWSENSORDEF(configs, SHT3XConfig, CFGSHT3X, prefix, bus, addr) - -SCSHT3X(SHT3X11, 1, 0x44); -SCSHT3X(SHT3X12, 1, 0x45); -SCSHT3X(SHT3X21, 2, 0x44); -SCSHT3X(SHT3X22, 2, 0x45); - -#else -SensorBase::Creator registerSHT3X(GwApi *api){ - return SensorBase::Creator(); -} - -#endif - - - diff --git a/lib/iictask/GwSHT3X.h b/lib/iictask/GwSHT3X.h deleted file mode 100644 index 6a5dfcf..0000000 --- a/lib/iictask/GwSHT3X.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef _GWSHT3X_H -#define _GWSHT3X_H -#include "GwIicSensors.h" -#ifdef _GWIIC - #if defined(GWSHT3X) || defined(GWSHT3X11) || defined(GWSHT3X12) || defined(GWSHT3X21) || defined(GWSHT3X22) - #define _GWSHT3X - #endif -#else - #undef _GWSHT3X - #undef GWSHT3X - #undef GWSHT3X11 - #undef GWSHT3X12 - #undef GWSHT3X21 - #undef GWSHT3X22 -#endif -#ifdef _GWSHT3X - #include "SHT3X.h" -#endif -SensorBase::Creator registerSHT3X(GwApi *api); -#endif \ No newline at end of file diff --git a/lib/iictask/SHT3X.cpp b/lib/iictask/SHT3X.cpp index 7830cf5..c36d92e 100644 --- a/lib/iictask/SHT3X.cpp +++ b/lib/iictask/SHT3X.cpp @@ -1,4 +1,4 @@ -#include "GwSHT3X.h" +#include "GwSHTXX.h" #ifdef _GWSHT3X bool SHT3X::init(uint8_t slave_addr_in, TwoWire* wire_in) diff --git a/lib/iictask/SHT4X.cpp b/lib/iictask/SHT4X.cpp new file mode 100644 index 0000000..7dc9235 --- /dev/null +++ b/lib/iictask/SHT4X.cpp @@ -0,0 +1,128 @@ +#include "SHT4X.h" +uint8_t crc8(const uint8_t *data, int len) { + /* + * + * CRC-8 formula from page 14 of SHT spec pdf + * + * Test data 0xBE, 0xEF should yield 0x92 + * + * Initialization data 0xFF + * Polynomial 0x31 (x8 + x5 +x4 +1) + * Final XOR 0x00 + */ + + const uint8_t POLYNOMIAL(0x31); + uint8_t crc(0xFF); + + for (int j = len; j; --j) { + crc ^= *data++; + + for (int i = 8; i; --i) { + crc = (crc & 0x80) ? (crc << 1) ^ POLYNOMIAL : (crc << 1); + } + } + return crc; +} + +bool SHT4X::begin(TwoWire* wire, uint8_t addr) { + _addr = addr; + _wire = wire; + int error; + _wire->beginTransmission(addr); + error = _wire->endTransmission(); + if (error == 0) { + return true; + } + return false; +} + +bool SHT4X::update() { + uint8_t readbuffer[6]; + uint8_t cmd = SHT4x_NOHEAT_HIGHPRECISION; + uint16_t duration = 10; + + if (_heater == SHT4X_NO_HEATER) { + if (_precision == SHT4X_HIGH_PRECISION) { + cmd = SHT4x_NOHEAT_HIGHPRECISION; + duration = 10; + } + if (_precision == SHT4X_MED_PRECISION) { + cmd = SHT4x_NOHEAT_MEDPRECISION; + duration = 5; + } + if (_precision == SHT4X_LOW_PRECISION) { + cmd = SHT4x_NOHEAT_LOWPRECISION; + duration = 2; + } + } + + if (_heater == SHT4X_HIGH_HEATER_1S) { + cmd = SHT4x_HIGHHEAT_1S; + duration = 1100; + } + if (_heater == SHT4X_HIGH_HEATER_100MS) { + cmd = SHT4x_HIGHHEAT_100MS; + duration = 110; + } + + if (_heater == SHT4X_MED_HEATER_1S) { + cmd = SHT4x_MEDHEAT_1S; + duration = 1100; + } + if (_heater == SHT4X_MED_HEATER_100MS) { + cmd = SHT4x_MEDHEAT_100MS; + duration = 110; + } + + if (_heater == SHT4X_LOW_HEATER_1S) { + cmd = SHT4x_LOWHEAT_1S; + duration = 1100; + } + if (_heater == SHT4X_LOW_HEATER_100MS) { + cmd = SHT4x_LOWHEAT_100MS; + duration = 110; + } + // _i2c.writeByte(_addr, cmd, 1); + _wire->beginTransmission(_addr); + _wire->write(cmd); + _wire->write(1); + _wire->endTransmission(); + + + delay(duration); + + _wire->requestFrom(_addr, (uint8_t)6); + + for (uint16_t i = 0; i < 6; i++) { + readbuffer[i] = _wire->read(); + } + + if (readbuffer[2] != crc8(readbuffer, 2) || + readbuffer[5] != crc8(readbuffer + 3, 2)) { + return false; + } + + float t_ticks = (uint16_t)readbuffer[0] * 256 + (uint16_t)readbuffer[1]; + float rh_ticks = (uint16_t)readbuffer[3] * 256 + (uint16_t)readbuffer[4]; + + cTemp = -45 + 175 * t_ticks / 65535; + humidity = -6 + 125 * rh_ticks / 65535; + humidity = min(max(humidity, (float)0.0), (float)100.0); + return true; +} + +void SHT4X::setPrecision(sht4x_precision_t prec) { + _precision = prec; +} + +sht4x_precision_t SHT4X::getPrecision(void) { + return _precision; +} + +void SHT4X::setHeater(sht4x_heater_t heat) { + _heater = heat; +} + +sht4x_heater_t SHT4X::getHeater(void) { + return _heater; +} diff --git a/lib/iictask/SHT4X.h b/lib/iictask/SHT4X.h new file mode 100644 index 0000000..dbfbabf --- /dev/null +++ b/lib/iictask/SHT4X.h @@ -0,0 +1,76 @@ +#ifndef __SHT4X_H_ +#define __SHT4X_H_ + +#include "Arduino.h" +#include "Wire.h" + +#define SHT40_I2C_ADDR_44 0x44 +#define SHT40_I2C_ADDR_45 0x45 +#define SHT41_I2C_ADDR_44 0x44 +#define SHT41_I2C_ADDR_45 0x45 +#define SHT45_I2C_ADDR_44 0x44 +#define SHT45_I2C_ADDR_45 0x45 + +#define SHT4x_DEFAULT_ADDR 0x44 /**< SHT4x I2C Address */ +#define SHT4x_NOHEAT_HIGHPRECISION \ + 0xFD /**< High precision measurement, no heater */ +#define SHT4x_NOHEAT_MEDPRECISION \ + 0xF6 /**< Medium precision measurement, no heater */ +#define SHT4x_NOHEAT_LOWPRECISION \ + 0xE0 /**< Low precision measurement, no heater */ + +#define SHT4x_HIGHHEAT_1S \ + 0x39 /**< High precision measurement, high heat for 1 sec */ +#define SHT4x_HIGHHEAT_100MS \ + 0x32 /**< High precision measurement, high heat for 0.1 sec */ +#define SHT4x_MEDHEAT_1S \ + 0x2F /**< High precision measurement, med heat for 1 sec */ +#define SHT4x_MEDHEAT_100MS \ + 0x24 /**< High precision measurement, med heat for 0.1 sec */ +#define SHT4x_LOWHEAT_1S \ + 0x1E /**< High precision measurement, low heat for 1 sec */ +#define SHT4x_LOWHEAT_100MS \ + 0x15 /**< High precision measurement, low heat for 0.1 sec */ + +#define SHT4x_READSERIAL 0x89 /**< Read Out of Serial Register */ +#define SHT4x_SOFTRESET 0x94 /**< Soft Reset */ + +typedef enum { + SHT4X_HIGH_PRECISION, + SHT4X_MED_PRECISION, + SHT4X_LOW_PRECISION, +} sht4x_precision_t; + +/** Optional pre-heater configuration setting */ +typedef enum { + SHT4X_NO_HEATER, + SHT4X_HIGH_HEATER_1S, + SHT4X_HIGH_HEATER_100MS, + SHT4X_MED_HEATER_1S, + SHT4X_MED_HEATER_100MS, + SHT4X_LOW_HEATER_1S, + SHT4X_LOW_HEATER_100MS, +} sht4x_heater_t; + +class SHT4X { + public: + bool begin(TwoWire* wire = &Wire, uint8_t addr = SHT40_I2C_ADDR_44); + bool update(void); + + float cTemp = 0; + float humidity = 0; + + void setPrecision(sht4x_precision_t prec); + sht4x_precision_t getPrecision(void); + void setHeater(sht4x_heater_t heat); + sht4x_heater_t getHeater(void); + + private: + TwoWire* _wire; + uint8_t _addr; + + sht4x_precision_t _precision = SHT4X_HIGH_PRECISION; + sht4x_heater_t _heater = SHT4X_NO_HEATER; +}; + +#endif diff --git a/lib/iictask/config.json b/lib/iictask/config.json index 54da57e..73147c6 100644 --- a/lib/iictask/config.json +++ b/lib/iictask/config.json @@ -196,6 +196,203 @@ } ] }, + { + "type": "array", + "name": "SHT4X", + "replace": [ + { + "b": "1", + "i": "11", + "n": "119" + }, + { + "b": "1", + "i": "12", + "n": "118" + }, + { + "b": "2", + "i": "21", + "n": "129" + }, + { + "b": "2", + "i": "22", + "n": "128" + } + + + ], + "children": [ + { + "name": "SHT4X$itmAct", + "label": "SHT4X$i Temp", + "type": "boolean", + "default": "true", + "description": "Enable the $i. I2C SHT4X temp sensor (bus $b)", + "category": "iicsensors$b", + "capabilities": { + "SHT4X$i": "true" + } + }, + { + "name": "SHT4X$itmSrc", + "label": "SHT4X$i Temp Type", + "type": "list", + "default": "2", + "description": "the NMEA2000 source type for the temperature", + "list": [ + { + "l": "SeaTemperature", + "v": "0" + }, + { + "l": "OutsideTemperature", + "v": "1" + }, + { + "l": "InsideTemperature", + "v": "2" + }, + { + "l": "EngineRoomTemperature", + "v": "3" + }, + { + "l": "MainCabinTemperature", + "v": "4" + }, + { + "l": "LiveWellTemperature", + "v": "5" + }, + { + "l": "BaitWellTemperature", + "v": "6" + }, + { + "l": "RefridgerationTemperature", + "v": "7" + }, + { + "l": "HeatingSystemTemperature", + "v": "8" + }, + { + "l": "DewPointTemperature", + "v": "9" + }, + { + "l": "ApparentWindChillTemperature", + "v": "10" + }, + { + "l": "TheoreticalWindChillTemperature", + "v": "11" + }, + { + "l": "HeatIndexTemperature", + "v": "12" + }, + { + "l": "FreezerTemperature", + "v": "13" + }, + { + "l": "ExhaustGasTemperature", + "v": "14" + }, + { + "l": "ShaftSealTemperature", + "v": "15" + } + ], + "category": "iicsensors$b", + "capabilities": { + "SHT4X$i": "true" + } + }, + { + "name": "SHT4X$ihuAct", + "label": "SHT4X$i Humidity", + "type": "boolean", + "default": "true", + "description": "Enable the $i. I2C SHT4X humidity sensor (bus $b)", + "category": "iicsensors$b", + "capabilities": { + "SHT4X$i": "true" + } + }, + { + "name": "SHT4X$ihuSrc", + "label": "SHT4X$i Humid Type", + "list": [ + { + "l": "OutsideHumidity", + "v": "1" + }, + { + "l": "Undef", + "v": "0xff" + } + ], + "category": "iicsensors$b", + "capabilities": { + "SHT4X": "true" + } + }, + { + "name": "SHT4X$iiid", + "label": "SHT4X$i N2K iid", + "type": "number", + "default": "$n", + "description": "the N2K instance id for the $i. SHT4X Temperature and Humidity ", + "category": "iicsensors$b", + "min": 0, + "max": 253, + "check": "checkMinMax", + "capabilities": { + "SHT4X$i": "true" + } + }, + { + "name": "SHT4X$iintv", + "label": "SHT4X$i Interval", + "type": "number", + "default": 2, + "description": "Interval(s) to query SHT4X Temperature and Humidity (1...300)", + "category": "iicsensors$b", + "min": 1, + "max": 300, + "check": "checkMinMax", + "capabilities": { + "SHT4X$i": "true" + } + }, + { + "name": "SHT4X$itmNam", + "label": "SHT4X$i Temp XDR", + "type": "String", + "default": "Temp$i", + "description": "set the XDR transducer name for the $i. SHT4X Temperature, leave empty to disable NMEA0183 XDR ", + "category": "iicsensors$b", + "capabilities": { + "SHT4X$i": "true" + } + }, + { + "name": "SHT4X$ihuNam", + "label": "SHT4X$i Humid XDR", + "type": "String", + "default": "Humidity$i", + "description": "set the XDR transducer name for the $i. SHT4X Humidity, leave empty to disable NMEA0183 XDR", + "category": "iicsensors$b", + "capabilities": { + "SHT4X$i": "true" + } + } + ] + }, { "type": "array", "name": "QMP6988", diff --git a/lib/iictask/platformio.ini b/lib/iictask/platformio.ini index c0f10f7..31e17a3 100644 --- a/lib/iictask/platformio.ini +++ b/lib/iictask/platformio.ini @@ -11,6 +11,17 @@ build_flags= -D M5_CAN_KIT ${env.build_flags} +[env:m5stack-atom-env4] +extends = sensors +board = m5stack-atom +lib_deps = + ${env.lib_deps} + ${sensors.lib_deps} +build_flags= + -D M5_ENV4 + -D M5_CAN_KIT + ${env.build_flags} + [env:m5stack-atom-bme280] extends = sensors diff --git a/lib/sensors/GwSensor.h b/lib/sensors/GwSensor.h index 48eb5b2..6bc611f 100644 --- a/lib/sensors/GwSensor.h +++ b/lib/sensors/GwSensor.h @@ -93,6 +93,7 @@ class GwSensorConfig{ } bool readConfig(T* s,GwConfigHandler *cfg){ if (s == nullptr) return false; + if (prefix != s->prefix) return false; configReader(s,cfg); return s->ok; } From fddc3c742b726bc56fa0a0fcc2e8647ece9418d3 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Mon, 29 Sep 2025 17:05:48 +0200 Subject: [PATCH 034/121] allow to switch PGN 130311 for sensors on/off, simplify config.json for i2csensors --- lib/iictask/GwBME280.cpp | 2 + lib/iictask/GwBMP280.cpp | 2 + lib/iictask/GwIicSensors.h | 1 + lib/iictask/GwIicTask.cpp | 1 + lib/iictask/GwQMP6988.cpp | 2 + lib/iictask/config.json | 345 ++++++++++++------------------------- 6 files changed, 118 insertions(+), 235 deletions(-) diff --git a/lib/iictask/GwBME280.cpp b/lib/iictask/GwBME280.cpp index 1bf541f..177a775 100644 --- a/lib/iictask/GwBME280.cpp +++ b/lib/iictask/GwBME280.cpp @@ -23,6 +23,7 @@ class BME280Config : public IICSensorBase{ bool prAct=true; bool tmAct=true; bool huAct=true; + bool sEnv=true; tN2kTempSource tmSrc=tN2kTempSource::N2kts_InsideTemperature; tN2kHumiditySource huSrc=tN2kHumiditySource::N2khs_InsideHumidity; tN2kPressureSource prSrc=tN2kPressureSource::N2kps_Atmospheric; @@ -152,6 +153,7 @@ SensorBase::Creator registerBME280(GwApi *api){ CFG_SGET(s, prNam, prefix); \ CFG_SGET(s, tmOff, prefix); \ CFG_SGET(s, prOff, prefix); \ + CFG_SGET(s, sEnv, prefix); \ s->busId = bus; \ s->addr = baddr; \ s->ok = true; \ diff --git a/lib/iictask/GwBMP280.cpp b/lib/iictask/GwBMP280.cpp index e51bd4b..bee51e4 100644 --- a/lib/iictask/GwBMP280.cpp +++ b/lib/iictask/GwBMP280.cpp @@ -29,6 +29,7 @@ class BMP280Config : public IICSensorBase{ public: bool prAct=true; bool tmAct=true; + bool sEnv=true; tN2kTempSource tmSrc=tN2kTempSource::N2kts_InsideTemperature; tN2kPressureSource prSrc=tN2kPressureSource::N2kps_Atmospheric; tN2kHumiditySource huSrc=tN2kHumiditySource::N2khs_Undef; @@ -150,6 +151,7 @@ SensorBase::Creator registerBMP280(GwApi *api){ CFG_SGET(s, prNam, prefix); \ CFG_SGET(s, tmOff, prefix); \ CFG_SGET(s, prOff, prefix); \ + CFG_SGET(s, sEnv,prefix); \ s->busId = bus; \ s->addr = baddr; \ s->ok = true; \ diff --git a/lib/iictask/GwIicSensors.h b/lib/iictask/GwIicSensors.h index 4937daa..a9465d5 100644 --- a/lib/iictask/GwIicSensors.h +++ b/lib/iictask/GwIicSensors.h @@ -104,6 +104,7 @@ void sendN2kTemperature(GwApi *api,CFG &cfg,double value, int counterId){ template void sendN2kEnvironmentalParameters(GwApi *api,CFG &cfg,double tmValue, double huValue, double prValue, int counterId){ + if (! cfg.sEnv) return; tN2kMsg msg; SetN2kEnvironmentalParameters(msg,1,cfg.tmSrc,tmValue,cfg.huSrc,huValue,prValue); api->sendN2kMessage(msg); diff --git a/lib/iictask/GwIicTask.cpp b/lib/iictask/GwIicTask.cpp index 3de396b..998e441 100644 --- a/lib/iictask/GwIicTask.cpp +++ b/lib/iictask/GwIicTask.cpp @@ -91,6 +91,7 @@ void initIicTask(GwApi *api){ GwConfigHandler *config=api->getConfig(); std::vector creators; creators.push_back(registerSHT3X(api)); + creators.push_back(registerSHT4X(api)); creators.push_back(registerQMP6988(api)); creators.push_back(registerBME280(api)); creators.push_back(registerBMP280(api)); diff --git a/lib/iictask/GwQMP6988.cpp b/lib/iictask/GwQMP6988.cpp index 4f2b78c..12b648f 100644 --- a/lib/iictask/GwQMP6988.cpp +++ b/lib/iictask/GwQMP6988.cpp @@ -9,6 +9,7 @@ class QMP6988Config : public IICSensorBase{ public: String prNam="Pressure"; bool prAct=true; + bool sEnv=true; tN2kPressureSource prSrc=tN2kPressureSource::N2kps_Atmospheric; float prOff=0; QMP6988 *device=nullptr; @@ -90,6 +91,7 @@ SensorBase::Creator registerQMP6988(GwApi *api){ CFG_SGET(s,prAct,prefix); \ CFG_SGET(s,intv,prefix); \ CFG_SGET(s,prOff,prefix); \ + CFG_SGET(s,sEnv,prefix); \ s->busId = bus; \ s->addr = baddr; \ s->ok = true; \ diff --git a/lib/iictask/config.json b/lib/iictask/config.json index 73147c6..5cd50af 100644 --- a/lib/iictask/config.json +++ b/lib/iictask/config.json @@ -1,49 +1,77 @@ [ { "type": "array", - "name": "SHT3X", + "name": "SHTXX", "replace": [ { "b": "1", "i": "11", - "n": "99" + "n": "99", + "x": "3" }, { "b": "1", "i": "12", - "n": "98" + "n": "98", + "x": "3" }, { "b": "2", "i": "21", - "n": "109" + "n": "109", + "x": "3" }, { "b": "2", "i": "22", - "n": "108" + "n": "108", + "x": "3" + }, + { + "b": "1", + "i": "11", + "n": "119", + "x": "4" + }, + { + "b": "1", + "i": "12", + "n": "118", + "x": "4" + }, + { + "b": "2", + "i": "21", + "n": "129", + "x": "4" + }, + { + "b": "2", + "i": "22", + "n": "128", + "x": "4" } ], "children": [ { - "name": "SHT3X$itmAct", - "label": "SHT3X$i Temp", + "name": "SHT$xX$itmAct", + "label": "SHT$xX$i Temp", "type": "boolean", "default": "true", - "description": "Enable the $i. I2C SHT3x temp sensor (bus $b)", + "description": "Enable the $i. I2C SHT$xX temp sensor (bus $b)", "category": "iicsensors$b", "capabilities": { - "SHT3X$i": "true" + "SHT$xX$i": "true" } }, { - "name": "SHT3X$itmSrc", - "label": "SHT3X$i Temp Type", + "name": "SHT$xX$itmSrc", + "label": "SHT$xX$i Temp Type", "type": "list", "default": "2", - "description": "the NMEA2000 source type for the temperature", + "description": "the NMEA2000 source type for the temperature (PGN 130312,130311)", "list": [ { "l": "SeaTemperature", @@ -112,23 +140,23 @@ ], "category": "iicsensors$b", "capabilities": { - "SHT3X$i": "true" + "SHT$xX$i": "true" } }, { - "name": "SHT3X$ihuAct", - "label": "SHT3X$i Humidity", + "name": "SHT$xX$ihuAct", + "label": "SHT$xX$i Humidity", "type": "boolean", "default": "true", - "description": "Enable the $i. I2C SHT3x humidity sensor (bus $b)", + "description": "Enable the $i. I2C SHT$xX humidity sensor (bus $b)", "category": "iicsensors$b", "capabilities": { - "SHT3X$i": "true" + "SHT$xX$i": "true" } }, { - "name": "SHT3X$ihuSrc", - "label": "SHT3X$i Humid Type", + "name": "SHT$xX$ihuSrc", + "label": "SHT$xX$i Humid Type", "list": [ { "l": "OutsideHumidity", @@ -141,254 +169,68 @@ ], "category": "iicsensors$b", "capabilities": { - "SHT3X": "true" + "SHT$xX": "true" } }, { - "name": "SHT3X$iiid", - "label": "SHT3X$i N2K iid", + "name": "SHT$xX$iiid", + "label": "SHT$xX$i N2K iid", "type": "number", "default": "$n", - "description": "the N2K instance id for the $i. SHT3X Temperature and Humidity ", + "description": "the N2K instance id for the $i. SHT$xX Temperature and Humidity (PGN 130312,130311) ", "category": "iicsensors$b", "min": 0, "max": 253, "check": "checkMinMax", "capabilities": { - "SHT3X$i": "true" + "SHT$xX$i": "true" } }, { - "name": "SHT3X$iintv", - "label": "SHT3X$i Interval", + "name": "SHT$xX$isEnv", + "label": "SHT$xX$i send Env", + "type": "boolean", + "default": "true", + "description": "also send PGN 130311", + "category": "iicsensors$b", + "capabilities": { + "SHT$xX$i": "true" + } + }, + { + "name": "SHT$xX$iintv", + "label": "SHT$xX$i Interval", "type": "number", "default": 2, - "description": "Interval(s) to query SHT3X Temperature and Humidity (1...300)", + "description": "Interval(s) to query SHT$xX Temperature and Humidity (1...300)", "category": "iicsensors$b", "min": 1, "max": 300, "check": "checkMinMax", "capabilities": { - "SHT3X$i": "true" + "SHT$xX$i": "true" } }, { - "name": "SHT3X$itmNam", - "label": "SHT3X$i Temp XDR", + "name": "SHT$xX$itmNam", + "label": "SHT$xX$i Temp XDR", "type": "String", "default": "Temp$i", - "description": "set the XDR transducer name for the $i. SHT3X Temperature, leave empty to disable NMEA0183 XDR ", + "description": "set the XDR transducer name for the $i. SHT$xX Temperature, leave empty to disable NMEA0183 XDR ", "category": "iicsensors$b", "capabilities": { - "SHT3X$i": "true" + "SHT$xX$i": "true" } }, { - "name": "SHT3X$ihuNam", - "label": "SHT3X$i Humid XDR", + "name": "SHT$xX$ihuNam", + "label": "SHT$xX$i Humid XDR", "type": "String", "default": "Humidity$i", - "description": "set the XDR transducer name for the $i. SHT3X Humidity, leave empty to disable NMEA0183 XDR", + "description": "set the XDR transducer name for the $i. SHT$xX Humidity, leave empty to disable NMEA0183 XDR", "category": "iicsensors$b", "capabilities": { - "SHT3X$i": "true" - } - } - ] - }, - { - "type": "array", - "name": "SHT4X", - "replace": [ - { - "b": "1", - "i": "11", - "n": "119" - }, - { - "b": "1", - "i": "12", - "n": "118" - }, - { - "b": "2", - "i": "21", - "n": "129" - }, - { - "b": "2", - "i": "22", - "n": "128" - } - - - ], - "children": [ - { - "name": "SHT4X$itmAct", - "label": "SHT4X$i Temp", - "type": "boolean", - "default": "true", - "description": "Enable the $i. I2C SHT4X temp sensor (bus $b)", - "category": "iicsensors$b", - "capabilities": { - "SHT4X$i": "true" - } - }, - { - "name": "SHT4X$itmSrc", - "label": "SHT4X$i Temp Type", - "type": "list", - "default": "2", - "description": "the NMEA2000 source type for the temperature", - "list": [ - { - "l": "SeaTemperature", - "v": "0" - }, - { - "l": "OutsideTemperature", - "v": "1" - }, - { - "l": "InsideTemperature", - "v": "2" - }, - { - "l": "EngineRoomTemperature", - "v": "3" - }, - { - "l": "MainCabinTemperature", - "v": "4" - }, - { - "l": "LiveWellTemperature", - "v": "5" - }, - { - "l": "BaitWellTemperature", - "v": "6" - }, - { - "l": "RefridgerationTemperature", - "v": "7" - }, - { - "l": "HeatingSystemTemperature", - "v": "8" - }, - { - "l": "DewPointTemperature", - "v": "9" - }, - { - "l": "ApparentWindChillTemperature", - "v": "10" - }, - { - "l": "TheoreticalWindChillTemperature", - "v": "11" - }, - { - "l": "HeatIndexTemperature", - "v": "12" - }, - { - "l": "FreezerTemperature", - "v": "13" - }, - { - "l": "ExhaustGasTemperature", - "v": "14" - }, - { - "l": "ShaftSealTemperature", - "v": "15" - } - ], - "category": "iicsensors$b", - "capabilities": { - "SHT4X$i": "true" - } - }, - { - "name": "SHT4X$ihuAct", - "label": "SHT4X$i Humidity", - "type": "boolean", - "default": "true", - "description": "Enable the $i. I2C SHT4X humidity sensor (bus $b)", - "category": "iicsensors$b", - "capabilities": { - "SHT4X$i": "true" - } - }, - { - "name": "SHT4X$ihuSrc", - "label": "SHT4X$i Humid Type", - "list": [ - { - "l": "OutsideHumidity", - "v": "1" - }, - { - "l": "Undef", - "v": "0xff" - } - ], - "category": "iicsensors$b", - "capabilities": { - "SHT4X": "true" - } - }, - { - "name": "SHT4X$iiid", - "label": "SHT4X$i N2K iid", - "type": "number", - "default": "$n", - "description": "the N2K instance id for the $i. SHT4X Temperature and Humidity ", - "category": "iicsensors$b", - "min": 0, - "max": 253, - "check": "checkMinMax", - "capabilities": { - "SHT4X$i": "true" - } - }, - { - "name": "SHT4X$iintv", - "label": "SHT4X$i Interval", - "type": "number", - "default": 2, - "description": "Interval(s) to query SHT4X Temperature and Humidity (1...300)", - "category": "iicsensors$b", - "min": 1, - "max": 300, - "check": "checkMinMax", - "capabilities": { - "SHT4X$i": "true" - } - }, - { - "name": "SHT4X$itmNam", - "label": "SHT4X$i Temp XDR", - "type": "String", - "default": "Temp$i", - "description": "set the XDR transducer name for the $i. SHT4X Temperature, leave empty to disable NMEA0183 XDR ", - "category": "iicsensors$b", - "capabilities": { - "SHT4X$i": "true" - } - }, - { - "name": "SHT4X$ihuNam", - "label": "SHT4X$i Humid XDR", - "type": "String", - "default": "Humidity$i", - "description": "set the XDR transducer name for the $i. SHT4X Humidity, leave empty to disable NMEA0183 XDR", - "category": "iicsensors$b", - "capabilities": { - "SHT4X$i": "true" + "SHT$xX$i": "true" } } ] @@ -444,6 +286,17 @@ "QMP6988$i": "true" } }, + { + "name": "QMP6988$isEnv", + "label": "QMP6988$i send Env", + "type": "boolean", + "default": "true", + "description": "also send PGN 130311", + "category": "iicsensors$b", + "capabilities": { + "QMP6988$i": "true" + } + }, { "name": "QMP6988$iintv", "label": "QMP6988-$i Interval", @@ -670,7 +523,7 @@ "label": "BME280-$i N2K iid", "type": "number", "default": "$n", - "description": "the N2K instance id for the BME280 Temperature and Humidity ", + "description": "the N2K instance id for the BME280 Temperature, Humidity, Pressure (PGN 130312,130313, 130314) ", "category": "iicsensors$b", "min": 0, "max": 253, @@ -679,6 +532,17 @@ "BME280$i": "true" } }, + { + "name": "BME280$isEnv", + "label": "BME280$i send Env", + "type": "boolean", + "default": "true", + "description": "also send PGN 130311", + "category": "iicsensors$b", + "capabilities": { + "BME280$i": "true" + } + }, { "name": "BME280$iintv", "label": "BME280-$i Interval", @@ -880,7 +744,7 @@ "label": "BMP280-$i N2K iid", "type": "number", "default": "$n", - "description": "the N2K instance id for the BMP280 Temperature", + "description": "the N2K instance id for the BMP280 Temperature/Pressure (PGN 130312,130314)", "category": "iicsensors$b", "min": 0, "max": 253, @@ -889,6 +753,17 @@ "BMP280$i": "true" } }, + { + "name": "BMP280$isEnv", + "label": "BMP280$i send Env", + "type": "boolean", + "default": "true", + "description": "also send PGN 130311", + "category": "iicsensors$b", + "capabilities": { + "BMP280$i": "true" + } + }, { "name": "BMP280$iintv", "label": "BMP280-$i Interval", From c21592599f26c40ddb43b4e6c305dfd775d15308 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Mon, 29 Sep 2025 17:51:41 +0200 Subject: [PATCH 035/121] re-add GwSHTXX* --- lib/iictask/GwSHTXX.cpp | 254 ++++++++++++++++++++++++++++++++++++++++ lib/iictask/GwSHTXX.h | 33 ++++++ 2 files changed, 287 insertions(+) create mode 100644 lib/iictask/GwSHTXX.cpp create mode 100644 lib/iictask/GwSHTXX.h diff --git a/lib/iictask/GwSHTXX.cpp b/lib/iictask/GwSHTXX.cpp new file mode 100644 index 0000000..0cfcca7 --- /dev/null +++ b/lib/iictask/GwSHTXX.cpp @@ -0,0 +1,254 @@ +#include "GwSHTXX.h" +#if defined(_GWSHT3X) || defined(_GWSHT4X) +class SHTXXConfig : public IICSensorBase{ + public: + String tmNam; + String huNam; + bool tmAct=false; + bool huAct=false; + bool sEnv=true; + tN2kHumiditySource huSrc; + tN2kTempSource tmSrc; + using IICSensorBase::IICSensorBase; + virtual bool isActive(){ + return tmAct || huAct; + } + virtual bool preinit(GwApi * api){ + GwLog *logger=api->getLogger(); + LOG_DEBUG(GwLog::LOG,"%s configured",prefix.c_str()); + addHumidXdr(api,*this); + addTempXdr(api,*this); + return isActive(); + } + virtual bool doMeasure(GwApi * api,double &temp, double &humid){ + return false; + } + virtual void measure(GwApi * api,TwoWire *wire, int counterId) override + { + GwLog *logger=api->getLogger(); + double temp = N2kDoubleNA; + double humid = N2kDoubleNA; + if (doMeasure(api,temp,humid)){ + temp = CToKelvin(temp); + LOG_DEBUG(GwLog::DEBUG, "%s measure temp=%2.1f, humid=%2.0f",prefix.c_str(), (float)temp, (float)humid); + if (huAct) + { + sendN2kHumidity(api, *this, humid, counterId); + } + if (tmAct) + { + sendN2kTemperature(api, *this, temp, counterId); + } + if (huAct || tmAct){ + sendN2kEnvironmentalParameters(api,*this,temp,humid,N2kDoubleNA,counterId); + } + } + } + +}; +/** + * we do not dynamically compute the config names + * just to get compile time errors if something does not fit + * correctly + */ +#define INITSHTXX(type,prefix,bus,baddr) \ +[] (type *s ,GwConfigHandler *cfg) { \ + CFG_SGET(s, tmNam, prefix); \ + CFG_SGET(s, huNam, prefix); \ + CFG_SGET(s, iid, prefix); \ + CFG_SGET(s, tmAct, prefix); \ + CFG_SGET(s, huAct, prefix); \ + CFG_SGET(s, intv, prefix); \ + CFG_SGET(s, huSrc, prefix); \ + CFG_SGET(s, tmSrc, prefix); \ + CFG_SGET(s, sEnv,prefix); \ + s->busId = bus; \ + s->addr = baddr; \ + s->ok = true; \ + s->intv *= 1000; \ +} + +#if defined(_GWSHT3X) +class SHT3XConfig; +static GwSensorConfigInitializerList configs3; +class SHT3XConfig : public SHTXXConfig{ + SHT3X *device=nullptr; + public: + using SHTXXConfig::SHTXXConfig; + virtual bool initDevice(GwApi * api,TwoWire *wire)override{ + if (! isActive()) return false; + device=new SHT3X(); + device->init(addr,wire); + GwLog *logger=api->getLogger(); + LOG_DEBUG(GwLog::LOG,"initialized %s at address %d, intv %ld",prefix.c_str(),(int)addr,intv); + return true; + } + virtual bool doMeasure(GwApi *api,double &temp, double &humid) override{ + if (!device) + return false; + int rt=0; + GwLog *logger=api->getLogger(); + if ((rt = device->get()) == 0) + { + temp = device->cTemp; + humid = device->humidity; + return true; + } + else{ + LOG_DEBUG(GwLog::DEBUG, "unable to query %s: %d",prefix.c_str(), rt); + } + return false; + } + virtual void readConfig(GwConfigHandler *cfg) override{ + if (ok) return; + configs3.readConfig(this,cfg); + return; + } +}; + +SensorBase::Creator creator3=[](GwApi *api,const String &prfx)-> SensorBase*{ + if (! configs3.knowsPrefix(prfx)) return nullptr; + return new SHT3XConfig(api,prfx); + }; +SensorBase::Creator registerSHT3X(GwApi *api){ + GwLog *logger=api->getLogger(); + #if defined(GWSHT3X) || defined (GWSHT3X11) + { + api->addSensor(creator3(api,"SHT3X11")); + CHECK_IIC1(); + #pragma message "GWSHT3X11 defined" + } + #endif + #if defined(GWSHT3X12) + { + api->addSensor(creator3(api,"SHT3X12")); + CHECK_IIC1(); + #pragma message "GWSHT3X12 defined" + } + #endif + #if defined(GWSHT3X21) + { + api->addSensor(creator3(api,"SHT3X21")); + CHECK_IIC2(); + #pragma message "GWSHT3X21 defined" + } + #endif + #if defined(GWSHT3X22) + { + api->addSensor(creator3(api,"SHT3X22")); + CHECK_IIC2(); + #pragma message "GWSHT3X22 defined" + } + #endif + return creator3; +}; + + +#define SCSHT3X(prefix, bus, addr) \ + GwSensorConfigInitializer __initCFGSHT3X ## prefix \ + (configs3,GwSensorConfig(#prefix,INITSHTXX(SHT3XConfig,prefix,bus,addr))); + +SCSHT3X(SHT3X11, 1, 0x44); +SCSHT3X(SHT3X12, 1, 0x45); +SCSHT3X(SHT3X21, 2, 0x44); +SCSHT3X(SHT3X22, 2, 0x45); + +#endif +#if defined(_GWSHT4X) +class SHT4XConfig; +static GwSensorConfigInitializerList configs4; +class SHT4XConfig : public SHTXXConfig{ + SHT4X *device=nullptr; + public: + using SHTXXConfig::SHTXXConfig; + virtual bool initDevice(GwApi * api,TwoWire *wire)override{ + if (! isActive()) return false; + device=new SHT4X(); + device->begin(wire,addr); + GwLog *logger=api->getLogger(); + LOG_DEBUG(GwLog::LOG,"initialized %s at address %d, intv %ld",prefix.c_str(),(int)addr,intv); + return true; + } + virtual bool doMeasure(GwApi *api,double &temp, double &humid) override{ + if (!device) + return false; + GwLog *logger=api->getLogger(); + if (device->update()) + { + temp = device->cTemp; + humid = device->humidity; + return true; + } + else{ + LOG_DEBUG(GwLog::DEBUG, "unable to query %s",prefix.c_str()); + } + return false; + } + virtual void readConfig(GwConfigHandler *cfg) override{ + if (ok) return; + configs4.readConfig(this,cfg); + return; + } +}; + +SensorBase::Creator creator4=[](GwApi *api,const String &prfx)-> SensorBase*{ + if (! configs4.knowsPrefix(prfx)) return nullptr; + return new SHT4XConfig(api,prfx); + }; +SensorBase::Creator registerSHT4X(GwApi *api){ + GwLog *logger=api->getLogger(); + #if defined(GWSHT4X) || defined (GWSHT4X11) + { + api->addSensor(creator3(api,"SHT4X11")); + CHECK_IIC1(); + #pragma message "GWSHT4X11 defined" + } + #endif + #if defined(GWSHT4X12) + { + api->addSensor(creator3(api,"SHT4X12")); + CHECK_IIC1(); + #pragma message "GWSHT4X12 defined" + } + #endif + #if defined(GWSHT4X21) + { + api->addSensor(creator3(api,"SHT4X21")); + CHECK_IIC2(); + #pragma message "GWSHT4X21 defined" + } + #endif + #if defined(GWSHT4X22) + { + api->addSensor(creator3(api,"SHT4X22")); + CHECK_IIC2(); + #pragma message "GWSHT4X22 defined" + } + #endif + return creator4; +}; + + +#define SCSHT4X(prefix, bus, addr) \ + GwSensorConfigInitializer __initCFGSHT4X ## prefix \ + (configs4,GwSensorConfig(#prefix,INITSHTXX(SHT4XConfig,prefix,bus,addr))); + +SCSHT4X(SHT4X11, 1, 0x44); +SCSHT4X(SHT4X12, 1, 0x45); +SCSHT4X(SHT4X21, 2, 0x44); +SCSHT4X(SHT4X22, 2, 0x45); +#endif +#endif +#ifndef _GWSHT3X +SensorBase::Creator registerSHT3X(GwApi *api){ + return SensorBase::Creator(); +} +#endif +#ifndef _GWSHT4X +SensorBase::Creator registerSHT4X(GwApi *api){ + return SensorBase::Creator(); +} +#endif + + + diff --git a/lib/iictask/GwSHTXX.h b/lib/iictask/GwSHTXX.h new file mode 100644 index 0000000..52829b9 --- /dev/null +++ b/lib/iictask/GwSHTXX.h @@ -0,0 +1,33 @@ +#ifndef _GWSHTXX_H +#define _GWSHTXX_H +#include "GwIicSensors.h" +#ifdef _GWIIC + #if defined(GWSHT3X) || defined(GWSHT3X11) || defined(GWSHT3X12) || defined(GWSHT3X21) || defined(GWSHT3X22) + #define _GWSHT3X + #endif + #if defined(GWSHT4X) || defined(GWSHT4X11) || defined(GWSHT4X12) || defined(GWSHT4X21) || defined(GWSHT4X22) + #define _GWSHT4X + #endif +#else + #undef _GWSHT3X + #undef GWSHT3X + #undef GWSHT3X11 + #undef GWSHT3X12 + #undef GWSHT3X21 + #undef GWSHT3X22 + #undef _GWSHT4X + #undef GWSHT4X + #undef GWSHT4X11 + #undef GWSHT4X12 + #undef GWSHT4X21 + #undef GWSHT4X22 +#endif +#ifdef _GWSHT3X + #include "SHT3X.h" +#endif +#ifdef _GWSHT4X + #include "SHT4X.h" +#endif +SensorBase::Creator registerSHT3X(GwApi *api); +SensorBase::Creator registerSHT4X(GwApi *api); +#endif \ No newline at end of file From 32862b9e29cd9032b290df1ed359d9954403b7fd Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Mon, 29 Sep 2025 17:53:14 +0200 Subject: [PATCH 036/121] avoid creating unmapped XDR entries for unset N2K values --- lib/nmea0183ton2k/NMEA0183DataToN2K.cpp | 2 +- lib/nmea2kto0183/N2kDataToNMEA0183.cpp | 47 +++++++++++++------------ lib/xdrmappings/GwXDRMappings.cpp | 3 +- lib/xdrmappings/GwXDRMappings.h | 2 +- 4 files changed, 28 insertions(+), 26 deletions(-) diff --git a/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp b/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp index ef17866..6989570 100644 --- a/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp +++ b/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp @@ -143,7 +143,7 @@ private: */ GwXDRFoundMapping getOtherFieldMapping(GwXDRFoundMapping &found, int field){ if (found.empty) return GwXDRFoundMapping(); - return xdrMappings->getMapping(found.definition->category, + return xdrMappings->getMapping(0,found.definition->category, found.definition->selector, field, found.instanceId); diff --git a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp index e2b61fd..0c0e68c 100644 --- a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp +++ b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp @@ -1147,12 +1147,12 @@ private: double Level=N2kDoubleNA; double Capacity=N2kDoubleNA; if (ParseN2kPGN127505(N2kMsg,Instance,FluidType,Level,Capacity)) { - GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRFLUID,FluidType,0,Instance); + GwXDRFoundMapping mapping=xdrMappings->getMapping(Level,XDRFLUID,FluidType,0,Instance); if (updateDouble(&mapping,Level)){ LOG_DEBUG(GwLog::DEBUG+1,"found fluidlevel mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(Level)); } - mapping=xdrMappings->getMapping(XDRFLUID,FluidType,1,Instance); + mapping=xdrMappings->getMapping(Capacity, XDRFLUID,FluidType,1,Instance); if (updateDouble(&mapping,Capacity)){ LOG_DEBUG(GwLog::DEBUG+1,"found fluid capacity mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(Capacity)); @@ -1170,19 +1170,19 @@ private: double BatteryTemperature=N2kDoubleNA; if (ParseN2kPGN127508(N2kMsg,BatteryInstance,BatteryVoltage,BatteryCurrent,BatteryTemperature,SID)) { int i=0; - GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRBAT,0,0,BatteryInstance); + GwXDRFoundMapping mapping=xdrMappings->getMapping(BatteryVoltage, XDRBAT,0,0,BatteryInstance); if (updateDouble(&mapping,BatteryVoltage)){ LOG_DEBUG(GwLog::DEBUG+1,"found BatteryVoltage mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(BatteryVoltage)); i++; } - mapping=xdrMappings->getMapping(XDRBAT,0,1,BatteryInstance); + mapping=xdrMappings->getMapping(BatteryCurrent,XDRBAT,0,1,BatteryInstance); if (updateDouble(&mapping,BatteryCurrent)){ LOG_DEBUG(GwLog::DEBUG+1,"found BatteryCurrent mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(BatteryCurrent)); i++; } - mapping=xdrMappings->getMapping(XDRBAT,0,2,BatteryInstance); + mapping=xdrMappings->getMapping(BatteryTemperature,XDRBAT,0,2,BatteryInstance); if (updateDouble(&mapping,BatteryTemperature)){ LOG_DEBUG(GwLog::DEBUG+1,"found BatteryTemperature mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(BatteryTemperature)); @@ -1214,13 +1214,13 @@ private: SendMessage(NMEA0183Msg); } int i=0; - GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRTEMP,N2kts_OutsideTemperature,0,0); + GwXDRFoundMapping mapping=xdrMappings->getMapping(OutsideAmbientAirTemperature, XDRTEMP,N2kts_OutsideTemperature,0,0); if (updateDouble(&mapping,OutsideAmbientAirTemperature)){ LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(OutsideAmbientAirTemperature)); i++; } - mapping=xdrMappings->getMapping(XDRPRESSURE,N2kps_Atmospheric,0,0); + mapping=xdrMappings->getMapping(AtmosphericPressure,XDRPRESSURE,N2kps_Atmospheric,0,0); if (updateDouble(&mapping,AtmosphericPressure)){ LOG_DEBUG(GwLog::DEBUG+1,"found pressure mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(AtmosphericPressure)); @@ -1255,19 +1255,19 @@ private: SendMessage(NMEA0183Msg); } - GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRTEMP,TempSource,0,0); + GwXDRFoundMapping mapping=xdrMappings->getMapping(Temperature, XDRTEMP,TempSource,0,0); if (updateDouble(&mapping,Temperature)){ LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(Temperature)); i++; } - mapping=xdrMappings->getMapping(XDRHUMIDITY,HumiditySource,0,0); + mapping=xdrMappings->getMapping(Humidity, XDRHUMIDITY,HumiditySource,0,0); if (updateDouble(&mapping,Humidity)){ LOG_DEBUG(GwLog::DEBUG+1,"found humidity mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(Humidity)); i++; } - mapping=xdrMappings->getMapping(XDRPRESSURE,N2kps_Atmospheric,0,0); + mapping=xdrMappings->getMapping(AtmosphericPressure, XDRPRESSURE,N2kps_Atmospheric,0,0); if (updateDouble(&mapping,AtmosphericPressure)){ LOG_DEBUG(GwLog::DEBUG+1,"found pressure mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(AtmosphericPressure)); @@ -1302,12 +1302,12 @@ private: SendMessage(NMEA0183Msg); } - GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRTEMP,(int)TemperatureSource,0,TemperatureInstance); + GwXDRFoundMapping mapping=xdrMappings->getMapping(Temperature, XDRTEMP,(int)TemperatureSource,0,TemperatureInstance); if (updateDouble(&mapping,Temperature)){ LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(Temperature)); } - mapping=xdrMappings->getMapping(XDRTEMP,(int)TemperatureSource,1,TemperatureInstance); + mapping=xdrMappings->getMapping(setTemperature, XDRTEMP,(int)TemperatureSource,1,TemperatureInstance); if (updateDouble(&mapping,setTemperature)){ LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(setTemperature)); @@ -1325,12 +1325,13 @@ private: LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN); return; } - GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRHUMIDITY,(int)HumiditySource,0,HumidityInstance); + GwXDRFoundMapping mapping; + mapping=xdrMappings->getMapping(ActualHumidity, XDRHUMIDITY,(int)HumiditySource,0,HumidityInstance); if (updateDouble(&mapping,ActualHumidity)){ LOG_DEBUG(GwLog::DEBUG+1,"found humidity mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(ActualHumidity)); } - mapping=xdrMappings->getMapping(XDRHUMIDITY,(int)HumiditySource,1,HumidityInstance); + mapping=xdrMappings->getMapping(SetHumidity, XDRHUMIDITY,(int)HumiditySource,1,HumidityInstance); if (updateDouble(&mapping,SetHumidity)){ LOG_DEBUG(GwLog::DEBUG+1,"found humidity mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(SetHumidity)); @@ -1348,7 +1349,7 @@ private: LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN); return; } - GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRPRESSURE,(int)PressureSource,0,PressureInstance); + GwXDRFoundMapping mapping=xdrMappings->getMapping(ActualPressure, XDRPRESSURE,(int)PressureSource,0,PressureInstance); if (! updateDouble(&mapping,ActualPressure)) return; LOG_DEBUG(GwLog::DEBUG+1,"found pressure mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(ActualPressure)); @@ -1366,12 +1367,12 @@ private: LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN); } for (int i=0;i<8;i++){ - GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRENGINE,0,i,instance); + GwXDRFoundMapping mapping=xdrMappings->getMapping(values[i], XDRENGINE,0,i,instance); if (! updateDouble(&mapping,values[i])) continue; addToXdr(mapping.buildXdrEntry(values[i])); } for (int i=0;i< 2;i++){ - GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRENGINE,0,i+8,instance); + GwXDRFoundMapping mapping=xdrMappings->getMapping(ivalues[i],XDRENGINE,0,i+8,instance); if (! updateDouble(&mapping,ivalues[i])) continue; addToXdr(mapping.buildXdrEntry((double)ivalues[i])); } @@ -1387,7 +1388,7 @@ private: LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN); } for (int i=0;i<3;i++){ - GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRATTITUDE,0,i,instance); + GwXDRFoundMapping mapping=xdrMappings->getMapping(values[i], XDRATTITUDE,0,i,instance); if (! updateDouble(&mapping,values[i])) continue; addToXdr(mapping.buildXdrEntry(values[i])); } @@ -1401,15 +1402,15 @@ private: speed,pressure,tilt)){ LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN); } - GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRENGINE,0,10,instance); + GwXDRFoundMapping mapping=xdrMappings->getMapping(speed, XDRENGINE,0,10,instance); if (updateDouble(&mapping,speed)){ addToXdr(mapping.buildXdrEntry(speed)); } - mapping=xdrMappings->getMapping(XDRENGINE,0,11,instance); + mapping=xdrMappings->getMapping(pressure, XDRENGINE,0,11,instance); if (updateDouble(&mapping,pressure)){ addToXdr(mapping.buildXdrEntry(pressure)); } - mapping=xdrMappings->getMapping(XDRENGINE,0,12,instance); + mapping=xdrMappings->getMapping(tilt, XDRENGINE,0,12,instance); if (updateDouble(&mapping,tilt)){ addToXdr(mapping.buildXdrEntry((double)tilt)); } @@ -1435,12 +1436,12 @@ private: LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN); return; } - GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRTEMP,(int)TemperatureSource,0,TemperatureInstance); + GwXDRFoundMapping mapping=xdrMappings->getMapping(Temperature, XDRTEMP,(int)TemperatureSource,0,TemperatureInstance); if (updateDouble(&mapping,Temperature)){ LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(Temperature)); } - mapping=xdrMappings->getMapping(XDRTEMP,(int)TemperatureSource,1,TemperatureInstance); + mapping=xdrMappings->getMapping(setTemperature, XDRTEMP,(int)TemperatureSource,1,TemperatureInstance); if (updateDouble(&mapping,setTemperature)){ LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(setTemperature)); diff --git a/lib/xdrmappings/GwXDRMappings.cpp b/lib/xdrmappings/GwXDRMappings.cpp index 28f88c2..211d394 100644 --- a/lib/xdrmappings/GwXDRMappings.cpp +++ b/lib/xdrmappings/GwXDRMappings.cpp @@ -431,7 +431,8 @@ GwXDRFoundMapping GwXDRMappings::getMapping(String xName,String xType,String xUn } return selectMapping(&(it->second),instance,n183Key.c_str()); } -GwXDRFoundMapping GwXDRMappings::getMapping(GwXDRCategory category,int selector,int field,int instance){ +GwXDRFoundMapping GwXDRMappings::getMapping(double value,GwXDRCategory category,int selector,int field,int instance){ + if (value == N2kDoubleNA) return GwXDRFoundMapping(); //do not add to unknown mappings unsigned long n2kKey=GwXDRMappingDef::n2kKey(category,selector,field); auto it=n2kMap.find(n2kKey); if (it == n2kMap.end()){ diff --git a/lib/xdrmappings/GwXDRMappings.h b/lib/xdrmappings/GwXDRMappings.h index 198a729..21eb9c3 100644 --- a/lib/xdrmappings/GwXDRMappings.h +++ b/lib/xdrmappings/GwXDRMappings.h @@ -244,7 +244,7 @@ class GwXDRMappings{ //get the mappings //the returned mapping will exactly contain one mapping def GwXDRFoundMapping getMapping(String xName,String xType,String xUnit); - GwXDRFoundMapping getMapping(GwXDRCategory category,int selector,int field=0,int instance=-1); + GwXDRFoundMapping getMapping(double value,GwXDRCategory category,int selector,int field=0,int instance=-1); String getXdrEntry(String mapping, double value,int instance=0); const char * getUnMapped(); const GwXDRType * findType(const String &typeString, const String &unitString) const; From 432a10bfb10af137e34837b0415dee1f95c1d684 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Mon, 29 Sep 2025 17:54:16 +0200 Subject: [PATCH 037/121] correctly send 130311 for QMP6988 --- lib/iictask/GwIicSensors.h | 12 +++++++++--- lib/iictask/GwQMP6988.cpp | 3 +++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/iictask/GwIicSensors.h b/lib/iictask/GwIicSensors.h index a9465d5..49fe2fe 100644 --- a/lib/iictask/GwIicSensors.h +++ b/lib/iictask/GwIicSensors.h @@ -108,9 +108,15 @@ void sendN2kEnvironmentalParameters(GwApi *api,CFG &cfg,double tmValue, double h tN2kMsg msg; SetN2kEnvironmentalParameters(msg,1,cfg.tmSrc,tmValue,cfg.huSrc,huValue,prValue); api->sendN2kMessage(msg); - api->increment(counterId,cfg.prefix+String("hum")); - api->increment(counterId,cfg.prefix+String("press")); - api->increment(counterId,cfg.prefix+String("temp")); + if (huValue != N2kDoubleNA){ + api->increment(counterId,cfg.prefix+String("ehum")); + } + if (prValue != N2kDoubleNA){ + api->increment(counterId,cfg.prefix+String("epress")); + } + if (tmValue != N2kDoubleNA){ + api->increment(counterId,cfg.prefix+String("etemp")); + } } #ifndef _GWI_IIC1 diff --git a/lib/iictask/GwQMP6988.cpp b/lib/iictask/GwQMP6988.cpp index 12b648f..1638907 100644 --- a/lib/iictask/GwQMP6988.cpp +++ b/lib/iictask/GwQMP6988.cpp @@ -10,6 +10,8 @@ class QMP6988Config : public IICSensorBase{ String prNam="Pressure"; bool prAct=true; bool sEnv=true; + tN2kTempSource tmSrc=tN2kTempSource::N2kts_InsideTemperature; + tN2kHumiditySource huSrc=tN2kHumiditySource::N2khs_Undef; tN2kPressureSource prSrc=tN2kPressureSource::N2kps_Atmospheric; float prOff=0; QMP6988 *device=nullptr; @@ -40,6 +42,7 @@ class QMP6988Config : public IICSensorBase{ float computed=pressure+prOff; LOG_DEBUG(GwLog::DEBUG,"%s measure %2.0fPa, computed %2.0fPa",prefix.c_str(), pressure,computed); sendN2kPressure(api,*this,computed,counterId); + sendN2kEnvironmentalParameters(api,*this,N2kDoubleNA,N2kDoubleNA,computed,counterId); } From 566d84d3e64d8feac07ab5626a6c05a6b76005ba Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Mon, 29 Sep 2025 18:04:04 +0200 Subject: [PATCH 038/121] correctly handle ifdefs for SHT4X --- lib/iictask/SHT4X.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/iictask/SHT4X.cpp b/lib/iictask/SHT4X.cpp index 7dc9235..6d14473 100644 --- a/lib/iictask/SHT4X.cpp +++ b/lib/iictask/SHT4X.cpp @@ -1,4 +1,6 @@ -#include "SHT4X.h" +#include "GwSHTXX.h" +#ifdef _GWSHT4X + uint8_t crc8(const uint8_t *data, int len) { /* * @@ -126,3 +128,4 @@ void SHT4X::setHeater(sht4x_heater_t heat) { sht4x_heater_t SHT4X::getHeater(void) { return _heater; } +#endif \ No newline at end of file From b68341312904b8b8e2ab0c1ae5e743621769d81b Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Mon, 29 Sep 2025 19:13:02 +0200 Subject: [PATCH 039/121] #117: add handling for an output enable pin for serial channels --- lib/channel/GwChannelList.cpp | 54 +++++++++++++++++++++++++++++------ webinstall/build.yaml | 22 ++++++++++++++ 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/lib/channel/GwChannelList.cpp b/lib/channel/GwChannelList.cpp index 87755b4..d89f227 100644 --- a/lib/channel/GwChannelList.cpp +++ b/lib/channel/GwChannelList.cpp @@ -15,8 +15,10 @@ class SerInit{ int tx=-1; int mode=-1; int fixedBaud=-1; - SerInit(int s,int r,int t, int m, int b=-1): - serial(s),rx(r),tx(t),mode(m),fixedBaud(b){} + int ena=-1; + int elow=1; + SerInit(int s,int r,int t, int m, int b=-1,int en=-1,int el=-1): + serial(s),rx(r),tx(t),mode(m),fixedBaud(b),ena(en),elow(el){} }; std::vector serialInits; @@ -47,11 +49,20 @@ static int typeFromMode(const char *mode){ #ifndef GWSERIAL_RX #define GWSERIAL_RX -1 #endif +#ifndef GWSERIAL_ENA +#define GWSERIAL_ENA -1 +#endif +#ifndef GWSERIAL_ELO +#define GWSERIAL_ELO 1 +#endif +#ifndef GWSERIAL_BAUD +#define GWSERIAL_BAUD -1 +#endif #ifdef GWSERIAL_TYPE - CFG_SERIAL(SERIAL1_CHANNEL_ID, GWSERIAL_RX, GWSERIAL_TX, GWSERIAL_TYPE) + CFG_SERIAL(SERIAL1_CHANNEL_ID, GWSERIAL_RX, GWSERIAL_TX, GWSERIAL_TYPE,GWSERIAL_BAUD,GWSERIAL_ENA,GWSERIAL_ELO) #else #ifdef GWSERIAL_MODE -CFG_SERIAL(SERIAL1_CHANNEL_ID, GWSERIAL_RX, GWSERIAL_TX, typeFromMode(GWSERIAL_MODE)) +CFG_SERIAL(SERIAL1_CHANNEL_ID, GWSERIAL_RX, GWSERIAL_TX, typeFromMode(GWSERIAL_MODE),GWSERIAL_BAUD,GWSERIAL_ENA,GWSERIAL_ELO) #endif #endif // serial 2 @@ -61,11 +72,20 @@ CFG_SERIAL(SERIAL1_CHANNEL_ID, GWSERIAL_RX, GWSERIAL_TX, typeFromMode(GWSERIAL_M #ifndef GWSERIAL2_RX #define GWSERIAL2_RX -1 #endif +#ifndef GWSERIAL2_ENA +#define GWSERIAL2_ENA -1 +#endif +#ifndef GWSERIAL2_ELO +#define GWSERIAL2_ELO 1 +#endif +#ifndef GWSERIAL2_BAUD +#define GWSERIAL2_BAUD -1 +#endif #ifdef GWSERIAL2_TYPE - CFG_SERIAL(SERIAL2_CHANNEL_ID, GWSERIAL2_RX, GWSERIAL2_TX, GWSERIAL2_TYPE) + CFG_SERIAL(SERIAL2_CHANNEL_ID, GWSERIAL2_RX, GWSERIAL2_TX, GWSERIAL2_TYPE,GWSERIAL2_BAUD,GWSERIAL2_ENA,GWSERIAL2_ELO) #else #ifdef GWSERIAL2_MODE -CFG_SERIAL(SERIAL2_CHANNEL_ID, GWSERIAL2_RX, GWSERIAL2_TX, typeFromMode(GWSERIAL2_MODE)) +CFG_SERIAL(SERIAL2_CHANNEL_ID, GWSERIAL2_RX, GWSERIAL2_TX, typeFromMode(GWSERIAL2_MODE),GWSERIAL2_BAUD,GWSERIAL2_ENA,GWSERIAL2_ELO) #endif #endif class GwSerialLog : public GwLogWriter @@ -300,7 +320,7 @@ static ChannelParam * findChannelParam(int id){ return param; } -static GwSerial * createSerialImpl(GwConfigHandler *config,GwLog *logger, int idx,int type,int rx,int tx, bool setLog){ +static GwSerial * createSerialImpl(GwConfigHandler *config,GwLog *logger, int idx,int type,int rx,int tx, bool setLog,int ena=-1,int elow=1){ LOG_DEBUG(GwLog::DEBUG,"create serial: channel=%d, rx=%d,tx=%d", idx,rx,tx); ChannelParam *param=findChannelParam(idx); @@ -325,6 +345,24 @@ static GwSerial * createSerialImpl(GwConfigHandler *config,GwLog *logger, int id LOG_DEBUG(GwLog::ERROR,"invalid serial config with id %d",param->id); return nullptr; } + if (ena >= 0){ + if (type == GWSERIAL_TYPE_UNI){ + String cfgMode=config->getString(param->direction); + int value=0; + if (cfgMode == "send"){ + value=elow?0:1; + } + else{ + value=elow?1:0; + } + LOG_DEBUG(GwLog::LOG,"serial %d: setting output enable %d to %d",param->id,ena,value); + pinMode(ena,OUTPUT); + digitalWrite(ena,value); + } + else{ + LOG_DEBUG(GwLog::ERROR,"serial %d: output enable ignored for mode %d",param->id, type); + } + } serialStream->begin(config->getInt(param->baud,115200),SERIAL_8N1,rx,tx); if (setLog){ logger->setWriter(new GwSerialLog(serialStream,config->getBool(param->preventLog,false))); @@ -446,7 +484,7 @@ void GwChannelList::begin(bool fallbackSerial){ //new serial config handling for (auto &&init:serialInits){ LOG_INFO("creating serial channel %d, rx=%d,tx=%d,type=%d",init.serial,init.rx,init.tx,init.mode); - GwSerial *ser=createSerialImpl(config,logger,init.serial,init.mode,init.rx,init.tx,false); + GwSerial *ser=createSerialImpl(config,logger,init.serial,init.mode,init.rx,init.tx,false,init.ena,init.elow); if (ser != nullptr){ channel=createChannel(logger,config,init.serial,ser); if (channel != nullptr){ diff --git a/webinstall/build.yaml b/webinstall/build.yaml index ab2ca3c..61ddf19 100644 --- a/webinstall/build.yaml +++ b/webinstall/build.yaml @@ -339,6 +339,26 @@ types: help: 'number of the GPIO pin for the transmit function' target: "define:#serial#TX" mandatory: true + - &serialEnablePin + <<: *gpiopin + key: ENA + label: "enable pin" + help: "GPIO pin for output enable" + target: "define:#serial#ENA" + mandatory: false + - &serialEnableLow + type: checkbox + key: ELOW + label: "enable low" + target: "define:#serial#ELO" + default: true + help: "set: low on enable pin for output, unset: high on enable pin for output" + values: + - key: true + value: 1 + - key: false + value: 0 + - &serialValues - key: true children: @@ -355,6 +375,8 @@ types: children: - *serialRX - *serialTX + - *serialEnablePin + - *serialEnableLow - key: bi value: 2 label: "BiDir" From 68239f6199d6c2934c85c1c721889221fd633490 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Mon, 29 Sep 2025 19:34:35 +0200 Subject: [PATCH 040/121] add fixed baud to cibuild, allow enable pin also for pure rx/tx serial --- lib/channel/GwChannelList.cpp | 25 +++++++++++++++++-------- webinstall/build.yaml | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/lib/channel/GwChannelList.cpp b/lib/channel/GwChannelList.cpp index d89f227..cc70ecc 100644 --- a/lib/channel/GwChannelList.cpp +++ b/lib/channel/GwChannelList.cpp @@ -346,15 +346,23 @@ static GwSerial * createSerialImpl(GwConfigHandler *config,GwLog *logger, int id return nullptr; } if (ena >= 0){ + int value=-1; if (type == GWSERIAL_TYPE_UNI){ - String cfgMode=config->getString(param->direction); - int value=0; - if (cfgMode == "send"){ + String cfgMode=config->getString(param->direction); + if (cfgMode == "send"){ value=elow?0:1; - } - else{ - value=elow?1:0; - } + } + else{ + value=elow?1:0; + } + } + if (type == GWSERIAL_TYPE_RX){ + value=elow?1:0; + } + if (type == GWSERIAL_TYPE_TX){ + value=elow?0:1; + } + if (value >= 0){ LOG_DEBUG(GwLog::LOG,"serial %d: setting output enable %d to %d",param->id,ena,value); pinMode(ena,OUTPUT); digitalWrite(ena,value); @@ -483,7 +491,8 @@ void GwChannelList::begin(bool fallbackSerial){ //new serial config handling for (auto &&init:serialInits){ - LOG_INFO("creating serial channel %d, rx=%d,tx=%d,type=%d",init.serial,init.rx,init.tx,init.mode); + LOG_INFO("creating serial channel %d, rx=%d,tx=%d,type=%d fixedBaud=%d ena=%d elow=%d", + init.serial,init.rx,init.tx,init.mode,init.fixedBaud,init.ena,init.elow); GwSerial *ser=createSerialImpl(config,logger,init.serial,init.mode,init.rx,init.tx,false,init.ena,init.elow); if (ser != nullptr){ channel=createChannel(logger,config,init.serial,ser); diff --git a/webinstall/build.yaml b/webinstall/build.yaml index 61ddf19..4085704 100644 --- a/webinstall/build.yaml +++ b/webinstall/build.yaml @@ -326,6 +326,24 @@ types: - PPIN23 - PPIN25 - PPIN33 + + - &baudselect + type: dropdown + help: 'Select the baud rate' + values: + - {label: unset,value:} + - 1200 + - 2400 + - 4800 + - 9600 + - 14400 + - 19200 + - 28800 + - 38400 + - 57600 + - 115200 + - 230400 + - 460800 - &serialRX <<: *gpioinput @@ -358,6 +376,13 @@ types: value: 1 - key: false value: 0 + + - &serialFixedBaud + <<: *baudselect + key: fixedBaud + label: "fixed baud" + help: "you can set a fixed baud rate here, this disables changing the baud rate in the UI" + target: "define:#serial#BAUD" - &serialValues - key: true @@ -377,6 +402,7 @@ types: - *serialTX - *serialEnablePin - *serialEnableLow + - *serialFixedBaud - key: bi value: 2 label: "BiDir" @@ -385,18 +411,25 @@ types: children: - *serialRX - *serialTX + - *serialFixedBaud - key: rx value: 3 label: "RX" description: "Input only" children: - *serialRX + - *serialEnablePin + - *serialEnableLow + - *serialFixedBaud - key: tx value: 1 label: "TX" description: "output only" children: - *serialTX + - *serialEnablePin + - *serialEnableLow + - *serialFixedBaud - &serial1 type: checkbox label: 'Serial 1' From 3cd508a2391a2952873edeff280ef5e779883d9c Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Tue, 30 Sep 2025 12:13:06 +0200 Subject: [PATCH 041/121] better description of pins for cibuild --- webinstall/build.yaml | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/webinstall/build.yaml b/webinstall/build.yaml index 4085704..3590c59 100644 --- a/webinstall/build.yaml +++ b/webinstall/build.yaml @@ -320,13 +320,22 @@ types: - &protogpio - {label: unset,value:} - - PPIN19 - - PPIN21 - - PPIN22 - - PPIN23 - - PPIN25 - - PPIN33 - + - {label: "PPIN19(left-2,gpio 19)", value: PPIN19} + - {label: "PPIN21(right-1,gpio 21)", value: PPIN21} + - {label: "PPIN22(left-1,gpio 22)", value: PPIN22} + - {label: "PPIN23(left-3,gpio 23)", value: PPIN23} + - {label: "PPIN25(right-2,gpio 25)", value: PPIN25} + - {label: "PPIN33(left-4,gpio 33)", value: PPIN33} + + - &protogpios3 + - {label: unset,value:} + - {label: "PPIN19(left-2,gpio 6)", value: PPIN19} + - {label: "PPIN21(right-1,gpio 39)", value: PPIN21} + - {label: "PPIN22(left-1,gpio 5)", value: PPIN22} + - {label: "PPIN23(left-3,gpio 7)", value: PPIN23} + - {label: "PPIN25(right-2,gpio 38)", value: PPIN25} + - {label: "PPIN33(left-4,gpio 8)", value: PPIN33} + - &baudselect type: dropdown help: 'Select the baud rate' @@ -369,7 +378,7 @@ types: key: ELOW label: "enable low" target: "define:#serial#ELO" - default: true + default: false help: "set: low on enable pin for output, unset: high on enable pin for output" values: - key: true @@ -774,8 +783,8 @@ types: url: "https://docs.m5stack.com/en/atom/atomhub" label: "Hub Proto" base: - gpioinputv: *protogpio - gpiopinv: *protogpio + gpioinputv: "#protogpio#" + gpiopinv: "#protogpio#" children: *m5protochildren - value: M5_PORTABC @@ -802,6 +811,7 @@ config: gpiopinv: *gpiopinv gpioinputv: *gpioinputv grv: "" + protogpio: *protogpio values: - value: m5stack-atom-generic label: m5stack-atom @@ -816,6 +826,8 @@ config: description: "M5 Stack AtomS3 light" url: "http://docs.m5stack.com/en/core/AtomS3%20Lite" resource: *esp32default + base: + protogpio: *protogpios3 children: - *m5base - *m5groove @@ -830,7 +842,7 @@ config: - value: nodemcu-generic label: nodemcu - description: "Node mcu esp32" + description: "Node mcu esp32,4MB flash, no PSRAM" url: "https://docs.platformio.org/en/stable/boards/espressif32/nodemcu-32s.html" resource: *esp32default children: @@ -864,6 +876,7 @@ config: base: gpiopinv: *gpiopinvs3 gpioinputv: *gpiopinvs3 + protogpio: *protogpios3 children: - *serial1 - *serial2 From 034a338a819a39c9cf939e0a7091d8a10c9ae187 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Tue, 30 Sep 2025 12:16:51 +0200 Subject: [PATCH 042/121] set default for serial enable low = 0 --- lib/channel/GwChannelList.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/channel/GwChannelList.cpp b/lib/channel/GwChannelList.cpp index cc70ecc..8a85863 100644 --- a/lib/channel/GwChannelList.cpp +++ b/lib/channel/GwChannelList.cpp @@ -53,7 +53,7 @@ static int typeFromMode(const char *mode){ #define GWSERIAL_ENA -1 #endif #ifndef GWSERIAL_ELO -#define GWSERIAL_ELO 1 +#define GWSERIAL_ELO 0 #endif #ifndef GWSERIAL_BAUD #define GWSERIAL_BAUD -1 @@ -76,7 +76,7 @@ CFG_SERIAL(SERIAL1_CHANNEL_ID, GWSERIAL_RX, GWSERIAL_TX, typeFromMode(GWSERIAL_M #define GWSERIAL2_ENA -1 #endif #ifndef GWSERIAL2_ELO -#define GWSERIAL2_ELO 1 +#define GWSERIAL2_ELO 0 #endif #ifndef GWSERIAL2_BAUD #define GWSERIAL2_BAUD -1 From 5493c9695cef9f2bb2db0ec0dc6ae828b907ba56 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Tue, 30 Sep 2025 15:27:50 +0200 Subject: [PATCH 043/121] add buildname to cibuild, use the firmware name for file names --- .github/workflows/release.yml | 2 +- lib/appinfo/GwAppInfo.h | 5 ++++- post.py | 8 ++++++-- src/main.cpp | 6 +++--- webinstall/build.yaml | 7 +++++++ webinstall/cibuild.js | 34 +++++++++++++++++++++++++++++++++- 6 files changed, 54 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2aeae17..ea089d5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,5 +62,5 @@ jobs: with: repo_token: ${{ secrets.GITHUB_TOKEN }} tag: ${{ steps.version.outputs.version}} - file: ./.pio/build/*/*-{all,update}.bin + file: ./.pio/build/*/*${{ steps.version.outputs.version }}*-{all,update}.bin file_glob: true diff --git a/lib/appinfo/GwAppInfo.h b/lib/appinfo/GwAppInfo.h index f7eb143..0ccf711 100644 --- a/lib/appinfo/GwAppInfo.h +++ b/lib/appinfo/GwAppInfo.h @@ -14,6 +14,9 @@ #define LOGLEVEL GwLog::DEBUG #endif #endif - +#ifdef GWBUILD_NAME +#define FIRMWARE_TYPE GWSTRINGIFY(GWBUILD_NAME) +#else #define FIRMWARE_TYPE GWSTRINGIFY(PIO_ENV_BUILD) +#endif #define IDF_VERSION GWSTRINGIFY(ESP_IDF_VERSION_MAJOR) "." GWSTRINGIFY(ESP_IDF_VERSION_MINOR) "." GWSTRINGIFY(ESP_IDF_VERSION_PATCH) \ No newline at end of file diff --git a/post.py b/post.py index 8fe2f27..a09635f 100644 --- a/post.py +++ b/post.py @@ -2,6 +2,7 @@ Import("env", "projenv") import os import glob import shutil +import re print("##post script running") HDROFFSET=288 @@ -39,6 +40,7 @@ def post(source,target,env): appoffset=env.subst("$ESP32_APP_OFFSET") firmware=env.subst("$BUILD_DIR/${PROGNAME}.bin") (fwname,version)=getFirmwareInfo(firmware) + fwname=re.sub(r"[^0-9A-Za-z_.-]*","",fwname) print("found fwname=%s, fwversion=%s"%(fwname,version)) python=env.subst("$PYTHONEXE") print("base=%s,esptool=%s,appoffset=%s,uploaderflags=%s"%(base,esptool,appoffset,uploaderflags)) @@ -70,10 +72,12 @@ def post(source,target,env): print("running %s"%" ".join(cmd)) env.Execute(" ".join(cmd),"#testpost") ofversion="-"+version - versionedFile=os.path.join(outdir,"%s%s-update.bin"%(base,ofversion)) + versionedFile=os.path.join(outdir,"%s%s-update.bin"%(fwname,ofversion)) shutil.copyfile(firmware,versionedFile) - versioneOutFile=os.path.join(outdir,"%s%s-all.bin"%(base,ofversion)) + print(f"wrote {versionedFile}") + versioneOutFile=os.path.join(outdir,"%s%s-all.bin"%(fwname,ofversion)) shutil.copyfile(outfile,versioneOutFile) + print(f"wrote {versioneOutFile}") env.AddPostAction( "$BUILD_DIR/${PROGNAME}.bin", post diff --git a/src/main.cpp b/src/main.cpp index de56930..44c715f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -72,9 +72,9 @@ const unsigned long HEAP_REPORT_TIME=2000; //set to 0 to disable heap reporting #define MAX_NMEA2000_MESSAGE_SEASMART_SIZE 500 #define MAX_NMEA0183_MESSAGE_SIZE MAX_NMEA2000_MESSAGE_SEASMART_SIZE //assert length of firmware name and version -CASSERT(strlen(FIRMWARE_TYPE) <= 32, "environment name (FIRMWARE_TYPE) must not exceed 32 chars"); -CASSERT(strlen(VERSION) <= 32, "VERSION must not exceed 32 chars"); -CASSERT(strlen(IDF_VERSION) <= 32,"IDF_VERSION must not exceed 32 chars"); +CASSERT(strlen(FIRMWARE_TYPE) <= 31, "environment name (FIRMWARE_TYPE) must not exceed 32 chars"); +CASSERT(strlen(VERSION) <= 31, "VERSION must not exceed 32 chars"); +CASSERT(strlen(IDF_VERSION) <= 31,"IDF_VERSION must not exceed 32 chars"); //https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/app_image_format.html //and removed the bugs in the doc... __attribute__((section(".rodata_custom_desc"))) esp_app_desc_t custom_app_desc = { diff --git a/webinstall/build.yaml b/webinstall/build.yaml index 3590c59..672c435 100644 --- a/webinstall/build.yaml +++ b/webinstall/build.yaml @@ -803,6 +803,13 @@ resources: config: children: + - type: string + label: 'Build Name' + key: buildname + target: "define:GWBUILD_NAME" + help: "Set a name to identify your build. Will also become the name for the generated files and the firmware type in the image. Max 31 characters." + max: 31 + allowed: "0-9A-Za-z_-" - type: select target: environment label: 'Board' diff --git a/webinstall/cibuild.js b/webinstall/cibuild.js index 690da1d..b64787c 100644 --- a/webinstall/cibuild.js +++ b/webinstall/cibuild.js @@ -234,7 +234,11 @@ class PipelineInfo{ } const downloadConfig=()=>{ let name=configName; - if (isModified) name=name.replace(/[0-9]*$/,'')+formatDate(undefined,true); + const buildname=config["root:buildname"] + if (buildname && name != buildname){ + name+="-"+buildname+"-"; + } + name=name.replace(/[0-9]*$/,'')+formatDate(undefined,true); name+=".json"; fileDownload(JSON.stringify(config),name); } @@ -521,6 +525,34 @@ class PipelineInfo{ addDescription(config,inputFrame); initialConfig=expandedValues[0]; } + if (config.type === 'string'){ + let ip=addEl('input','t'+config.type,inputFrame); + addDescription(config,inputFrame); + ip.value=current?current:""; + ip.addEventListener('change',(ev)=>{ + let value=ev.target.value; + let modified=false; + if (config.max){ + if (value && value.length > config.max){ + modified=true; + value=value.substring(0,config.max); + } + } + if (config.allowed){ + let check=new RegExp("[^"+config.allowed+"]","g"); + let nv=value.replace(check,""); + if (nv != value){ + modified=true; + value=nv; + } + } + if (modified){ + ev.target.value=value; + } + callback(Object.assign({},config,{key: value,value:value}),false); + + }); + } let childFrame=addEl('div','childFrame',frame); if (initialConfig !== undefined){ callback(initialConfig,true,childFrame); From 6da87e4455a75dadc1dc11eebe0e8ec34e3d4513 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Tue, 30 Sep 2025 20:03:45 +0200 Subject: [PATCH 044/121] add buildname to ci output file names, correctly set initial build name --- webinstall/cibuild.js | 53 +++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/webinstall/cibuild.js b/webinstall/cibuild.js index b64787c..285bacc 100644 --- a/webinstall/cibuild.js +++ b/webinstall/cibuild.js @@ -167,8 +167,17 @@ class PipelineInfo{ updateStatus(); if (gitSha !== undefined) param.tag=gitSha; param.config=JSON.stringify(config); + let buildname=config['root:buildname'] + if (buildname){ + param.suffix="-"+buildname + } if (buildVersion !== undefined){ - param.suffix="-"+buildVersion; + if (param.suffix){ + param.suffix+="-"+buildVersion; + } + else{ + param.suffix="-"+buildVersion; + } } fetchJson(API,Object.assign({ api:'start'},param)) @@ -528,28 +537,32 @@ class PipelineInfo{ if (config.type === 'string'){ let ip=addEl('input','t'+config.type,inputFrame); addDescription(config,inputFrame); - ip.value=current?current:""; + const buildChild=(value)=>{ + if (value) { + if (config.max) { + if (value && value.length > config.max) { + value = value.substring(0, config.max); + } + } + if (config.allowed) { + let check = new RegExp("[^" + config.allowed + "]", "g"); + let nv = value.replace(check, ""); + if (nv != value) { + value = nv; + } + } + } + return Object.assign({},config,{key: value,value:value}); + } + initialConfig=buildChild(current); + ip.value=initialConfig.value||""; ip.addEventListener('change',(ev)=>{ let value=ev.target.value; - let modified=false; - if (config.max){ - if (value && value.length > config.max){ - modified=true; - value=value.substring(0,config.max); - } + let cbv=buildChild(value); + if (cbv.value != value){ + ev.target.value=cbv.value; } - if (config.allowed){ - let check=new RegExp("[^"+config.allowed+"]","g"); - let nv=value.replace(check,""); - if (nv != value){ - modified=true; - value=nv; - } - } - if (modified){ - ev.target.value=value; - } - callback(Object.assign({},config,{key: value,value:value}),false); + callback(cbv,false); }); } From 9211b13dcd013d89595d568fc54573388396664d Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Tue, 30 Sep 2025 20:09:13 +0200 Subject: [PATCH 045/121] #113: add env4 to cibuild --- webinstall/build.yaml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/webinstall/build.yaml b/webinstall/build.yaml index 672c435..0234010 100644 --- a/webinstall/build.yaml +++ b/webinstall/build.yaml @@ -53,16 +53,16 @@ types: - value: M5_ENV3#grv# key: true resource: qmp69881#grv#1,sht3x#grv#1 -# - label: "M5 ENV4" -# type: checkbox -# key: m5env4#grv# -# target: define -# url: "https://docs.m5stack.com/en/unit/ENV%E2%85%A3%20Unit" -# description: "M5 sensor module temperature, humidity, pressure" -# values: -# - value: M5_ENV4#grv# -# key: true -# resource: bmp280#grv#1,sht3x#grv#1 + - label: "M5 ENV4" + type: checkbox + key: m5env4#grv# + target: define + url: "https://docs.m5stack.com/en/unit/ENV%E2%85%A3%20Unit" + description: "M5 sensor module temperature, humidity, pressure" + values: + - value: M5_ENV4#grv# + key: true + resource: bmp280#grv#1,sht4x#grv#1 - type: checkbox label: SHT3X-1 description: "SHT30 temperature and humidity sensor 0x44" From fb62e41bd98302141e2492c5d7da33d3eeb8f3da Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Tue, 7 Oct 2025 12:58:28 +0200 Subject: [PATCH 046/121] prepare relase 20251007 --- Readme.md | 13 +++++++++++++ doc/Conversions.odt | Bin 26253 -> 36415 bytes doc/Conversions.pdf | Bin 38386 -> 152289 bytes lib/exampletask/Readme.md | 2 +- 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index d3fae7f..b833857 100644 --- a/Readme.md +++ b/Readme.md @@ -174,6 +174,19 @@ For details refer to the [example description](lib/exampletask/Readme.md). Changelog --------- +[20251007](../../releases/tag/20251007) +********* +* add AIS Aton translations (PGN 129041 <-> Ais class 21) +* improved mapping of AIS transducer information (NMEA2000) to AIS channel and Talker on NMEA0183 +* [#114](../../issues/114) correctly translate AIS type 1/3 from PGN 129038 +* add support for a generic S3 build in the build UI +* [#117](../../issues/117) add support for a transmit enable pin for RS 485 conections (also in the build UI) +* [#116](../../issues/116) SDA and SCL are swapped in the build UI +* [#112](../../issues/112) clearify licenses +* [#110](../../issues/110) / [#115](../../pull/115) support for the M5 GPS unit v1.1 +* [#102](../../issues/102) optimize Wifi reconnect handling +* [#111](../../pull/111) allow for a custom python build script + [20250305](../../releases/tag/20250305) ********* * better handling for reconnect to a raspberry pi after reset [#102](../../issues/102) diff --git a/doc/Conversions.odt b/doc/Conversions.odt index a44672b0fb4ff6cb0e502ef7d9ca216e493636a9..1a181ac6c931a1ce8d8631ca872315ef45c5bae9 100644 GIT binary patch delta 35065 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 RA%T#N$q8xdY)3jlVgQ_O28RFu literal 26253 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 diff --git a/doc/Conversions.pdf b/doc/Conversions.pdf index 3ed26e12f99dc9cde4f31bbe9916a6eb3ae92375..0361c69f5f609bb3fcb2422aa77c955eb25d7368 100644 GIT binary patch literal 152289 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 z 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 delta 11753 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 diff --git a/lib/exampletask/Readme.md b/lib/exampletask/Readme.md index 7c701fb..3220242 100644 --- a/lib/exampletask/Readme.md +++ b/lib/exampletask/Readme.md @@ -58,7 +58,7 @@ Files Starting from Version 20250305 you should normally not use this file name any more as those styles would be added for all build environments. Instead define a parameter _custom_css_ in your [platformio.ini](platformio.ini) for the environments you would like to add some styles for. This parameter accepts a list of file names (relative to the project root, separated by , or as multi line entry) * [script.py](script.py)
- Starting from version 202509xx you can define a parameter "custom_script" in your [platformio.ini](platformio.ini). + Starting from version 20251007 you can define a parameter "custom_script" in your [platformio.ini](platformio.ini). This parameter can contain a list of file names (relative to the project root) that will be added as a [platformio extra script](https://docs.platformio.org/en/latest/scripting/index.html#scripting). The scripts will be loaded at the end of the main [extra_script](../../extra_script.py). You can add code there that is specific for your build. Example: From 18b46ae5a0f5f2c5f148ffbb536808d7a86e43e2 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Tue, 7 Oct 2025 13:00:28 +0200 Subject: [PATCH 047/121] prepare relase 20251007 --- Readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Readme.md b/Readme.md index b833857..874e4d7 100644 --- a/Readme.md +++ b/Readme.md @@ -186,6 +186,7 @@ Changelog * [#110](../../issues/110) / [#115](../../pull/115) support for the M5 GPS unit v1.1 * [#102](../../issues/102) optimize Wifi reconnect handling * [#111](../../pull/111) allow for a custom python build script +* [#113](../../issues/113) support for M5 stack Env4 [20250305](../../releases/tag/20250305) ********* From 32099487fa4f3c27f8c4d2c8edfb3281d04b9aed Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Tue, 7 Oct 2025 13:02:19 +0200 Subject: [PATCH 048/121] prepare relase 20251007 --- Readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Readme.md b/Readme.md index 874e4d7..25eb111 100644 --- a/Readme.md +++ b/Readme.md @@ -178,6 +178,7 @@ Changelog ********* * add AIS Aton translations (PGN 129041 <-> Ais class 21) * improved mapping of AIS transducer information (NMEA2000) to AIS channel and Talker on NMEA0183 +* use a forked version of the NMEA2000 library (as an intermediate workaround) * [#114](../../issues/114) correctly translate AIS type 1/3 from PGN 129038 * add support for a generic S3 build in the build UI * [#117](../../issues/117) add support for a transmit enable pin for RS 485 conections (also in the build UI) From bcc24ee99d52803d181f4d029712431c082a12f7 Mon Sep 17 00:00:00 2001 From: Ulrich Meine Date: Fri, 17 Oct 2025 00:42:13 +0200 Subject: [PATCH 049/121] OBPcharts principle working --- lib/obp60task/OBPcharts.cpp | 412 ++++++++++++++++++++++++++++----- lib/obp60task/OBPcharts.h | 127 ++++------ lib/obp60task/PageWindPlot.cpp | 49 ++-- 3 files changed, 426 insertions(+), 162 deletions(-) diff --git a/lib/obp60task/OBPcharts.cpp b/lib/obp60task/OBPcharts.cpp index 44c782e..907712b 100644 --- a/lib/obp60task/OBPcharts.cpp +++ b/lib/obp60task/OBPcharts.cpp @@ -1,75 +1,367 @@ // Function lib for display of boat data in various chart formats #include "OBPcharts.h" +#include "OBP60Extensions.h" +#include "OBPRingBuffer.h" // --- Class Chart --------------- -void Chart::drawChrtHdr() { - // chart header label + lines - int i; - getdisplay().fillRect(0, top, cWidth, 2, commonData->fgcolor); +template +Chart::Chart(RingBuffer& dataBuf, int8_t chrtDir, int8_t chrtSz, int dfltRng, CommonData& common, bool useSimuData) + : dataBuf(dataBuf) + , chrtDir(chrtDir) + , chrtSz(chrtSz) + , dfltRng(dfltRng) + , commonData(&common) + , useSimuData(useSimuData) +{ + logger = commonData->logger; + fgColor = commonData->fgcolor; + bgColor = commonData->bgcolor; - // horizontal chart labels - getdisplay().drawLine(cStart.x, cStart.y, cWidth, cStart.y); - getdisplay().fillRect(cStart.x, cStart.y, cWidth, 2, commonData->fgcolor); + LOG_DEBUG(GwLog::DEBUG, "Chart create: dataBuf: %p", (void*)&dataBuf); + dWidth = getdisplay().width(); + dHeight = getdisplay().height(); - getdisplay().setFont(&Ubuntu_Bold10pt8b); - getdisplay().setCursor(cStart.x, cStart.y + 12); - getdisplay().print(dbName); // Wind data name + if (chrtDir == 0) { + // horizontal chart timeline direction + timAxis = dWidth - xOffset; + switch (chrtSz) { + case 0: + valAxis = dHeight - top - bottom; + cStart = { xOffset, top }; + break; + case 1: + valAxis = (dHeight - top - bottom) / 2 - gap; + cStart = { xOffset, top }; + break; + case 2: + valAxis = (dHeight - top - bottom) / 2 - gap; + cStart = { xOffset, top + (valAxis + gap) + gap }; + break; + default: + LOG_DEBUG(GwLog::DEBUG, "displayChart: wrong parameter"); + return; + } + } else if (chrtDir == 1) { + // vertical chart timeline direction + timAxis = dHeight - top - bottom; + switch (chrtSz) { + case 0: + valAxis = dWidth - xOffset; + cStart = { xOffset, top }; + break; + case 1: + valAxis = dWidth / 2 - gap; + cStart = { 0, top }; + break; + case 2: + valAxis = dWidth / 2 - gap; + cStart = { dWidth / 2 + gap, top }; + break; + default: + LOG_DEBUG(GwLog::DEBUG, "displayChart: wrong parameter"); + return; + } + } else { + LOG_DEBUG(GwLog::DEBUG, "displayChart: wrong parameter"); + return; + } + // xCenter = timAxis / 2; - getdisplay().setFont(&Ubuntu_Bold12pt8b); - if (chrtSze == 0) { - i = -1 * (chrtIntv / 8 - 2); - } else { - i = -1 * (chrtIntv / 4 - 2); - } - for (int j = 50; j <= (cWidth - 50); j += 50 ) { - getdisplay().setCursor(cStart.x + j - 16, cStart.y + 12); - getdisplay().print(i++); // time interval - // i++; - getdisplay().drawLine(cStart.x + j - 30, cStart.y, cStart.x - 30, cHeight + top); - } + dataBuf.getMetaData(dbName, dbFormat); + dbMAX_VAL = dataBuf.getMaxVal(); + bufSize = dataBuf.getCapacity(); + LOG_DEBUG(GwLog::DEBUG, "Chart create: dWidth: %d, dHeight: %d, timAxis: %d, valAxis: %d, cStart {x,y}: %d, %d, dbname: %s", dWidth, dHeight, timAxis, valAxis, cStart.x, cStart.y, dbName); +}; + +template +Chart::~Chart() +{ } -void Chart::drawChrtGrd(const int chrtRng) { - // chart Y axis labels + lines - int i; +// chart time axis label + lines +template +void Chart::drawChrtTimeAxis(int8_t chrtIntv) +{ + int timeRng; + float slots, intv, i; + char sTime[6]; - getdisplay().setFont(&Ubuntu_Bold8pt8b); - if (chrtDir == 0) { - i = -1 * (chrtRng / 4 - 2); - for (int j = cStart.x; j <= (cHeight - (cHeight / 4)); j += cHeight / 4 ) { - getdisplay().drawLine(0, cStart.y, cWidth, cStart.y); - getdisplay().setCursor(0, cStart.y + 12); - if (i < 10) - getdisplay().printf("!!%1d", i); // Range value - else if (i < 100) - getdisplay().printf("!%2d", i); // Range value - else - getdisplay().printf("%3d", i); // Range value - i += (chrtRng / 4); - } - } else { - i = -1 * (chrtRng / 8 - 2); - for (int j = cStart.x; j <= (cHeight - (cHeight / 8)); j += cHeight / 8 ) { - getdisplay().drawLine(cStart.x + j - 30, cStart.y, cStart.x - 30, cHeight + top); - getdisplay().setCursor(0, cStart.y + 12); - if (i < 10) - getdisplay().printf("!!%1d", i); // Range value - else if (i < 100) - getdisplay().printf("!%2d", i); // Range value - else - getdisplay().printf("%3d", i); // Range value - i += (chrtRng / 4); - } - } + getdisplay().setTextColor(fgColor); + if (chrtDir == 0) { // horizontal chart + getdisplay().fillRect(0, top, dWidth, 2, fgColor); + getdisplay().setFont(&Ubuntu_Bold8pt8b); + + timeRng = chrtIntv * 4; // Chart time interval: [1] 4 min., [2] 8 min., [3] 12 min., [4] 16 min., [8] 32 min. + slots = (timAxis - xOffset) / 75.0; // number of axis labels + intv = timeRng / slots; // minutes per chart axis interval + i = timeRng; // Chart axis label start at -32, -16, -12, ... minutes + + for (int j = 0; j < timAxis - 30; j += 75) { + LOG_DEBUG(GwLog::DEBUG, "ChartHdr: timAxis: %d, {x,y}: {%d,%d}, i: %.1f, j: %d, chrtIntv: %d, intv: %.1f, slots: %.1f", timAxis, cStart.x, cStart.y, i, j, chrtIntv, intv, slots); + if (chrtIntv < 3) { + snprintf(sTime, size_t(sTime), "-%.1f", i); + drawTextCenter(cStart.x + j - 8, cStart.y - 8, sTime); // time interval + } else { + snprintf(sTime, size_t(sTime), "-%.0f", std::round(i)); + drawTextCenter(cStart.x + j - 4, cStart.y - 8, sTime); // time interval + } + getdisplay().drawLine(cStart.x + j, cStart.y, cStart.x + j, cStart.y + 5, fgColor); + i -= intv; + } + /* getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(timAxis - 8, cStart.y - 2); + getdisplay().print("min"); */ + + } else { // chrtDir == 1; vertical chart + getdisplay().setFont(&Ubuntu_Bold8pt8b); + timeRng = chrtIntv * 4; // Chart time interval: [1] 4 min., [2] 8 min., [3] 12 min., [4] 16 min., [8] 32 min. + slots = timAxis / 75.0; // number of axis labels + intv = timeRng / slots; // minutes per chart axis interval + i = 0; // Chart axis label start at -32, -16, -12, ... minutes + + for (int j = 0; j < (timAxis - 75); j += 75) { // don't print time label at lower end + LOG_DEBUG(GwLog::DEBUG, "ChartHdr: timAxis: %d, {x,y}: {%d,%d}, i: %.1f, j: %d, chrtIntv: %d, intv: %.1f, slots: %.1f", timAxis, cStart.x, cStart.y, i, j, chrtIntv, intv, slots); + if (chrtIntv < 3) { // print 1 decimal if time range is single digit (4 or 8 minutes) + snprintf(sTime, size_t(sTime), "%.1f", i * -1); + } else { + snprintf(sTime, size_t(sTime), "%.0f", std::round(i) * -1); + } + drawTextCenter(dWidth / 2, cStart.y + j, sTime); // time value + getdisplay().drawLine(cStart.x, cStart.y + j, cStart.x + valAxis, cStart.y + j, fgColor); // Grid line + i += intv; + } + } } -bool Chart::drawChrt(int8_t chrtIntv, int dfltRng) { -// hstryBuf = buffer to display -// bValue = present value to display additionally to chart -// chrtDir = chart direction: [0] = vertical, [1] = horizontal -// chrtSze = chart size: [0] = full size, [1] = half size left half/top, [2] half size right half/bottom -// chrtIntv = chart time interval: [1] 4 min., [2] 8 min., [3] 12 min., [4] 16 min., [5] 32 min. -// dfltRng = default range of chart, e.g. 30 = [0..30] +// chart value axis labels + lines +template +void Chart::drawChrtValAxis() +{ + float slots; + int i, intv; + char sVal[6]; + getdisplay().setFont(&Ubuntu_Bold10pt8b); + if (chrtDir == 0) { // horizontal chart + slots = valAxis / 60.0; // number of axis labels + intv = static_cast(round(chrtRng / slots)); + i = intv; + for (int j = 60; j < valAxis - 30; j += 60) { + LOG_DEBUG(GwLog::DEBUG, "ChartGrd: chrtRng: %d, intv: %d, slots: %.1f, valAxis: %d, i: %d, j: %d", chrtRng, intv, slots, valAxis, i, j); + getdisplay().fillRect(cStart.x - xOffset, cStart.y + j - 9, cStart.x - xOffset + 28, 12, bgColor); // Clear small area to remove potential chart lines + String sVal = String(static_cast(round(i))); + getdisplay().setCursor((3 - sVal.length()) * 9, cStart.y + j + 4); // value right-formated + getdisplay().printf("%s", sVal); // Range value + i += intv; + getdisplay().drawLine(cStart.x + 2, cStart.y + j, cStart.x + timAxis, cStart.y + j, fgColor); + } + getdisplay().setFont(&Ubuntu_Bold12pt8b); + drawTextRalign(cStart.x + timAxis, cStart.y - 3, dataBuf.getName()); // buffer data name + + } else { // chrtDir == 1; vertical chart + getdisplay().fillRect(cStart.x, top, valAxis, 2, fgColor); // top chart line + getdisplay().setCursor(cStart.x, cStart.y - 2); + snprintf(sVal, sizeof(sVal), "%d", dataBuf.getMin(numBufVals) / 1000); + getdisplay().printf("%s", sVal); // Range low end + snprintf(sVal, sizeof(sVal), "%.0f", round(chrtRng / 2)); + drawTextCenter(cStart.x + (valAxis / 2), cStart.y - 10, sVal); // Range mid end + snprintf(sVal, sizeof(sVal), "%.0f", round(chrtRng)); + drawTextRalign(cStart.x + valAxis - 1, cStart.y - 2, sVal); // Range high end + for (int j = 0; j <= valAxis; j += (valAxis / 2)) { + getdisplay().drawLine(cStart.x + j - 1, cStart.y, cStart.x + j - 1, cStart.y + timAxis, fgColor); + } + getdisplay().setFont(&Ubuntu_Bold12pt8b); + drawTextCenter(cStart.x + (valAxis / 4) + 4, cStart.y - 11, dataBuf.getName()); // buffer data name + LOG_DEBUG(GwLog::DEBUG, "ChartGrd: chrtRng: %d, intv: %d, slots: %.1f, valAxis: %d, i: %d, sVal.length: %d", chrtRng, intv, slots, valAxis, i, sizeof(sVal)); + } } + +// draw chart +template +void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue currValue) +{ + float chrtScl; // Scale for data values in pixels per value + int chrtVal; // Current data value + static int chrtPrevVal; // Last data value in chart area + bool bufDataValid = false; // Flag to indicate if buffer data is valid + static int numNoData; // Counter for multiple invalid data values in a row + // GwApi::BoatValue currValue; // temporary boat value to display current data buffer value + + int x, y; // x and y coordinates for drawing + static int prevX, prevY; // Last x and y coordinates for drawing + + // Identify buffer size and buffer start position for chart + count = dataBuf.getCurrentSize(); + currIdx = dataBuf.getLastIdx(); + numAddedBufVals = (currIdx - lastAddedIdx + bufSize) % bufSize; // Number of values added to buffer since last display + if (chrtIntv != oldChrtIntv || count == 1) { + // new data interval selected by user; this is only x * 230 values instead of 240 seconds (4 minutes) per interval step + intvBufSize = timAxis * chrtIntv; + numBufVals = min(count, (timAxis - 60) * chrtIntv); + bufStart = max(0, count - numBufVals); + lastAddedIdx = currIdx; + oldChrtIntv = chrtIntv; + } else { + numBufVals = numBufVals + numAddedBufVals; + lastAddedIdx = currIdx; + if (count == bufSize) { + bufStart = max(0, bufStart - numAddedBufVals); + } + } + + calcChrtRng(); + chrtScl = float(valAxis) / float(chrtRng); // Chart scale: pixels per value step + + // Do we have valid buffer data? + if (dataBuf.getMax() == dbMAX_VAL) { + // only values in buffer -> no valid wind data available + bufDataValid = false; + } else if (!currValue.valid && !useSimuData) { + // currently no valid boat data available and no simulation mode + numNoData++; + bufDataValid = true; + if (numNoData > 3) { + // If more than 4 invalid values in a row, send message + bufDataValid = false; + } + } else { + numNoData = 0; // reset data error counter + bufDataValid = true; // At least some wind data available + } + + // Draw wind values in chart + //*********************************************************************** + if (bufDataValid) { + for (int i = 0; i < (numBufVals / chrtIntv); i++) { + chrtVal = static_cast(dataBuf.get(bufStart + (i * chrtIntv))); // show the latest wind values in buffer; keep 1st value constant in a rolling buffer + if (chrtVal == dbMAX_VAL) { + chrtPrevVal = dbMAX_VAL; + } else { + chrtVal = static_cast((chrtVal / 1000.0) + 0.5); // Convert to real value and round + if (chrtDir == 0) { // horizontal chart + x = cStart.x + i; // Position in chart area + y = cStart.y + (chrtVal * chrtScl); // value + } else { // vertical chart + x = cStart.x + (chrtVal * chrtScl); // value + y = cStart.y + timAxis - i; // Position in chart area + } + if (i >= (numBufVals / chrtIntv) - 10) // log chart data of 1 line (adjust for test purposes) + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Chart: i: %d, chrtVal: %d, {x,y} {%d,%d}", i, chrtVal, x, y); + + if ((i == 0) || (chrtPrevVal == dbMAX_VAL)) { + // just a dot for 1st chart point or after some invalid values + prevX = x; + prevY = y; + } + // Draw line with 2 pixels width + make sure vertical lines are drawn correctly + if (chrtDir == 0 || x == prevX) { // vertical line + getdisplay().drawLine(prevX, prevY, x, y, fgColor); + getdisplay().drawLine(prevX - 1, prevY, x - 1, y, fgColor); + } else if (chrtDir == 1 || x != prevX) { // line with some horizontal trend -> normal state + getdisplay().drawLine(prevX, prevY, x, y, fgColor); + getdisplay().drawLine(prevX, prevY - 1, x, y - 1, fgColor); + } + chrtPrevVal = chrtVal; + prevX = x; + prevY = y; + } + // Reaching chart area bottom end + if (i >= timAxis - 1) { + oldChrtIntv = 0; // force reset of buffer start and number of values to show in next display loop + break; + } + } + + // drawChrtValAxis(); + + // uses BoatValue temp variable to format latest buffer value + // doesn't work unfortunately when 'simulation data' is active, because OBP60Formatter generates own simulation value in that case + uint16_t lastVal = dataBuf.getLast(); + currValue.value = lastVal / 1000.0; + currValue.valid = (static_cast(lastVal) != dbMAX_VAL); + LOG_DEBUG(GwLog::DEBUG, "Chart drawChrt: lastVal: %d, currValue-value: %.1f, Valid: %d, Name: %s, Address: %p", lastVal, currValue.value, + currValue.valid, currValue.getName(), (void*)&currValue); + prntCurrValue(&currValue, { x, y }); + + } else { + // No valid data available + LOG_DEBUG(GwLog::LOG, "PageWindPlot: No valid data available"); + getdisplay().setFont(&Ubuntu_Bold10pt8b); + int pX, pY; + if (chrtDir == 0) { + pX = dWidth / 2; + pY = cStart.y + (valAxis / 2) - 10; + } else { + pX = valAxis / 2; + pY = cStart.y + (timAxis / 2) - 10; + } + getdisplay().fillRect(pX - 33, pY - 10, 66, 24, commonData->bgcolor); // Clear area for message + drawTextCenter(pX, pY, "No data"); + } + + drawChrtValAxis(); +} + +// Print current data value +template +void Chart::prntCurrValue(GwApi::BoatValue* currValue, const Pos chrtPos) +{ + int currentZone; + static int lastZone = 0; + static bool flipVal = false; + int xPosVal; + static const int yPosVal = (chrtDir == 0) ? cStart.y + valAxis - 5 : cStart.y + timAxis - 5; + + // flexible move of location for latest boat data value, in case chart data is printed at the current location + /* xPosVal = flipVal ? 8 : valAxis - 135; + currentZone = (chrtPos.y >= yPosVal - 32) && (chrtPos.y <= yPosVal + 6) && (chrtPos.x >= xPosVal - 4) && (chrtPos.x <= xPosVal + 146) ? 1 : 0; // Define current zone for data value + if (currentZone != lastZone) { + // Only flip when x moves to a different zone + if ((chrtPos.y >= yPosVal - 32) && (chrtPos.y <= yPosVal + 6) && (chrtPos.x >= xPosVal - 3) && (chrtPos.x <= xPosVal + 146)) { + flipVal = !flipVal; + xPosVal = flipVal ? 8 : valAxis - 135; + } + } + lastZone = currentZone; */ + + xPosVal = (chrtDir == 0) ? cStart.x + timAxis - 117 : cStart.x + valAxis - 117; + FormattedData frmtDbData = formatValue(currValue, *commonData); + double testdbValue = frmtDbData.value; + String sdbValue = frmtDbData.svalue; // value (string) + String dbUnit = frmtDbData.unit; // Unit of value + LOG_DEBUG(GwLog::DEBUG, "Chart CurrValue: dbValue: %.2f, sdbValue: %s, fmrtDbValue: %.2f, dbFormat: %s, dbUnit: %s, Valid: %d, Name: %s, Address: %p", currValue->value, sdbValue, + testdbValue, currValue->getFormat(), dbUnit, currValue->valid, currValue->getName(), (void*)currValue); + getdisplay().fillRect(xPosVal - 3, yPosVal - 34, 118, 40, bgColor); // Clear area for TWS value + getdisplay().setFont(&DSEG7Classic_BoldItalic16pt7b); + getdisplay().setCursor(xPosVal, yPosVal); + if (useSimuData) + getdisplay().printf("%2.1f", currValue->value); // Value + else + getdisplay().print(sdbValue); // Value + // getdisplay().setFont(&Ubuntu_Bold12pt8b); + // getdisplay().setCursor(xPosVal + 76, yPosVal - 14); + // getdisplay().print(dbName); // Name + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(xPosVal + 76, yPosVal + 1); + getdisplay().print(dbUnit); // Unit +} + +// check and adjust chart range +template +void Chart::calcChrtRng() +{ + int diffRng; + + diffRng = dataBuf.getMax(numBufVals) / 1000; + if (diffRng > chrtRng) { + chrtRng = int((diffRng + (diffRng >= 0 ? 9 : -1)) / 10) * 10; // Round up to next 10 value + } else if (diffRng + 10 < chrtRng) { // Reduce chart range for higher resolution if possible + chrtRng = max(dfltRng, int((diffRng + (diffRng >= 0 ? 9 : -1)) / 10) * 10); + } + LOG_DEBUG(GwLog::DEBUG, "Chart Range: diffRng: %d, chrtRng: %d, Min: %.0f, Max: %.0f", diffRng, chrtRng, dataBuf.getMin(numBufVals) / 1000, dataBuf.getMax(numBufVals) / 1000); +} + +// Explicitly instantiate class with required data types to avoid linker errors +template class Chart; +template class Chart; // --- Class Chart --------------- diff --git a/lib/obp60task/OBPcharts.h b/lib/obp60task/OBPcharts.h index 813c2da..2494213 100644 --- a/lib/obp60task/OBPcharts.h +++ b/lib/obp60task/OBPcharts.h @@ -1,103 +1,62 @@ // Function lib for display of boat data in various chart formats #pragma once -#include "OBP60Extensions.h" +#include +#include #include "Pagedata.h" -// #include "OBPDataOperations.h" -// #include "OBPRingBuffer.h" -struct Point { +struct Pos { int x; int y; }; +template class RingBuffer; +class GwLog; +template class Chart { protected: - RingBuffer* dataBuf; // Buffer to display - GwApi::BoatValue* bValue; // Present value to display additionally to chart - int8_t chrtDir; // Chart direction: [0] = vertical, [1] = horizontal - int8_t chrtSze; // Chart size: [0] = full size, [1] = half size left/top, [2] half size right/bottom - int8_t chrtIntv; // Chart time interval: [4] 4 min., [8] 8 min., [12] 12 min., [16] 16 min., [32] 32 min. + CommonData *commonData; + GwLog *logger; + + RingBuffer &dataBuf; // Buffer to display + int8_t chrtDir; // Chart timeline direction: [0] = horizontal, [1] = vertical + int8_t chrtSz; // Chart size: [0] = full size, [1] = half size left/top, [2] half size right/bottom int dfltRng; // Default range of chart, e.g. 30 = [0..30] + uint16_t fgColor; // color code for any screen writing + uint16_t bgColor; // color code for screen background + bool useSimuData; // flag to indicate if simulation data is active int top = 48; // display top header lines int bottom = 22; // display bottom lines - int gap = 4; // gap between 2 charts; actual gap is 2x - int cWidth; - int cHeight; - Point cStart; // start point for chart area - int cLines; // number of chart lines - int xCenter; // x center point of chart + int gap = 20; // gap between 2 charts; actual gap is 2x + int xOffset = 33; // offset for horizontal axis (time/value), because of space for left vertical axis labeling + int yOffset = 10; // offset for vertical axis (time/value), because of space for top horizontal axis labeling + int dWidth; // Display width + int dHeight; // Display height + int timAxis, valAxis; // size of time and value chart axis + Pos cStart; // start point of chart area + int chrtRng; // Range of buffer values from min to max value - String dbName, dbFormat; - int16_t dbMAX_VAL; - size_t bufSize; - GwApi::BoatValue* bValue; + String dbName, dbFormat; // Name and format of data buffer + int16_t dbMAX_VAL; // Highest possible value of buffer of type -> indicates invalid value in buffer + size_t bufSize; // History buffer size: 1.920 values for 32 min. history chart + int intvBufSize; // Buffer size used for currently selected time interval + int count; // current size of buffer + int numBufVals; // number of wind values available for current interval selection + int bufStart; // 1st data value in buffer to show + int numAddedBufVals; // Number of values added to buffer since last display + size_t currIdx; // Current index in TWD history buffer + size_t lastIdx; // Last index of TWD history buffer + size_t lastAddedIdx = 0; // Last index of TWD history buffer when new data was added + int oldChrtIntv = 0; // remember recent user selection of data interval + + void calcChrtRng(); + void drawChrtValAxis(); public: - Chart(RingBuffer* dataBuf, GwApi::BoatValue* bValue, int8_t chrtDir, int8_t chrtSz, int8_t chrtIntv, int dfltRng, GwLog* logger) - : dataBuf(dataBuf) - , bValue(bValue) - , chrtDir(chrtDir) - , chrtSze(chrtSze) - , chrtIntv(chrtIntv) - , dfltRng(dfltRng) - { - cWidth = getdisplay().width(); - cHeight = getdisplay().height(); - cHeight = cHeight - top - bottom; - if (chrtDir == 0) { - // vertical chart - switch (chrtSze) { - case 0: - // default is already set - break; - case 1: - cWidth = cWidth; - cHeight = cHeight / 2 - gap; - cStart = { 30, cHeight + top }; - break; - case 2: - cWidth = cWidth; - cHeight = cHeight / 2 - gap; - cStart = { cWidth + gap, top }; - break; - default: - LOG_DEBUG(GwLog::DEBUG, "displayChart: wrong parameter"); - return; - } - } else if (chrtDir == 1) { - // horizontal chart - switch (chrtSze) { - case 0: - cStart = { 0, cHeight - bottom }; - break; - case 1: - cWidth = cWidth / 2 - gap; - cHeight = cHeight; - cStart = { 0, cHeight - bottom }; - break; - case 2: - cWidth = cWidth / 2 - gap; - cHeight = cHeight; - cStart = { cWidth + gap, cHeight - bottom }; - break; - default: - LOG_DEBUG(GwLog::DEBUG, "displayChart: wrong parameter"); - return; - } - } else { - LOG_DEBUG(GwLog::DEBUG, "displayChart: wrong parameter"); - return; - } - xCenter = cWidth / 2; - cLines = cHeight - 22; + Chart(RingBuffer& dataBuf, int8_t chrtDir, int8_t chrtSz, int dfltRng, CommonData& common, bool useSimuData); + ~Chart(); + void drawChrtTimeAxis(int8_t chrtIntv); + void drawChrt(int8_t chrtIntv, GwApi::BoatValue currValue); + void prntCurrValue(GwApi::BoatValue* currValue, Pos chrtPos); - dataBuf->getMetaData(dbName, dbFormat); - dbMAX_VAL = dataBuf->getMaxVal(); - bufSize = dataBuf->getCapacity(); - bValue->setFormat(dataBuf->getFormat()); - }; - void drawChrtHdr(); - void drawChrtGrd(const int chrtRng); - bool drawChrt(int8_t chrtIntv, int dfltRng); }; \ No newline at end of file diff --git a/lib/obp60task/PageWindPlot.cpp b/lib/obp60task/PageWindPlot.cpp index 5a92554..eb2d079 100644 --- a/lib/obp60task/PageWindPlot.cpp +++ b/lib/obp60task/PageWindPlot.cpp @@ -1,11 +1,11 @@ #if defined BOARD_OBP60S3 || defined BOARD_OBP40S3 +#include "Pagedata.h" #include "BoatDataCalibration.h" #include "OBP60Extensions.h" #include "OBPDataOperations.h" #include "OBPRingBuffer.h" #include "OBPcharts.h" -#include "Pagedata.h" #include static const double radToDeg = 180.0 / M_PI; // Conversion factor from radians to degrees @@ -117,7 +117,7 @@ public: // Key functions virtual int handleKey(int key) { - // Set chart mode TWD | TWS -> to be implemented + // Set chart mode TWD | TWS if (key == 1) { if (chrtMode == 'D') { chrtMode = 'S'; @@ -189,6 +189,9 @@ public: static String wdName, wdFormat; // Wind direction name and format static String wsName, wsFormat; // Wind speed name and format static int16_t wdMAX_VAL; // Max. value of wd history buffer, indicating invalid values + static std::unique_ptr> twsFlChart; // chart object for wind speed chart + static std::unique_ptr> twdHfChart; // chart object for wind direction chart + static std::unique_ptr> twsHfChart; // chart object for wind speed chart float wsValue; // Wind speed value in chart area String wsUnit; // Wind speed unit in chart area static GwApi::BoatValue* wsBVal = new GwApi::BoatValue("TWS"); // temp BoatValue for wind speed unit identification; required by OBP60Formater @@ -253,7 +256,7 @@ public: isInitialized = true; // Set flag to indicate that page is now initialized } - // read boat data values; TWD only for validation test, TWS for display of current value + // read boat data values; TWD/AWS only for validation test for (int i = 0; i < numBoatData; i++) { bvalue = pageData.values[i]; BDataValid[i] = bvalue->valid; @@ -280,9 +283,18 @@ public: wsBVal->setFormat(wsHstry->getFormat()); lastAddedIdx = wdHstry->getLastIdx(); + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot twsChart: *wsHstry: %p", wsHstry); + twsFlChart = std::unique_ptr>(new Chart(*wsHstry, 0, 0, 15, *commonData, useSimuData)); + twdHfChart = std::unique_ptr>(new Chart(*wdHstry, 1, 1, 15, *commonData, useSimuData)); + twsHfChart = std::unique_ptr>(new Chart(*wsHstry, 1, 2, 15, *commonData, useSimuData)); + oldShowTruW = showTruW; } + // Set display in partial refresh mode + getdisplay().setPartialWindow(0, 0, width, height); // Set partial update + getdisplay().setTextColor(commonData->fgcolor); + if (chrtMode == 'D') { // Identify buffer size and buffer start position for chart count = wdHstry->getCurrentSize(); @@ -302,8 +314,7 @@ public: bufStart = max(0, bufStart - numAddedBufVals); } } - // LOG_DEBUG(GwLog::DEBUG,"PSRAM Size: %d kByte; free: %d Byte", ESP.getPsramSize()/1024, ESP.getFreePsram()); - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Dataset: count: %d, xWD: %.1f, xWS: %.2f, xWD_valid? %d, intvBufSize: %d, numWndVals: %d, bufStart: %d, numAddedBufVals: %d, lastIdx: %d, wind source: %s", + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Dataset: count: %d, xWD: %.1f, xWS: %.2f, xWD_valid? %d, intvBufSize: %d, numWndVals: %d, bufStart: %d, numAddedBufVals: %d, lastIdx: %d, wind source: %s", count, wdHstry->getLast() / 1000.0 * radToDeg, wsHstry->getLast() / 1000.0 * 1.94384, BDataValid[0], intvBufSize, numWndVals, bufStart, numAddedBufVals, wdHstry->getLastIdx(), showTruW ? "True" : "App"); @@ -335,10 +346,6 @@ public: // Draw page //*********************************************************************** - // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, width, height); // Set partial update - getdisplay().setTextColor(commonData->fgcolor); - // chart lines getdisplay().fillRect(0, yOffset, width, 2, commonData->fgcolor); getdisplay().fillRect(xCenter, yOffset, 1, cHeight, commonData->fgcolor); @@ -348,17 +355,17 @@ public: getdisplay().setFont(&Ubuntu_Bold12pt8b); getdisplay().setCursor(xCenter - 88, yOffset - 3); getdisplay().print(wdName); // Wind data name - snprintf(sWndLbl, 4, "%03d", (wndCenter < 0) ? (wndCenter + 360) : wndCenter); + snprintf(sWndLbl, size_t(sWndLbl), "%03d", (wndCenter < 0) ? (wndCenter + 360) : wndCenter); drawTextCenter(xCenter, yOffset - 11, sWndLbl); getdisplay().drawCircle(xCenter + 25, yOffset - 17, 2, commonData->fgcolor); // symbol getdisplay().drawCircle(xCenter + 25, yOffset - 17, 3, commonData->fgcolor); // symbol getdisplay().setCursor(1, yOffset - 3); - snprintf(sWndLbl, 4, "%03d", (wndLeft < 0) ? (wndLeft + 360) : wndLeft); + snprintf(sWndLbl, size_t(sWndLbl), "%03d", (wndLeft < 0) ? (wndLeft + 360) : wndLeft); getdisplay().print(sWndLbl); // Wind left value getdisplay().drawCircle(46, yOffset - 17, 2, commonData->fgcolor); // symbol getdisplay().drawCircle(46, yOffset - 17, 3, commonData->fgcolor); // symbol getdisplay().setCursor(width - 50, yOffset - 3); - snprintf(sWndLbl, 4, "%03d", (wndRight < 0) ? (wndRight + 360) : wndRight); + snprintf(sWndLbl, size_t(sWndLbl), "%03d", (wndRight < 0) ? (wndRight + 360) : wndRight); getdisplay().print(sWndLbl); // Wind right value getdisplay().drawCircle(width - 5, yOffset - 17, 2, commonData->fgcolor); // symbol getdisplay().drawCircle(width - 5, yOffset - 17, 3, commonData->fgcolor); // symbol @@ -505,14 +512,20 @@ public: } getdisplay().printf("%3d", chrtLbl); // Wind value label } -/* } else if (chrtMode == 'S') { - wsValue = wsHstry->getLast(); - Chart twsChart(wsHstry, wsBVal, 0, 0, dataIntv, dfltRng, logger); - twsChart.drawChrtHdr(); - twsChart.drawChrtGrd(40); + } else if (chrtMode == 'S') { +// wsValue = wsHstry->getLast(); + twsFlChart->drawChrtTimeAxis(dataIntv); + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot chart: wsBVal.name: %s, format: %s, wsBVal.value: %.1f, address: %p", wsBVal->getName(), wsBVal->getFormat(), wsBVal->value, wsBVal); + twsFlChart->drawChrt(dataIntv, *wsBVal); } else if (chrtMode == 'B') { - } */ +// wsValue = wsHstry->getLast(); + twdHfChart->drawChrtTimeAxis(dataIntv); + twsHfChart->drawChrtTimeAxis(dataIntv); + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot chart: wsBVal.name: %s, format: %s, wsBVal.value: %.1f, address: %p", wsBVal->getName(), wsBVal->getFormat(), wsBVal->value, wsBVal); + twdHfChart->drawChrt(dataIntv, *wsBVal); + twsHfChart->drawChrt(dataIntv, *wsBVal); + } LOG_DEBUG(GwLog::DEBUG, "PageWindPlot time: %ld", millis() - timer); return PAGE_UPDATE; From 469a81f87dd041422817a57b8500825b2ac43cdb Mon Sep 17 00:00:00 2001 From: TobiasE-github Date: Sun, 16 Nov 2025 10:44:13 +0100 Subject: [PATCH 050/121] WindRoseFlex: less clutter, display A or T in the cetner --- lib/obp60task/PageWindRoseFlex.cpp | 33 ++++++++++++------------------ 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/lib/obp60task/PageWindRoseFlex.cpp b/lib/obp60task/PageWindRoseFlex.cpp index 69ad611..26efa28 100644 --- a/lib/obp60task/PageWindRoseFlex.cpp +++ b/lib/obp60task/PageWindRoseFlex.cpp @@ -8,7 +8,6 @@ class PageWindRoseFlex : public Page { int16_t lp = 80; // Pointer length char source = 'A'; // data source (A)pparent | (T)rue -String ssource="App."; // String for Data Source public: PageWindRoseFlex(CommonData &common){ @@ -26,10 +25,8 @@ public: // Code for set source if(source == 'A'){ source = 'T'; - ssource = "True"; // String to display } else { source = 'A'; - ssource = "App."; // String to display } } return key; // Commit the key @@ -381,12 +378,22 @@ public: } // Center circle - getdisplay().fillCircle(200, 150, startwidth + 6, commonData->bgcolor); - getdisplay().fillCircle(200, 150, startwidth + 4, commonData->fgcolor); + getdisplay().fillCircle(200, 150, startwidth + 8, commonData->bgcolor); + getdisplay().fillCircle(200, 150, startwidth + 6, commonData->fgcolor); + getdisplay().fillCircle(200, 150, startwidth + 4, commonData->bgcolor); + getdisplay().setFont(&Ubuntu_Bold10pt8b); + if (source=='A'){ + getdisplay().setCursor(193, 155); + } + else { + getdisplay().setCursor(195, 156); + } + getdisplay().print({source}); + //******************************************************************************************* - // Show value6 (=fourth user-configured parameter) and ssource, so that they do not collide with the wind pointer + // Show value6 (=fourth user-configured parameter) if ( cos(value1) > 0){ //pointer points upwards getdisplay().setFont(&DSEG7Classic_BoldItalic16pt7b); @@ -401,13 +408,6 @@ if ( cos(value1) > 0){ else{ getdisplay().print(unit6old); // Unit } - if (sin(value1)>0){ - getdisplay().setCursor(160, 130); - } - else{ - getdisplay().setCursor(220, 130); - } - getdisplay().print(ssource); // true or app. } else{ // pointer points downwards @@ -423,13 +423,6 @@ else{ else{ getdisplay().print(unit6old); // Unit } - if (sin(value1)>0){ - getdisplay().setCursor(160, 200); - } - else{ - getdisplay().setCursor(220, 200); - } - getdisplay().print(ssource); //true or app. } return PAGE_UPDATE; From dd5f05922afe0b98c0892e27cb3f375381958cff Mon Sep 17 00:00:00 2001 From: Ulrich Meine Date: Sat, 22 Nov 2025 01:32:50 +0100 Subject: [PATCH 051/121] Added to OBP60Formatter to return numerical converted value --- lib/obp60task/OBP60Formatter.cpp | 31 +++++++++++++++++++++++++++++++ lib/obp60task/Pagedata.h | 7 ++++--- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/lib/obp60task/OBP60Formatter.cpp b/lib/obp60task/OBP60Formatter.cpp index cfdcc96..e4e73e0 100644 --- a/lib/obp60task/OBP60Formatter.cpp +++ b/lib/obp60task/OBP60Formatter.cpp @@ -55,6 +55,8 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ static int dayoffset = 0; double rawvalue = 0; + result.cvalue = value->value; + // Load configuration values String stimeZone = commondata.config->getString(commondata.config->timeZone); // [UTC -14.00...+12.00] double timeZone = stimeZone.toDouble(); @@ -149,6 +151,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ val = modf(val*3600.0/60.0, &intmin); modf(val*60.0,&intsec); snprintf(buffer, bsize, "%02.0f:%02.0f:%02.0f", inthr, intmin, intsec); + result.cvalue = timeInSeconds; } else{ static long sec; @@ -158,6 +161,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ } sec = sec % 60; snprintf(buffer, bsize, "11:36:%02i", int(sec)); + result.cvalue = sec; lasttime = millis(); } if(timeZone == 0){ @@ -178,6 +182,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer, bsize, "%3.0f", rawvalue); } result.unit = ""; + result.cvalue = rawvalue; } //######################################################## else if (value->getFormat() == "formatCourse" || value->getFormat() == "formatWind"){ @@ -195,6 +200,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ // Format 3 numbers with prefix zero snprintf(buffer,bsize,"%03.0f",course); result.unit = "Deg"; + result.cvalue = course; } //######################################################## else if (value->getFormat() == "formatKnots" && (value->getName() == "SOG" || value->getName() == "STW")){ @@ -228,6 +234,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ else { snprintf(buffer, bsize, fmt_dec_100, speed); } + result.cvalue = speed; } //######################################################## else if (value->getFormat() == "formatKnots" && (value->getName() == "AWS" || value->getName() == "TWS" || value->getName() == "MaxAws" || value->getName() == "MaxTws")){ @@ -308,6 +315,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer, bsize, fmt_dec_100, speed); } } + result.cvalue = speed; } //######################################################## else if (value->getFormat() == "formatRot"){ @@ -334,6 +342,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ if (rotation <= -10 || rotation >= 10){ snprintf(buffer, bsize, "%3.0f", rotation); } + result.cvalue = rotation; } //######################################################## else if (value->getFormat() == "formatDop"){ @@ -359,6 +368,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ else { snprintf(buffer, bsize, fmt_dec_100, dop); } + result.cvalue = dop; } //######################################################## else if (value->getFormat() == "formatLatitude"){ @@ -383,6 +393,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ rawvalue = 35.0 + float(random(0, 10)) / 10000.0; snprintf(buffer, bsize, " 51\" %2.4f' N", rawvalue); } + result.cvalue = rawvalue; } //######################################################## else if (value->getFormat() == "formatLongitude"){ @@ -407,6 +418,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ rawvalue = 6.0 + float(random(0, 10)) / 100000.0; snprintf(buffer, bsize, " 15\" %2.4f'", rawvalue); } + result.cvalue = rawvalue; } //######################################################## else if (value->getFormat() == "formatDepth"){ @@ -435,6 +447,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ else { snprintf(buffer, bsize, fmt_dec_100, depth); } + result.cvalue = depth; } //######################################################## else if (value->getFormat() == "formatXte"){ @@ -467,6 +480,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ if(xte >= 100){ snprintf(buffer,bsize,"%3.0f",xte); } + result.cvalue = xte; } //######################################################## else if (value->getFormat() == "kelvinToC"){ @@ -499,6 +513,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ else { snprintf(buffer, bsize, fmt_dec_100, temp); } + result.cvalue = temp; } //######################################################## else if (value->getFormat() == "mtr2nm"){ @@ -531,6 +546,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ else { snprintf(buffer, bsize, fmt_dec_100, distance); } + result.cvalue = distance; } //######################################################## // Special XDR formats @@ -549,6 +565,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ } snprintf(buffer, bsize, "%4.0f", pressure); result.unit = "hPa"; + result.cvalue = pressure; } //######################################################## else if (value->getFormat() == "formatXdr:P:B"){ @@ -564,6 +581,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ } snprintf(buffer, bsize, "%4.0f", pressure); result.unit = "mBar"; + result.cvalue = pressure; } //######################################################## else if (value->getFormat() == "formatXdr:U:V"){ @@ -583,6 +601,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer, bsize, fmt_dec_10, voltage); } result.unit = "V"; + result.cvalue = voltage; } //######################################################## else if (value->getFormat() == "formatXdr:I:A"){ @@ -605,6 +624,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer, bsize, fmt_dec_100, current); } result.unit = "A"; + result.cvalue = current; } //######################################################## else if (value->getFormat() == "formatXdr:C:K"){ @@ -627,6 +647,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer, bsize, fmt_dec_100, temperature); } result.unit = "Deg C"; + result.cvalue = temperature; } //######################################################## else if (value->getFormat() == "formatXdr:C:C"){ @@ -649,6 +670,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer, bsize, fmt_dec_100, temperature); } result.unit = "Deg C"; + result.cvalue = temperature; } //######################################################## else if (value->getFormat() == "formatXdr:H:P"){ @@ -671,6 +693,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer, bsize, fmt_dec_100, humidity); } result.unit = "%"; + result.cvalue = humidity; } //######################################################## else if (value->getFormat() == "formatXdr:V:P"){ @@ -693,6 +716,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer, bsize, fmt_dec_100, volume); } result.unit = "%"; + result.cvalue = volume; } //######################################################## else if (value->getFormat() == "formatXdr:V:M"){ @@ -715,6 +739,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer, bsize, fmt_dec_100, volume); } result.unit = "l"; + result.cvalue = volume; } //######################################################## else if (value->getFormat() == "formatXdr:R:I"){ @@ -737,6 +762,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer, bsize, fmt_dec_100, flow); } result.unit = "l/min"; + result.cvalue = flow; } //######################################################## else if (value->getFormat() == "formatXdr:G:"){ @@ -759,6 +785,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer, bsize, fmt_dec_100, generic); } result.unit = ""; + result.cvalue = generic; } //######################################################## else if (value->getFormat() == "formatXdr:A:P"){ @@ -781,6 +808,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer, bsize, fmt_dec_100, dplace); } result.unit = "%"; + result.cvalue = dplace; } //######################################################## else if (value->getFormat() == "formatXdr:A:D"){ @@ -801,6 +829,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer,bsize,"%3.0f",angle); } result.unit = "Deg"; + result.cvalue = angle; } //######################################################## else if (value->getFormat() == "formatXdr:T:R"){ @@ -823,6 +852,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer, bsize, fmt_dec_100, rpm); } result.unit = "rpm"; + result.cvalue = rpm; } //######################################################## // Default format @@ -838,6 +868,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer, bsize, fmt_dec_100, value->value); } result.unit = ""; + result.cvalue = value->value; } buffer[bsize] = 0; result.value = rawvalue; // Return value is only necessary in case of simulation of graphic pointer diff --git a/lib/obp60task/Pagedata.h b/lib/obp60task/Pagedata.h index 3f56a7c..9c515b4 100644 --- a/lib/obp60task/Pagedata.h +++ b/lib/obp60task/Pagedata.h @@ -195,9 +195,10 @@ String formatLongitude(double lon); // Structure for formatted boat values typedef struct{ - double value; - String svalue; - String unit; + double value; // SI value of boat data value + double cvalue; // value converted to target unit + String svalue; // value converted to target unit and formatted + String unit; // target value unit } FormattedData; // Formatter for boat values From 489ee7ed09a0ebaee6d4b27a967d75d8b1ae7b66 Mon Sep 17 00:00:00 2001 From: Ulrich Meine Date: Sat, 22 Nov 2025 02:33:58 +0100 Subject: [PATCH 052/121] Lots of fixes and enhancements for OBPcharts; ringbuffer now returns values - internally still 2-byte storage; charts operate now with SI values; added flexible multiplier to history buffer; included data calibration for history data --- lib/obp60task/OBPDataOperations.cpp | 75 ++-- lib/obp60task/OBPDataOperations.h | 27 +- lib/obp60task/OBPRingBuffer.h | 50 ++- lib/obp60task/OBPRingBuffer.tpp | 155 ++++--- lib/obp60task/OBPcharts.cpp | 609 +++++++++++++++++++--------- lib/obp60task/OBPcharts.h | 31 +- lib/obp60task/PageWindPlot.cpp | 86 ++-- lib/obp60task/obp60task.cpp | 2 +- 8 files changed, 676 insertions(+), 359 deletions(-) diff --git a/lib/obp60task/OBPDataOperations.cpp b/lib/obp60task/OBPDataOperations.cpp index 73908ac..c68ef53 100644 --- a/lib/obp60task/OBPDataOperations.cpp +++ b/lib/obp60task/OBPDataOperations.cpp @@ -1,15 +1,19 @@ #include "OBPDataOperations.h" +#include "BoatDataCalibration.h" // Functions lib for data instance calibration +#include // --- Class HstryBuf --------------- + // Init history buffers for selected boat data void HstryBuf::init(BoatValueList* boatValues, GwLog *log) { logger = log; int hstryUpdFreq = 1000; // Update frequency for history buffers in ms - int hstryMinVal = 0; // Minimum value for these history buffers - twdHstryMax = 6283; // Max value for wind direction (TWD, AWD) in rad [0...2*PI], shifted by 1000 for 3 decimals - twsHstryMax = 65000; // Max value for wind speed (TWS, AWS) in m/s [0..65], shifted by 1000 for 3 decimals + int mltplr = 1000; // Multiplier which transforms original value into buffer type format + double hstryMinVal = 0; // Minimum value for these history buffers + twdHstryMax = 2 * M_PI; // Max value for wind direction (TWD, AWD) in rad [0...2*PI] + twsHstryMax = 65; // Max value for wind speed (TWS, AWS) in m/s [0..65] (limit due to type capacity of buffer - shifted by ) awdHstryMax = twdHstryMax; awsHstryMax = twsHstryMax; twdHstryMin = hstryMinVal; @@ -19,10 +23,12 @@ void HstryBuf::init(BoatValueList* boatValues, GwLog *log) { const double DBL_MAX = std::numeric_limits::max(); // Initialize history buffers with meta data - hstryBufList.twdHstry->setMetaData("TWD", "formatCourse", hstryUpdFreq, hstryMinVal, twdHstryMax); - hstryBufList.twsHstry->setMetaData("TWS", "formatKnots", hstryUpdFreq, hstryMinVal, twsHstryMax); - hstryBufList.awdHstry->setMetaData("AWD", "formatCourse", hstryUpdFreq, hstryMinVal, twdHstryMax); - hstryBufList.awsHstry->setMetaData("AWS", "formatKnots", hstryUpdFreq, hstryMinVal, twsHstryMax); + mltplr = 10000; // Store 4 decimals for course data + hstryBufList.twdHstry->setMetaData("TWD", "formatCourse", hstryUpdFreq, mltplr, hstryMinVal, twdHstryMax); + hstryBufList.awdHstry->setMetaData("AWD", "formatCourse", hstryUpdFreq, mltplr, hstryMinVal, twdHstryMax); + mltplr = 1000; // Store 3 decimals for windspeed data + hstryBufList.twsHstry->setMetaData("TWS", "formatKnots", hstryUpdFreq, mltplr, hstryMinVal, twsHstryMax); + hstryBufList.awsHstry->setMetaData("AWS", "formatKnots", hstryUpdFreq, mltplr, hstryMinVal, twsHstryMax); // create boat values for history data types, if they don't exist yet twdBVal = boatValues->findValueOrCreate(hstryBufList.twdHstry->getName()); @@ -49,30 +55,32 @@ void HstryBuf::init(BoatValueList* boatValues, GwLog *log) { //void HstryBuf::handleHstryBuf(GwApi* api, BoatValueList* boatValues, bool useSimuData) { void HstryBuf::handleHstryBuf(bool useSimuData) { - static int16_t twd = 20; //initial value only relevant if we use simulation data - static uint16_t tws = 20; //initial value only relevant if we use simulation data - static double awd, aws, hdt = 20; //initial value only relevant if we use simulation data + static double twd, tws, awd, aws, hdt = 20; //initial value only relevant if we use simulation data GwApi::BoatValue *calBVal; // temp variable just for data calibration -> we don't want to calibrate the original data here LOG_DEBUG(GwLog::DEBUG,"obp60task handleHstryBuf: TWD_isValid? %d, twdBVal: %.1f, twaBVal: %.1f, twsBVal: %.1f", twdBVal->valid, twdBVal->value * RAD_TO_DEG, twaBVal->value * RAD_TO_DEG, twsBVal->value * 3.6 / 1.852); if (twdBVal->valid) { +// if (!useSimuData) { calBVal = new GwApi::BoatValue("TWD"); // temporary solution for calibration of history buffer values calBVal->setFormat(twdBVal->getFormat()); calBVal->value = twdBVal->value; calBVal->valid = twdBVal->valid; calibrationData.calibrateInstance(calBVal, logger); // Check if boat data value is to be calibrated - twd = static_cast(std::round(calBVal->value * 1000.0)); + twd = calBVal->value; if (twd >= twdHstryMin && twd <= twdHstryMax) { hstryBufList.twdHstry->add(twd); + LOG_DEBUG(GwLog::DEBUG,"obp60task handleHstryBuf: calBVal.value %.2f, twd: %.2f, twdHstryMin: %.1f, twdHstryMax: %.2f", calBVal->value, twd, twdHstryMin, twdHstryMax); } delete calBVal; calBVal = nullptr; } else if (useSimuData) { +// } else { twd += random(-20, 20); - twd = WindUtils::to360(twd); - hstryBufList.twdHstry->add(static_cast(DegToRad(twd) * 1000.0)); + twd += static_cast(random(-349, 349) / 1000.0); // add up to +/- 20 degree in RAD + twd = WindUtils::to2PI(twd); + hstryBufList.twdHstry->add(twd); } if (twsBVal->valid) { @@ -81,15 +89,16 @@ void HstryBuf::handleHstryBuf(bool useSimuData) { calBVal->value = twsBVal->value; calBVal->valid = twsBVal->valid; calibrationData.calibrateInstance(calBVal, logger); // Check if boat data value is to be calibrated - tws = static_cast(std::round(calBVal->value * 1000)); + tws = calBVal->value; if (tws >= twsHstryMin && tws <= twsHstryMax) { hstryBufList.twsHstry->add(tws); } delete calBVal; calBVal = nullptr; } else if (useSimuData) { - tws += random(-5000, 5000); // TWS value in m/s; expands to 3 decimals - tws = constrain(tws, 0, 25000); // Limit TWS to [0..25] m/s + // tws += random(-5000, 5000); // TWS value in m/s; expands to 3 decimals + tws += static_cast(random(-5000, 5000) / 1000.0); // add up to +/- 5 m/s TWS speed + tws = constrain(tws, 0, 40); // Limit TWS to [0..40] m/s hstryBufList.twsHstry->add(tws); } @@ -109,16 +118,16 @@ void HstryBuf::handleHstryBuf(bool useSimuData) { calibrationData.calibrateInstance(calBVal, logger); // Check if boat data value is to be calibrated awdBVal->value = calBVal->value; awdBVal->valid = true; - awd = std::round(calBVal->value * 1000.0); + awd = calBVal->value; if (awd >= awdHstryMin && awd <= awdHstryMax) { - hstryBufList.awdHstry->add(static_cast(awd)); + hstryBufList.awdHstry->add(awd); } delete calBVal; calBVal = nullptr; } else if (useSimuData) { - awd += random(-20, 20); - awd = WindUtils::to360(awd); - hstryBufList.awdHstry->add(static_cast(DegToRad(awd) * 1000.0)); + awd += static_cast(random(-349, 349) / 1000.0); // add up to +/- 20 degree in RAD + awd = WindUtils::to2PI(awd); + hstryBufList.awdHstry->add(awd); } if (awsBVal->valid) { @@ -127,26 +136,28 @@ void HstryBuf::handleHstryBuf(bool useSimuData) { calBVal->value = awsBVal->value; calBVal->valid = awsBVal->valid; calibrationData.calibrateInstance(calBVal, logger); // Check if boat data value is to be calibrated - aws = std::round(calBVal->value * 1000); + aws = calBVal->value; if (aws >= awsHstryMin && aws <= awsHstryMax) { - hstryBufList.awsHstry->add(static_cast(aws)); + hstryBufList.awsHstry->add(aws); } delete calBVal; calBVal = nullptr; } else if (useSimuData) { - aws += random(-5000, 5000); // TWS value in m/s; expands to 1 decimal - aws = constrain(aws, 0, 25000); // Limit TWS to [0..25] m/s + aws += static_cast(random(-5000, 5000) / 1000.0); // add up to +/- 5 m/s TWS speed + aws = constrain(aws, 0, 40); // Limit TWS to [0..40] m/s hstryBufList.awsHstry->add(aws); } + LOG_DEBUG(GwLog::DEBUG,"obp60task handleHstryBuf-End: Buffer twdHstry: %.3f, twsHstry: %.3f, awdHstry: %.3f, awsHstry: %.3f", hstryBufList.twdHstry->getLast(), hstryBufList.twsHstry->getLast(), + hstryBufList.awdHstry->getLast(),hstryBufList.awsHstry->getLast()); } // --- Class HstryBuf --------------- // --- Class WindUtils -------------- double WindUtils::to2PI(double a) { - a = fmod(a, 2 * M_PI); + a = fmod(a, M_TWOPI); if (a < 0.0) { - a += 2 * M_PI; + a += M_TWOPI; } return a; } @@ -162,18 +173,18 @@ double WindUtils::toPI(double a) double WindUtils::to360(double a) { - a = fmod(a, 360); + a = fmod(a, 360.0); if (a < 0.0) { - a += 360; + a += 360.0; } return a; } double WindUtils::to180(double a) { - a += 180; + a += 180.0; a = to360(a); - a -= 180; + a -= 180.0; return a; } @@ -263,7 +274,7 @@ bool WindUtils::calcTrueWind(const double* awaVal, const double* awsVal, // If STW and SOG are not available, we cannot calculate true wind return false; } - // Serial.println("\ncalcTrueWind: HDT: " + String(hdt) + ", CTW: " + String(ctw) + ", STW: " + String(stw)); +// Serial.println("\ncalcTrueWind: HDT: " + String(hdt) + ", CTW: " + String(ctw) + ", STW: " + String(stw)); if ((*awaVal == DBL_MAX) || (*awsVal == DBL_MAX)) { // Cannot calculate true wind without valid AWA, AWS; other checks are done earlier diff --git a/lib/obp60task/OBPDataOperations.h b/lib/obp60task/OBPDataOperations.h index fc520f9..5ee604b 100644 --- a/lib/obp60task/OBPDataOperations.h +++ b/lib/obp60task/OBPDataOperations.h @@ -2,14 +2,12 @@ #pragma once #include #include "OBPRingBuffer.h" -#include "BoatDataCalibration.h" // Functions lib for data instance calibration #include "obp60task.h" -#include typedef struct { - RingBuffer* twdHstry; + RingBuffer* twdHstry; RingBuffer* twsHstry; - RingBuffer* awdHstry; + RingBuffer* awdHstry; RingBuffer* awsHstry; } tBoatHstryData; // Holds pointers to all history buffers for boat data @@ -17,18 +15,18 @@ class HstryBuf { private: GwLog *logger; - RingBuffer twdHstry; // Circular buffer to store true wind direction values + RingBuffer twdHstry; // Circular buffer to store true wind direction values RingBuffer twsHstry; // Circular buffer to store true wind speed values (TWS) - RingBuffer awdHstry; // Circular buffer to store apparant wind direction values + RingBuffer awdHstry; // Circular buffer to store apparant wind direction values RingBuffer awsHstry; // Circular buffer to store apparant xwind speed values (AWS) - int16_t twdHstryMin; // Min value for wind direction (TWD) in history buffer - int16_t twdHstryMax; // Max value for wind direction (TWD) in history buffer - uint16_t twsHstryMin; - uint16_t twsHstryMax; - int16_t awdHstryMin; - int16_t awdHstryMax; - uint16_t awsHstryMin; - uint16_t awsHstryMax; + double twdHstryMin; // Min value for wind direction (TWD) in history buffer + double twdHstryMax; // Max value for wind direction (TWD) in history buffer + double twsHstryMin; + double twsHstryMax; + double awdHstryMin; + double awdHstryMax; + double awsHstryMin; + double awsHstryMax; // boat values for buffers and for true wind calculation GwApi::BoatValue *twdBVal, *twsBVal, *twaBVal, *awdBVal, *awsBVal; @@ -72,6 +70,7 @@ public: hdmBVal = boatValues->findValueOrCreate("HDM"); varBVal = boatValues->findValueOrCreate("VAR"); }; + static double to2PI(double a); static double toPI(double a); static double to360(double a); diff --git a/lib/obp60task/OBPRingBuffer.h b/lib/obp60task/OBPRingBuffer.h index 4d13da6..15ad5c1 100644 --- a/lib/obp60task/OBPRingBuffer.h +++ b/lib/obp60task/OBPRingBuffer.h @@ -1,10 +1,6 @@ #pragma once +//#include "FreeRTOS.h" #include "GwSynchronized.h" -#include "WString.h" -#include "esp_heap_caps.h" -#include -#include -#include #include template @@ -41,7 +37,6 @@ bool operator!=(const PSRAMAllocator&, const PSRAMAllocator&) { return fal template class RingBuffer { private: - // std::vector buffer; // THE buffer vector std::vector> buffer; // THE buffer vector, allocated in PSRAM size_t capacity; size_t head; // Points to the next insertion position @@ -51,49 +46,52 @@ private: bool is_Full; // Indicates that all buffer elements are used and ringing is in use T MIN_VAL; // lowest possible value of buffer of type T MAX_VAL; // highest possible value of buffer of type -> indicates invalid value in buffer + double dblMIN_VAL, dblMAX_VAL; // MIN_VAL, MAX_VAL in double format mutable SemaphoreHandle_t bufLocker; // metadata for buffer String dataName; // Name of boat data in buffer String dataFmt; // Format of boat data in buffer int updFreq; // Update frequency in milliseconds - T smallest; // Value range of buffer: smallest value; needs to be => MIN_VAL - T largest; // Value range of buffer: biggest value; needs to be < MAX_VAL, since MAX_VAL indicates invalid entries + double mltplr; // Multiplier which transforms original value into buffer type format + double smallest; // Value range of buffer: smallest value; needs to be => MIN_VAL + double largest; // Value range of buffer: biggest value; needs to be < MAX_VAL, since MAX_VAL indicates invalid entries void initCommon(); public: RingBuffer(); RingBuffer(size_t size); - void setMetaData(String name, String format, int updateFrequency, T minValue, T maxValue); // Set meta data for buffer - bool getMetaData(String& name, String& format, int& updateFrequency, T& minValue, T& maxValue); // Get meta data of buffer + void setMetaData(String name, String format, int updateFrequency, double multiplier, double minValue, double maxValue); // Set meta data for buffer + bool getMetaData(String& name, String& format, int& updateFrequency, double& multiplier, double& minValue, double& maxValue); // Get meta data of buffer bool getMetaData(String& name, String& format); String getName() const; // Get buffer name String getFormat() const; // Get buffer data format - void add(const T& value); // Add a new value to buffer - T get(size_t index) const; // Get value at specific position (0-based index from oldest to newest) - T getFirst() const; // Get the first (oldest) value in buffer - T getLast() const; // Get the last (newest) value in buffer - T getMin() const; // Get the lowest value in buffer - T getMin(size_t amount) const; // Get minimum value of the last values of buffer - T getMax() const; // Get the highest value in buffer - T getMax(size_t amount) const; // Get maximum value of the last values of buffer - T getMid() const; // Get mid value between and value in buffer - T getMid(size_t amount) const; // Get mid value between and value of the last values of buffer - T getMedian() const; // Get the median value in buffer - T getMedian(size_t amount) const; // Get the median value of the last values of buffer + void add(const double& value); // Add a new value to buffer + double get(size_t index) const; // Get value at specific position (0-based index from oldest to newest) + double getFirst() const; // Get the first (oldest) value in buffer + double getLast() const; // Get the last (newest) value in buffer + double getMin() const; // Get the lowest value in buffer + double getMin(size_t amount) const; // Get minimum value of the last values of buffer + double getMax() const; // Get the highest value in buffer + double getMax(size_t amount) const; // Get maximum value of the last values of buffer + double getMid() const; // Get mid value between and value in buffer + double getMid(size_t amount) const; // Get mid value between and value of the last values of buffer + double getMedian() const; // Get the median value in buffer + double getMedian(size_t amount) const; // Get the median value of the last values of buffer size_t getCapacity() const; // Get the buffer capacity (maximum size) size_t getCurrentSize() const; // Get the current number of elements in buffer size_t getFirstIdx() const; // Get the index of oldest value in buffer size_t getLastIdx() const; // Get the index of newest value in buffer bool isEmpty() const; // Check if buffer is empty bool isFull() const; // Check if buffer is full - T getMinVal() const; // Get lowest possible value for buffer - T getMaxVal() const; // Get highest possible value for buffer; used for unset/invalid buffer data + double getMinVal() const; // Get lowest possible value for buffer + double getMaxVal() const; // Get highest possible value for buffer; used for unset/invalid buffer data void clear(); // Clear buffer void resize(size_t size); // Delete buffer and set new size - T operator[](size_t index) const; // Operator[] for convenient access (same as get()) - std::vector getAllValues() const; // Get all current values as a vector + double operator[](size_t index) const; // Operator[] for convenient access (same as get()) + std::vector getAllValues() const; // Get all current values in native buffer format as a vector + std::vector getAllValues(size_t amount) const; // Get last values in native buffer format as a vector }; #include "OBPRingBuffer.tpp" \ No newline at end of file diff --git a/lib/obp60task/OBPRingBuffer.tpp b/lib/obp60task/OBPRingBuffer.tpp index 9174568..281e89d 100644 --- a/lib/obp60task/OBPRingBuffer.tpp +++ b/lib/obp60task/OBPRingBuffer.tpp @@ -1,14 +1,20 @@ #include "OBPRingBuffer.h" +#include +#include template -void RingBuffer::initCommon() { +void RingBuffer::initCommon() +{ MIN_VAL = std::numeric_limits::lowest(); MAX_VAL = std::numeric_limits::max(); + dblMIN_VAL = static_cast(MIN_VAL); + dblMAX_VAL = static_cast(MAX_VAL); dataName = ""; dataFmt = ""; updFreq = -1; - smallest = MIN_VAL; - largest = MAX_VAL; + mltplr = 1; + smallest = dblMIN_VAL; + largest = dblMAX_VAL; bufLocker = xSemaphoreCreateMutex(); } @@ -42,19 +48,20 @@ RingBuffer::RingBuffer(size_t size) // Specify meta data of buffer content template -void RingBuffer::setMetaData(String name, String format, int updateFrequency, T minValue, T maxValue) +void RingBuffer::setMetaData(String name, String format, int updateFrequency, double multiplier, double minValue, double maxValue) { GWSYNCHRONIZED(&bufLocker); dataName = name; dataFmt = format; updFreq = updateFrequency; - smallest = std::max(MIN_VAL, minValue); - largest = std::min(MAX_VAL, maxValue); + mltplr = multiplier; + smallest = std::max(dblMIN_VAL, minValue); + largest = std::min(dblMAX_VAL, maxValue); } // Get meta data of buffer content template -bool RingBuffer::getMetaData(String& name, String& format, int& updateFrequency, T& minValue, T& maxValue) +bool RingBuffer::getMetaData(String& name, String& format, int& updateFrequency, double& multiplier, double& minValue, double& maxValue) { if (dataName == "" || dataFmt == "" || updFreq == -1) { return false; // Meta data not set @@ -64,6 +71,7 @@ bool RingBuffer::getMetaData(String& name, String& format, int& updateFrequen name = dataName; format = dataFmt; updateFrequency = updFreq; + multiplier = mltplr; minValue = smallest; maxValue = largest; return true; @@ -99,13 +107,13 @@ String RingBuffer::getFormat() const // Add a new value to buffer template -void RingBuffer::add(const T& value) +void RingBuffer::add(const double& value) { GWSYNCHRONIZED(&bufLocker); if (value < smallest || value > largest) { buffer[head] = MAX_VAL; // Store MAX_VAL if value is out of range } else { - buffer[head] = value; + buffer[head] = static_cast(std::round(value * mltplr)); } last = head; @@ -117,63 +125,63 @@ void RingBuffer::add(const T& value) is_Full = true; } } - + // Serial.printf("Ringbuffer: value %.3f, multiplier: %.1f, buffer: %d\n", value, mltplr, buffer[head]); head = (head + 1) % capacity; } // Get value at specific position (0-based index from oldest to newest) template -T RingBuffer::get(size_t index) const +double RingBuffer::get(size_t index) const { GWSYNCHRONIZED(&bufLocker); if (isEmpty() || index < 0 || index >= count) { - return MAX_VAL; + return dblMAX_VAL; } size_t realIndex = (first + index) % capacity; - return buffer[realIndex]; + return static_cast(buffer[realIndex] / mltplr); } // Operator[] for convenient access (same as get()) template -T RingBuffer::operator[](size_t index) const +double RingBuffer::operator[](size_t index) const { return get(index); } // Get the first (oldest) value in the buffer template -T RingBuffer::getFirst() const +double RingBuffer::getFirst() const { if (isEmpty()) { - return MAX_VAL; + return dblMAX_VAL; } return get(0); } // Get the last (newest) value in the buffer template -T RingBuffer::getLast() const +double RingBuffer::getLast() const { if (isEmpty()) { - return MAX_VAL; + return dblMAX_VAL; } return get(count - 1); } // Get the lowest value in the buffer template -T RingBuffer::getMin() const +double RingBuffer::getMin() const { if (isEmpty()) { - return MAX_VAL; + return dblMAX_VAL; } - T minVal = MAX_VAL; - T value; + double minVal = dblMAX_VAL; + double value; for (size_t i = 0; i < count; i++) { value = get(i); - if (value < minVal && value != MAX_VAL) { + if (value < minVal && value != dblMAX_VAL) { minVal = value; } } @@ -182,19 +190,19 @@ T RingBuffer::getMin() const // Get minimum value of the last values of buffer template -T RingBuffer::getMin(size_t amount) const +double RingBuffer::getMin(size_t amount) const { if (isEmpty() || amount <= 0) { - return MAX_VAL; + return dblMAX_VAL; } if (amount > count) amount = count; - T minVal = MAX_VAL; - T value; + double minVal = dblMAX_VAL; + double value; for (size_t i = 0; i < amount; i++) { value = get(count - 1 - i); - if (value < minVal && value != MAX_VAL) { + if (value < minVal && value != dblMAX_VAL) { minVal = value; } } @@ -203,75 +211,81 @@ T RingBuffer::getMin(size_t amount) const // Get the highest value in the buffer template -T RingBuffer::getMax() const +double RingBuffer::getMax() const { if (isEmpty()) { - return MAX_VAL; + return dblMAX_VAL; } - T maxVal = MIN_VAL; - T value; + double maxVal = dblMIN_VAL; + double value; for (size_t i = 0; i < count; i++) { value = get(i); - if (value > maxVal && value != MAX_VAL) { + if (value > maxVal && value != dblMAX_VAL) { maxVal = value; } } + if (maxVal == dblMIN_VAL) { // no change of initial value -> buffer has only invalid values (MAX_VAL) + maxVal = dblMAX_VAL; + } return maxVal; } // Get maximum value of the last values of buffer template -T RingBuffer::getMax(size_t amount) const +double RingBuffer::getMax(size_t amount) const { if (isEmpty() || amount <= 0) { - return MAX_VAL; + return dblMAX_VAL; } if (amount > count) amount = count; - T maxVal = MIN_VAL; - T value; + double maxVal = dblMIN_VAL; + double value; for (size_t i = 0; i < amount; i++) { value = get(count - 1 - i); - if (value > maxVal && value != MAX_VAL) { + if (value > maxVal && value != dblMAX_VAL) { maxVal = value; } } + if (maxVal == dblMIN_VAL) { // no change of initial value -> buffer has only invalid values (MAX_VAL) + maxVal = dblMAX_VAL; + } return maxVal; } // Get mid value between and value in the buffer template -T RingBuffer::getMid() const +double RingBuffer::getMid() const { if (isEmpty()) { - return MAX_VAL; + return dblMAX_VAL; } - return (getMin() + getMax()) / static_cast(2); + return (getMin() + getMax()) / 2; } // Get mid value between and value of the last values of buffer template -T RingBuffer::getMid(size_t amount) const +double RingBuffer::getMid(size_t amount) const { if (isEmpty() || amount <= 0) { - return MAX_VAL; + return dblMAX_VAL; } if (amount > count) amount = count; - return (getMin(amount) + getMax(amount)) / static_cast(2); + return (getMin(amount) + getMax(amount)) / 2; } // Get the median value in the buffer template -T RingBuffer::getMedian() const +double RingBuffer::getMedian() const { if (isEmpty()) { - return MAX_VAL; + return dblMAX_VAL; } // Create a temporary vector with current valid elements @@ -287,20 +301,20 @@ T RingBuffer::getMedian() const if (count % 2 == 1) { // Odd number of elements - return temp[count / 2]; + return static_cast(temp[count / 2]); } else { // Even number of elements - return average of middle two // Note: For integer types, this truncates. For floating point, it's exact. - return (temp[count / 2 - 1] + temp[count / 2]) / 2; + return static_cast((temp[count / 2 - 1] + temp[count / 2]) / 2); } } // Get the median value of the last values of buffer template -T RingBuffer::getMedian(size_t amount) const +double RingBuffer::getMedian(size_t amount) const { if (isEmpty() || amount <= 0) { - return MAX_VAL; + return dblMAX_VAL; } if (amount > count) amount = count; @@ -310,7 +324,7 @@ T RingBuffer::getMedian(size_t amount) const temp.reserve(amount); for (size_t i = 0; i < amount; i++) { - temp.push_back(get(i)); + temp.push_back(get(count - 1 - i)); } // Sort to find median @@ -318,11 +332,11 @@ T RingBuffer::getMedian(size_t amount) const if (amount % 2 == 1) { // Odd number of elements - return temp[amount / 2]; + return static_cast(temp[amount / 2]); } else { // Even number of elements - return average of middle two // Note: For integer types, this truncates. For floating point, it's exact. - return (temp[amount / 2 - 1] + temp[amount / 2]) / 2; + return static_cast((temp[amount / 2 - 1] + temp[amount / 2]) / 2); } } @@ -370,16 +384,16 @@ bool RingBuffer::isFull() const // Get lowest possible value for buffer template -T RingBuffer::getMinVal() const +double RingBuffer::getMinVal() const { - return MIN_VAL; + return dblMIN_VAL; } // Get highest possible value for buffer; used for unset/invalid buffer data template -T RingBuffer::getMaxVal() const +double RingBuffer::getMaxVal() const { - return MAX_VAL; + return dblMAX_VAL; } // Clear buffer @@ -411,16 +425,37 @@ void RingBuffer::resize(size_t newSize) buffer.resize(newSize, MAX_VAL); } -// Get all current values as a vector +// Get all current values in native buffer format as a vector template -std::vector RingBuffer::getAllValues() const +std::vector RingBuffer::getAllValues() const { - std::vector result; + std::vector result; result.reserve(count); for (size_t i = 0; i < count; i++) { result.push_back(get(i)); } + return result; +} + +// Get last values in native buffer format as a vector +template +std::vector RingBuffer::getAllValues(size_t amount) const +{ + std::vector result; + + if (isEmpty() || amount <= 0) { + return result; + } + if (amount > count) + amount = count; + + result.reserve(amount); + + for (size_t i = 0; i < amount; i++) { + result.push_back(get(count - 1 - i)); + } + return result; } \ No newline at end of file diff --git a/lib/obp60task/OBPcharts.cpp b/lib/obp60task/OBPcharts.cpp index 907712b..96954dc 100644 --- a/lib/obp60task/OBPcharts.cpp +++ b/lib/obp60task/OBPcharts.cpp @@ -5,7 +5,7 @@ // --- Class Chart --------------- template -Chart::Chart(RingBuffer& dataBuf, int8_t chrtDir, int8_t chrtSz, int dfltRng, CommonData& common, bool useSimuData) +Chart::Chart(RingBuffer& dataBuf, int8_t chrtDir, int8_t chrtSz, double dfltRng, CommonData& common, bool useSimuData) : dataBuf(dataBuf) , chrtDir(chrtDir) , chrtSz(chrtSz) @@ -17,28 +17,28 @@ Chart::Chart(RingBuffer& dataBuf, int8_t chrtDir, int8_t chrtSz, int dfltR fgColor = commonData->fgcolor; bgColor = commonData->bgcolor; - LOG_DEBUG(GwLog::DEBUG, "Chart create: dataBuf: %p", (void*)&dataBuf); + LOG_DEBUG(GwLog::DEBUG, "Chart Init: dataBuf: %p", (void*)&dataBuf); dWidth = getdisplay().width(); dHeight = getdisplay().height(); if (chrtDir == 0) { // horizontal chart timeline direction - timAxis = dWidth - xOffset; + timAxis = dWidth; switch (chrtSz) { case 0: valAxis = dHeight - top - bottom; - cStart = { xOffset, top }; + cStart = { 0, top }; break; case 1: valAxis = (dHeight - top - bottom) / 2 - gap; - cStart = { xOffset, top }; + cStart = { 0, top }; break; case 2: valAxis = (dHeight - top - bottom) / 2 - gap; - cStart = { xOffset, top + (valAxis + gap) + gap }; + cStart = { 0, top + (valAxis + gap) + gap }; break; default: - LOG_DEBUG(GwLog::DEBUG, "displayChart: wrong parameter"); + LOG_DEBUG(GwLog::ERROR, "displayChart: wrong init parameter"); return; } } else if (chrtDir == 1) { @@ -46,31 +46,52 @@ Chart::Chart(RingBuffer& dataBuf, int8_t chrtDir, int8_t chrtSz, int dfltR timAxis = dHeight - top - bottom; switch (chrtSz) { case 0: - valAxis = dWidth - xOffset; - cStart = { xOffset, top }; + valAxis = dWidth; + cStart = { 0, top }; break; case 1: - valAxis = dWidth / 2 - gap; + valAxis = dWidth / 2 - gap - 1; cStart = { 0, top }; break; case 2: - valAxis = dWidth / 2 - gap; + valAxis = dWidth / 2 - gap - 1; cStart = { dWidth / 2 + gap, top }; break; default: - LOG_DEBUG(GwLog::DEBUG, "displayChart: wrong parameter"); + LOG_DEBUG(GwLog::ERROR, "displayChart: wrong init parameter"); return; } } else { - LOG_DEBUG(GwLog::DEBUG, "displayChart: wrong parameter"); + LOG_DEBUG(GwLog::ERROR, "displayChart: wrong init parameter"); return; } - // xCenter = timAxis / 2; dataBuf.getMetaData(dbName, dbFormat); + dbMIN_VAL = dataBuf.getMinVal(); dbMAX_VAL = dataBuf.getMaxVal(); bufSize = dataBuf.getCapacity(); - LOG_DEBUG(GwLog::DEBUG, "Chart create: dWidth: %d, dHeight: %d, timAxis: %d, valAxis: %d, cStart {x,y}: %d, %d, dbname: %s", dWidth, dHeight, timAxis, valAxis, cStart.x, cStart.y, dbName); + + if (dbFormat == "formatCourse" || dbFormat == "FormatWind" || dbFormat == "FormatRot") { + + if (dbFormat == "FormatRot") { + chrtDataFmt = 2; // Chart is showing data of rotational format + } else { + chrtDataFmt = 1; // Chart is showing data of course / wind format + } + rngStep = M_TWOPI / 360.0 * 10.0; // +/-10 degrees on each end of chrtMid; we are calculating with SI values + + } else { + chrtDataFmt = 0; // Chart is showing any other data format than + rngStep = 5.0; // +/- 10 for all other values (eg. m/s, m, K, mBar) + } + + chrtMin = 0; + chrtMax = 0; + chrtMid = dbMAX_VAL; + chrtRng = dfltRng; + recalcRngCntr = true; // initialize on first screen call + + LOG_DEBUG(GwLog::DEBUG, "Chart Init: dWidth: %d, dHeight: %d, timAxis: %d, valAxis: %d, cStart {x,y}: %d, %d, dbname: %s, rngStep: %.4f", dWidth, dHeight, timAxis, valAxis, cStart.x, cStart.y, dbName, rngStep); }; template @@ -78,114 +99,25 @@ Chart::~Chart() { } -// chart time axis label + lines +// Perform all actions to draw chart +// Parameters are chart time interval, and the current boat data value to be printed template -void Chart::drawChrtTimeAxis(int8_t chrtIntv) +void Chart::showChrt(int8_t chrtIntv, GwApi::BoatValue currValue) { - int timeRng; - float slots, intv, i; - char sTime[6]; - - getdisplay().setTextColor(fgColor); - if (chrtDir == 0) { // horizontal chart - getdisplay().fillRect(0, top, dWidth, 2, fgColor); - getdisplay().setFont(&Ubuntu_Bold8pt8b); - - timeRng = chrtIntv * 4; // Chart time interval: [1] 4 min., [2] 8 min., [3] 12 min., [4] 16 min., [8] 32 min. - slots = (timAxis - xOffset) / 75.0; // number of axis labels - intv = timeRng / slots; // minutes per chart axis interval - i = timeRng; // Chart axis label start at -32, -16, -12, ... minutes - - for (int j = 0; j < timAxis - 30; j += 75) { - LOG_DEBUG(GwLog::DEBUG, "ChartHdr: timAxis: %d, {x,y}: {%d,%d}, i: %.1f, j: %d, chrtIntv: %d, intv: %.1f, slots: %.1f", timAxis, cStart.x, cStart.y, i, j, chrtIntv, intv, slots); - if (chrtIntv < 3) { - snprintf(sTime, size_t(sTime), "-%.1f", i); - drawTextCenter(cStart.x + j - 8, cStart.y - 8, sTime); // time interval - } else { - snprintf(sTime, size_t(sTime), "-%.0f", std::round(i)); - drawTextCenter(cStart.x + j - 4, cStart.y - 8, sTime); // time interval - } - getdisplay().drawLine(cStart.x + j, cStart.y, cStart.x + j, cStart.y + 5, fgColor); - i -= intv; - } - /* getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(timAxis - 8, cStart.y - 2); - getdisplay().print("min"); */ - - } else { // chrtDir == 1; vertical chart - getdisplay().setFont(&Ubuntu_Bold8pt8b); - timeRng = chrtIntv * 4; // Chart time interval: [1] 4 min., [2] 8 min., [3] 12 min., [4] 16 min., [8] 32 min. - slots = timAxis / 75.0; // number of axis labels - intv = timeRng / slots; // minutes per chart axis interval - i = 0; // Chart axis label start at -32, -16, -12, ... minutes - - for (int j = 0; j < (timAxis - 75); j += 75) { // don't print time label at lower end - LOG_DEBUG(GwLog::DEBUG, "ChartHdr: timAxis: %d, {x,y}: {%d,%d}, i: %.1f, j: %d, chrtIntv: %d, intv: %.1f, slots: %.1f", timAxis, cStart.x, cStart.y, i, j, chrtIntv, intv, slots); - if (chrtIntv < 3) { // print 1 decimal if time range is single digit (4 or 8 minutes) - snprintf(sTime, size_t(sTime), "%.1f", i * -1); - } else { - snprintf(sTime, size_t(sTime), "%.0f", std::round(i) * -1); - } - drawTextCenter(dWidth / 2, cStart.y + j, sTime); // time value - getdisplay().drawLine(cStart.x, cStart.y + j, cStart.x + valAxis, cStart.y + j, fgColor); // Grid line - i += intv; - } - } -} - -// chart value axis labels + lines -template -void Chart::drawChrtValAxis() -{ - float slots; - int i, intv; - char sVal[6]; - - getdisplay().setFont(&Ubuntu_Bold10pt8b); - if (chrtDir == 0) { // horizontal chart - slots = valAxis / 60.0; // number of axis labels - intv = static_cast(round(chrtRng / slots)); - i = intv; - for (int j = 60; j < valAxis - 30; j += 60) { - LOG_DEBUG(GwLog::DEBUG, "ChartGrd: chrtRng: %d, intv: %d, slots: %.1f, valAxis: %d, i: %d, j: %d", chrtRng, intv, slots, valAxis, i, j); - getdisplay().fillRect(cStart.x - xOffset, cStart.y + j - 9, cStart.x - xOffset + 28, 12, bgColor); // Clear small area to remove potential chart lines - String sVal = String(static_cast(round(i))); - getdisplay().setCursor((3 - sVal.length()) * 9, cStart.y + j + 4); // value right-formated - getdisplay().printf("%s", sVal); // Range value - i += intv; - getdisplay().drawLine(cStart.x + 2, cStart.y + j, cStart.x + timAxis, cStart.y + j, fgColor); - } - getdisplay().setFont(&Ubuntu_Bold12pt8b); - drawTextRalign(cStart.x + timAxis, cStart.y - 3, dataBuf.getName()); // buffer data name - - } else { // chrtDir == 1; vertical chart - getdisplay().fillRect(cStart.x, top, valAxis, 2, fgColor); // top chart line - getdisplay().setCursor(cStart.x, cStart.y - 2); - snprintf(sVal, sizeof(sVal), "%d", dataBuf.getMin(numBufVals) / 1000); - getdisplay().printf("%s", sVal); // Range low end - snprintf(sVal, sizeof(sVal), "%.0f", round(chrtRng / 2)); - drawTextCenter(cStart.x + (valAxis / 2), cStart.y - 10, sVal); // Range mid end - snprintf(sVal, sizeof(sVal), "%.0f", round(chrtRng)); - drawTextRalign(cStart.x + valAxis - 1, cStart.y - 2, sVal); // Range high end - for (int j = 0; j <= valAxis; j += (valAxis / 2)) { - getdisplay().drawLine(cStart.x + j - 1, cStart.y, cStart.x + j - 1, cStart.y + timAxis, fgColor); - } - getdisplay().setFont(&Ubuntu_Bold12pt8b); - drawTextCenter(cStart.x + (valAxis / 4) + 4, cStart.y - 11, dataBuf.getName()); // buffer data name - LOG_DEBUG(GwLog::DEBUG, "ChartGrd: chrtRng: %d, intv: %d, slots: %.1f, valAxis: %d, i: %d, sVal.length: %d", chrtRng, intv, slots, valAxis, i, sizeof(sVal)); - } + drawChrtTimeAxis(chrtIntv); + drawChrt(chrtIntv, currValue); + drawChrtValAxis(); } // draw chart template -void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue currValue) +void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) { - float chrtScl; // Scale for data values in pixels per value - int chrtVal; // Current data value - static int chrtPrevVal; // Last data value in chart area + double chrtVal; // Current data value + double chrtScl; // Scale for data values in pixels per value + static double chrtPrevVal; // Last data value in chart area bool bufDataValid = false; // Flag to indicate if buffer data is valid static int numNoData; // Counter for multiple invalid data values in a row - // GwApi::BoatValue currValue; // temporary boat value to display current data buffer value int x, y; // x and y coordinates for drawing static int prevX, prevY; // Last x and y coordinates for drawing @@ -194,13 +126,15 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue currValue) count = dataBuf.getCurrentSize(); currIdx = dataBuf.getLastIdx(); numAddedBufVals = (currIdx - lastAddedIdx + bufSize) % bufSize; // Number of values added to buffer since last display + if (chrtIntv != oldChrtIntv || count == 1) { // new data interval selected by user; this is only x * 230 values instead of 240 seconds (4 minutes) per interval step - intvBufSize = timAxis * chrtIntv; - numBufVals = min(count, (timAxis - 60) * chrtIntv); + // intvBufSize = timAxis * chrtIntv; // obsolete + numBufVals = min(count, (timAxis - 60) * chrtIntv); // keep free or release 60 values on chart for plotting of new values bufStart = max(0, count - numBufVals); lastAddedIdx = currIdx; oldChrtIntv = chrtIntv; + } else { numBufVals = numBufVals + numAddedBufVals; lastAddedIdx = currIdx; @@ -209,19 +143,16 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue currValue) } } - calcChrtRng(); - chrtScl = float(valAxis) / float(chrtRng); // Chart scale: pixels per value step + calcChrtBorders(chrtMid, chrtMin, chrtMax, chrtRng); + chrtScl = double(valAxis) / chrtRng; // Chart scale: pixels per value step // Do we have valid buffer data? - if (dataBuf.getMax() == dbMAX_VAL) { - // only values in buffer -> no valid wind data available + if (dataBuf.getMax() == dbMAX_VAL) { // only values in buffer -> no valid wind data available bufDataValid = false; - } else if (!currValue.valid && !useSimuData) { - // currently no valid boat data available and no simulation mode + } else if (!currValue.valid && !useSimuData) { // currently no valid boat data available and no simulation mode numNoData++; bufDataValid = true; - if (numNoData > 3) { - // If more than 4 invalid values in a row, send message + if (numNoData > 3) { // If more than 4 invalid values in a row, send message bufDataValid = false; } } else { @@ -233,135 +164,451 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue currValue) //*********************************************************************** if (bufDataValid) { for (int i = 0; i < (numBufVals / chrtIntv); i++) { - chrtVal = static_cast(dataBuf.get(bufStart + (i * chrtIntv))); // show the latest wind values in buffer; keep 1st value constant in a rolling buffer + chrtVal = dataBuf.get(bufStart + (i * chrtIntv)); // show the latest wind values in buffer; keep 1st value constant in a rolling buffer if (chrtVal == dbMAX_VAL) { chrtPrevVal = dbMAX_VAL; } else { - chrtVal = static_cast((chrtVal / 1000.0) + 0.5); // Convert to real value and round + if (chrtDir == 0) { // horizontal chart x = cStart.x + i; // Position in chart area - y = cStart.y + (chrtVal * chrtScl); // value + if (chrtDataFmt == 0) { + y = cStart.y + static_cast(((chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round + } else { // degree type value + y = cStart.y + static_cast((WindUtils::to2PI(chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round + } } else { // vertical chart - x = cStart.x + (chrtVal * chrtScl); // value y = cStart.y + timAxis - i; // Position in chart area + if (chrtDataFmt == 0) { + x = cStart.x + static_cast(((chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round + } else { // degree type value + x = cStart.x + static_cast((WindUtils::to2PI(chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round + } } - if (i >= (numBufVals / chrtIntv) - 10) // log chart data of 1 line (adjust for test purposes) - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Chart: i: %d, chrtVal: %d, {x,y} {%d,%d}", i, chrtVal, x, y); + + if (i >= (numBufVals / chrtIntv) - 4) // log chart data of 1 line (adjust for test purposes) + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Chart: i: %d, chrtVal: %.4f, {x,y} {%d,%d}", i, chrtVal, x, y); if ((i == 0) || (chrtPrevVal == dbMAX_VAL)) { // just a dot for 1st chart point or after some invalid values prevX = x; prevY = y; + + } else if (chrtDataFmt != 0) { // cross borders check for degree values; shift values to [-PI..0..PI]; when crossing borders, range is 2x PI degrees + + // Normalize both values relative to chrtMin (shift range to start at 0) + double normCurr = WindUtils::to2PI(chrtVal - chrtMin); + double normPrev = WindUtils::to2PI(chrtPrevVal - chrtMin); + // Check if pixel positions are far apart (crossing chart boundary); happens when one value is near chrtMax and the other near chrtMin + bool crossedBorders = std::abs(normCurr - normPrev) > (chrtRng / 2.0); + + if (crossedBorders) { // If current value crosses chart borders compared to previous value, split line + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Chart: crossedBorders: %d, chrtVal: %.2f, chrtPrevVal: %.2f", crossedBorders, chrtVal, chrtPrevVal); + bool wrappingFromHighToLow = normCurr < normPrev; // Determine which edge we're crossing + int xSplit = wrappingFromHighToLow ? (cStart.x + valAxis) : cStart.x; + getdisplay().drawLine(prevX, prevY, xSplit, y, fgColor); + getdisplay().drawLine(prevX, prevY - 1, ((xSplit != prevX) ? xSplit : xSplit - 1), ((xSplit != prevX) ? y - 1 : y), fgColor); + prevX = wrappingFromHighToLow ? cStart.x : (cStart.x + valAxis); + } } + // Draw line with 2 pixels width + make sure vertical lines are drawn correctly if (chrtDir == 0 || x == prevX) { // vertical line getdisplay().drawLine(prevX, prevY, x, y, fgColor); getdisplay().drawLine(prevX - 1, prevY, x - 1, y, fgColor); +// getdisplay().drawLine(prevX + 1, prevY, x - 1, y, fgColor); } else if (chrtDir == 1 || x != prevX) { // line with some horizontal trend -> normal state getdisplay().drawLine(prevX, prevY, x, y, fgColor); getdisplay().drawLine(prevX, prevY - 1, x, y - 1, fgColor); +// getdisplay().drawLine(prevX, prevY + 1, x, y - 1, fgColor); } chrtPrevVal = chrtVal; prevX = x; prevY = y; } + // Reaching chart area bottom end if (i >= timAxis - 1) { oldChrtIntv = 0; // force reset of buffer start and number of values to show in next display loop + + if (chrtDataFmt == 1) { // degree of course or wind + recalcRngCntr = true; + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot FreeTop: timAxis: %d, i: %d, bufStart: %d, numBufVals: %d, recalcRngCntr: %d", timAxis, i, bufStart, numBufVals, recalcRngCntr); + } break; } } - // drawChrtValAxis(); - // uses BoatValue temp variable to format latest buffer value // doesn't work unfortunately when 'simulation data' is active, because OBP60Formatter generates own simulation value in that case - uint16_t lastVal = dataBuf.getLast(); - currValue.value = lastVal / 1000.0; - currValue.valid = (static_cast(lastVal) != dbMAX_VAL); - LOG_DEBUG(GwLog::DEBUG, "Chart drawChrt: lastVal: %d, currValue-value: %.1f, Valid: %d, Name: %s, Address: %p", lastVal, currValue.value, - currValue.valid, currValue.getName(), (void*)&currValue); - prntCurrValue(&currValue, { x, y }); + currValue.value = dataBuf.getLast(); + currValue.valid = currValue.value != dbMAX_VAL; + Chart::prntCurrValue(currValue, { x, y }); + LOG_DEBUG(GwLog::DEBUG, "Chart drawChrt: currValue-value: %.1f, Valid: %d, Name: %s, Address: %p", currValue.value, currValue.valid, currValue.getName(), (void*)&currValue); } else { // No valid data available - LOG_DEBUG(GwLog::LOG, "PageWindPlot: No valid data available"); getdisplay().setFont(&Ubuntu_Bold10pt8b); + int pX, pY; - if (chrtDir == 0) { - pX = dWidth / 2; + if (chrtDir == 0) { // horizontal chart + pX = cStart.x + (timAxis / 2); pY = cStart.y + (valAxis / 2) - 10; - } else { - pX = valAxis / 2; + } else { // vertical chart + pX = cStart.x + (valAxis / 2); pY = cStart.y + (timAxis / 2) - 10; } - getdisplay().fillRect(pX - 33, pY - 10, 66, 24, commonData->bgcolor); // Clear area for message + + getdisplay().fillRect(pX - 33, pY - 10, 66, 24, bgColor); // Clear area for message drawTextCenter(pX, pY, "No data"); + LOG_DEBUG(GwLog::LOG, "PageWindPlot: No valid data available"); + } +} + +// Get maximum difference of last of dataBuf ringbuffer values to center chart +template +double Chart::getRng(double center, size_t amount) +{ + size_t count = dataBuf.getCurrentSize(); + + if (dataBuf.isEmpty() || amount <= 0) { + return dbMAX_VAL; + } + if (amount > count) + amount = count; + + double value = 0; + double range = 0; + double maxRng = dbMIN_VAL; + + // Start from the newest value (last) and go backwards x times + for (size_t i = 0; i < amount; i++) { + value = dataBuf.get(count - 1 - i); + + if (value == dbMAX_VAL) { + continue; // ignore invalid values + } + + range = abs(fmod((value - center + (M_TWOPI + M_PI)), M_TWOPI) - M_PI); + if (range > maxRng) + maxRng = range; } - drawChrtValAxis(); + if (maxRng > M_PI) { + maxRng = M_PI; + } + + return (maxRng != dbMIN_VAL ? maxRng : dbMAX_VAL); // Return range from to +} + +// check and adjust chart range and set range borders and range middle +template +void Chart::calcChrtBorders(double& rngMid, double& rngMin, double& rngMax, double& rng) +{ + if (chrtDataFmt == 0) { + // Chart data is of any type but 'degree' + + double oldRngMin = rngMin; + double oldRngMax = rngMax; + + // Chart starts at lowest range value, but at least '0' or includes even negative values + double currMinVal = dataBuf.getMin(numBufVals); + LOG_DEBUG(GwLog::DEBUG, "calcChrtRange0a: currMinVal: %.1f, currMaxVal: %.1f, rngMin: %.1f, rngMid: %.1f, rngMax: %.1f, rng: %.1f, rngStep: %.1f, oldRngMin: %.1f, oldRngMax: %.1f, dfltRng: %.1f, numBufVals: %d", + currMinVal, dataBuf.getMax(numBufVals), rngMin, rngMid, rngMax, rng, rngStep, oldRngMin, oldRngMax, dfltRng, numBufVals); + + if (currMinVal != dbMAX_VAL) { // current min value is valid + if (currMinVal > 0 && dbMIN_VAL == 0) { // Chart range starts at least at '0' or includes negative values + rngMin = 0; + } else if (currMinVal < oldRngMin || (oldRngMin < 0 && (currMinVal > (oldRngMin + rngStep)))) { // decrease rngMin if required or increase if lowest value is higher than old rngMin + rngMin = std::floor(currMinVal / rngStep) * rngStep; + } + } // otherwise keep rngMin unchanged + + double currMaxVal = dataBuf.getMax(numBufVals); + if (currMaxVal != dbMAX_VAL) { // current max value is valid + if ((currMaxVal > oldRngMax) || (currMaxVal < (oldRngMax - rngStep))) { // increase rngMax if required or decrease if lowest value is lower than old rngMax + rngMax = std::ceil(currMaxVal / rngStep) * rngStep; + rngMax = std::max(rngMax, rngMin + dfltRng); // keep at least default chart range + } + } // otherwise keep rngMax unchanged + + rngMid = (rngMin + rngMax) / 2.0; + rng = rngMax - rngMin; + LOG_DEBUG(GwLog::DEBUG, "calcChrtRange1a: currMinVal: %.1f, currMaxVal: %.1f, rngMin: %.1f, rngMid: %.1f, rngMax: %.1f, rng: %.1f, rngStep: %.1f, oldRngMin: %.1f, oldRngMax: %.1f, dfltRng: %.1f, numBufVals: %d", + currMinVal, currMaxVal, rngMin, rngMid, rngMax, rng, rngStep, oldRngMin, oldRngMax, dfltRng, numBufVals); + } else { + + if (chrtDataFmt == 1) { + // Chart data is of type 'course' or 'wind' + + if ((count == 1 && rngMid == 0) || rngMid == dbMAX_VAL) { + recalcRngCntr = true; // initialize + } + + // Set rngMid + if (recalcRngCntr) { + rngMid = dataBuf.getMid(numBufVals); + if (rngMid == dbMAX_VAL) { + rngMid = 0; + } else { + rngMid = std::round(rngMid / rngStep) * rngStep; // Set new center value; round to next value + + // Check if range between 'min' and 'max' is > 180° or crosses '0' + rngMin = dataBuf.getMin(numBufVals); + rngMax = dataBuf.getMax(numBufVals); + rng = (rngMax >= rngMin ? rngMax - rngMin : M_TWOPI - rngMin + rngMax); + rng = max(rng, dfltRng); // keep at least default chart range + if (rng > M_PI) { // If wind range > 180°, adjust wndCenter to smaller wind range end + rngMid = WindUtils::to2PI(rngMid + M_PI); + } + } + recalcRngCntr = false; // Reset flag for determination + LOG_DEBUG(GwLog::DEBUG, "calcChrtRange1b: rngMid: %.1f°, rngMin: %.1f°, rngMax: %.1f°, rng: %.1f°, rngStep: %.1f°", rngMid * RAD_TO_DEG, rngMin * RAD_TO_DEG, rngMax * RAD_TO_DEG, + rng * RAD_TO_DEG, rngStep * RAD_TO_DEG); + } + + } else if (chrtDataFmt == 2) { + // Chart data is of type 'rotation'; then we want to have always to be '0' + rngMid = 0; + } + + // check and adjust range between left, center, and right chart limit + double halfRng = rng / 2.0; // we calculate with range between and edges + double diffRng = getRng(rngMid, numBufVals); + // LOG_DEBUG(GwLog::DEBUG, "calcChrtRange2: diffRng: %.1f°, halfRng: %.1f°", diffRng * RAD_TO_DEG, halfRng * RAD_TO_DEG); + diffRng = (diffRng == dbMAX_VAL ? 0 : std::ceil(diffRng / rngStep) * rngStep); + // LOG_DEBUG(GwLog::DEBUG, "calcChrtRange2: diffRng: %.1f°, halfRng: %.1f°", diffRng * RAD_TO_DEG, halfRng * RAD_TO_DEG); + + if (diffRng > halfRng) { + halfRng = diffRng; // round to next value + } else if (diffRng + rngStep < halfRng) { // Reduce chart range for higher resolution if possible + halfRng = max(dfltRng / 2.0, diffRng); + } + + rngMin = WindUtils::to2PI(rngMid - halfRng); + rngMax = (halfRng < M_PI ? rngMid + halfRng : rngMid + halfRng - (M_TWOPI / 360)); // if chart range is 360°, then make 1° smaller than + rngMax = WindUtils::to2PI(rngMax); + // LOG_DEBUG(GwLog::DEBUG, "calcChrtRange2: diffRng: %.1f°, halfRng: %.1f°", diffRng * RAD_TO_DEG, halfRng * RAD_TO_DEG); + + rng = halfRng * 2.0; + LOG_DEBUG(GwLog::DEBUG, "calcChrtRange2b: rngMid: %.1f°, rngMin: %.1f°, rngMax: %.1f°, diffRng: %.1f°, rng: %.1f°, rngStep: %.1f°", rngMid * RAD_TO_DEG, rngMin * RAD_TO_DEG, rngMax * RAD_TO_DEG, + diffRng * RAD_TO_DEG, rng * RAD_TO_DEG, rngStep * RAD_TO_DEG); + } +} + +// chart time axis label + lines +template +void Chart::drawChrtTimeAxis(int8_t chrtIntv) +{ + int timeRng; + float slots, intv, i; + char sTime[6]; + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setTextColor(fgColor); + + if (chrtDir == 0) { // horizontal chart + getdisplay().fillRect(0, top, dWidth, 2, fgColor); + + timeRng = chrtIntv * 4; // Chart time interval: [1] 4 min., [2] 8 min., [3] 12 min., [4] 16 min., [8] 32 min. + slots = timAxis / 80.0; // number of axis labels + intv = timeRng / slots; // minutes per chart axis interval + i = timeRng; // Chart axis label start at -32, -16, -12, ... minutes + + for (int j = 0; j < timAxis - 30; j += 80) { // fill time axis with values but keep area free on right hand side for value label + // LOG_DEBUG(GwLog::DEBUG, "ChartTimeAxis: timAxis: %d, {x,y}: {%d,%d}, i: %.1f, j: %d, chrtIntv: %d, intv: %.1f, slots: %.1f", timAxis, cStart.x, cStart.y, i, j, chrtIntv, intv, slots); + + // Format time label based on interval + if (chrtIntv < 3) { + snprintf(sTime, sizeof(sTime), "-%.1f", i); + } else { + snprintf(sTime, sizeof(sTime), "-%.0f", std::round(i)); + } + + // draw text with appropriate offset + // int tOffset = (j == 0) ? 13 : (chrtIntv < 3 ? -4 : -4); + int tOffset = j == 0 ? 13 : -4; + drawTextCenter(cStart.x + j + tOffset, cStart.y - 8, sTime); + getdisplay().drawLine(cStart.x + j, cStart.y, cStart.x + j, cStart.y + 5, fgColor); // draw short vertical time mark + + i -= intv; + } + + } else { // chrtDir == 1; vertical chart + timeRng = chrtIntv * 4; // chart time interval: [1] 4 min., [2] 8 min., [3] 12 min., [4] 16 min., [8] 32 min. + slots = timAxis / 75.0; // number of axis labels + intv = timeRng / slots; // minutes per chart axis interval + i = -intv; // chart axis label start at -32, -16, -12, ... minutes + + for (int j = 75; j < (timAxis - 75); j += 75) { // don't print time label at upper and lower end of time axis + if (chrtIntv < 3) { // print 1 decimal if time range is single digit (4 or 8 minutes) + snprintf(sTime, sizeof(sTime), "%.1f", i); + } else { + snprintf(sTime, sizeof(sTime), "%.0f", std::floor(i)); + } + + getdisplay().drawLine(cStart.x, cStart.y + j, cStart.x + valAxis, cStart.y + j, fgColor); // Grid line + + if (chrtSz == 0) { // full size chart + getdisplay().fillRect(0, cStart.y + j - 9, 32, 15, bgColor); // clear small area to remove potential chart lines + getdisplay().setCursor((4 - strlen(sTime)) * 7, cStart.y + j + 3); // time value; print left screen; value right-formated + getdisplay().printf("%s", sTime); // Range value + } else if (chrtSz == 2) { // half size chart; right side + drawTextCenter(dWidth / 2, cStart.y + j, sTime); // time value; print mid screen + } + + i -= intv; + } + } +} + +// chart value axis labels + lines +template +void Chart::drawChrtValAxis() +{ + double slots; + int i, intv; + double cVal, cchrtRng, crngMin; + char sVal[6]; + std::unique_ptr tmpBVal; // Temp variable to get formatted and converted data value from OBP60Formatter + tmpBVal = std::unique_ptr(new GwApi::BoatValue(dataBuf.getName())); + tmpBVal->setFormat(dataBuf.getFormat()); + tmpBVal->valid = true; + + if (chrtDir == 0) { // horizontal chart + slots = valAxis / 60.0; // number of axis labels + tmpBVal->value = chrtRng; + cchrtRng = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) + intv = static_cast(round(cchrtRng / slots)); + i = intv; + + getdisplay().setFont(&Ubuntu_Bold10pt8b); + + for (int j = 60; j < valAxis - 30; j += 60) { + LOG_DEBUG(GwLog::DEBUG, "ChartValAxis: chrtRng: %.2f, cchrtRng: %.2f, intv: %d, slots: %.1f, valAxis: %d, i: %d, j: %d", chrtRng, cchrtRng, intv, slots, valAxis, i, j); + getdisplay().drawLine(cStart.x, cStart.y + j, cStart.x + timAxis, cStart.y + j, fgColor); + + getdisplay().fillRect(cStart.x, cStart.y + j - 9, cStart.x + 32, 18, bgColor); // Clear small area to remove potential chart lines + String sVal = String(i); + getdisplay().setCursor((3 - sVal.length()) * 8, cStart.y + j + 6); // value right-formated + getdisplay().printf("%s", sVal); // Range value + + i += intv; + } + + getdisplay().setFont(&Ubuntu_Bold12pt8b); + drawTextRalign(cStart.x + timAxis, cStart.y - 3, dbName); // buffer data name + + } else { // chrtDir == 1; vertical chart + getdisplay().setFont(&Ubuntu_Bold10pt8b); + + getdisplay().fillRect(cStart.x, top, valAxis, 2, fgColor); // top chart line + getdisplay().setCursor(cStart.x, cStart.y - 2); + tmpBVal->value = chrtMin; + cVal = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) + snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); + getdisplay().printf("%s", sVal); // Range low end + + tmpBVal->value = chrtMid; + cVal = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) + snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); + drawTextCenter(cStart.x + (valAxis / 2), cStart.y - 10, sVal); // Range mid end + + tmpBVal->value = chrtMax; + cVal = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) + snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); + drawTextRalign(cStart.x + valAxis - 1, cStart.y - 2, sVal); // Range high end + + for (int j = 0; j <= valAxis + 2; j += ((valAxis + 2) / 2)) { + getdisplay().drawLine(cStart.x + j, cStart.y, cStart.x + j, cStart.y + timAxis, fgColor); + } + + if (chrtSz == 0) { + getdisplay().setFont(&Ubuntu_Bold12pt8b); + drawTextCenter(cStart.x + (valAxis / 4) + 5, cStart.y - 11, dbName); // buffer data name + } + LOG_DEBUG(GwLog::DEBUG, "ChartGrd: chrtRng: %.2f, intv: %d, slots: %.1f, valAxis: %d, i: %d", chrtRng, intv, slots, valAxis, i); + } } // Print current data value template -void Chart::prntCurrValue(GwApi::BoatValue* currValue, const Pos chrtPos) +void Chart::prntCurrValue(GwApi::BoatValue& currValue, const Pos chrtPos) { int currentZone; static int lastZone = 0; static bool flipVal = false; int xPosVal; static const int yPosVal = (chrtDir == 0) ? cStart.y + valAxis - 5 : cStart.y + timAxis - 5; + xPosVal = cStart.x + 1; - // flexible move of location for latest boat data value, in case chart data is printed at the current location - /* xPosVal = flipVal ? 8 : valAxis - 135; - currentZone = (chrtPos.y >= yPosVal - 32) && (chrtPos.y <= yPosVal + 6) && (chrtPos.x >= xPosVal - 4) && (chrtPos.x <= xPosVal + 146) ? 1 : 0; // Define current zone for data value - if (currentZone != lastZone) { - // Only flip when x moves to a different zone - if ((chrtPos.y >= yPosVal - 32) && (chrtPos.y <= yPosVal + 6) && (chrtPos.x >= xPosVal - 3) && (chrtPos.x <= xPosVal + 146)) { - flipVal = !flipVal; - xPosVal = flipVal ? 8 : valAxis - 135; - } - } - lastZone = currentZone; */ - - xPosVal = (chrtDir == 0) ? cStart.x + timAxis - 117 : cStart.x + valAxis - 117; - FormattedData frmtDbData = formatValue(currValue, *commonData); + FormattedData frmtDbData = formatValue(&currValue, *commonData); double testdbValue = frmtDbData.value; String sdbValue = frmtDbData.svalue; // value (string) String dbUnit = frmtDbData.unit; // Unit of value - LOG_DEBUG(GwLog::DEBUG, "Chart CurrValue: dbValue: %.2f, sdbValue: %s, fmrtDbValue: %.2f, dbFormat: %s, dbUnit: %s, Valid: %d, Name: %s, Address: %p", currValue->value, sdbValue, - testdbValue, currValue->getFormat(), dbUnit, currValue->valid, currValue->getName(), (void*)currValue); - getdisplay().fillRect(xPosVal - 3, yPosVal - 34, 118, 40, bgColor); // Clear area for TWS value + LOG_DEBUG(GwLog::DEBUG, "Chart CurrValue: dbValue: %.2f, sdbValue: %s, fmrtDbValue: %.2f, dbFormat: %s, dbUnit: %s, Valid: %d, Name: %s, Address: %p", currValue.value, sdbValue, + testdbValue, currValue.getFormat(), dbUnit, currValue.valid, currValue.getName(), currValue); + + getdisplay().fillRect(xPosVal, yPosVal - 34, 121, 40, bgColor); // Clear area for TWS value getdisplay().setFont(&DSEG7Classic_BoldItalic16pt7b); - getdisplay().setCursor(xPosVal, yPosVal); - if (useSimuData) - getdisplay().printf("%2.1f", currValue->value); // Value - else + getdisplay().setCursor(xPosVal + 1, yPosVal); + if (useSimuData) { + getdisplay().printf("%2.1f", currValue.value); // Value + } else { getdisplay().print(sdbValue); // Value - // getdisplay().setFont(&Ubuntu_Bold12pt8b); - // getdisplay().setCursor(xPosVal + 76, yPosVal - 14); - // getdisplay().print(dbName); // Name + } + + getdisplay().setFont(&Ubuntu_Bold10pt8b); + getdisplay().setCursor(xPosVal + 76, yPosVal - 17); + getdisplay().print(dbName); // Name + getdisplay().setFont(&Ubuntu_Bold8pt8b); getdisplay().setCursor(xPosVal + 76, yPosVal + 1); getdisplay().print(dbUnit); // Unit } -// check and adjust chart range +// Identify Min and Max values of range for course data and select them considering smallest gap +// E.g., Min=30°, Max=270° will be converted to smaller range of Min=270° and Max=30° +// obsolete; creates random results by purpose with large data arrays when data is equally distributed template -void Chart::calcChrtRng() +void Chart::getAngleMinMax(const std::vector& angles, double& rngMin, double& rngMax) { - int diffRng; - - diffRng = dataBuf.getMax(numBufVals) / 1000; - if (diffRng > chrtRng) { - chrtRng = int((diffRng + (diffRng >= 0 ? 9 : -1)) / 10) * 10; // Round up to next 10 value - } else if (diffRng + 10 < chrtRng) { // Reduce chart range for higher resolution if possible - chrtRng = max(dfltRng, int((diffRng + (diffRng >= 0 ? 9 : -1)) / 10) * 10); + if (angles.empty()) { + rngMin = 0; + rngMax = 0; + return; } - LOG_DEBUG(GwLog::DEBUG, "Chart Range: diffRng: %d, chrtRng: %d, Min: %.0f, Max: %.0f", diffRng, chrtRng, dataBuf.getMin(numBufVals) / 1000, dataBuf.getMax(numBufVals) / 1000); + + if (angles.size() == 1) { + rngMin = angles[0]; + rngMax = angles[0]; + return; + } + + // Sort angles + std::vector sorted = angles; + std::sort(sorted.begin(), sorted.end()); + + // Find the largest gap between consecutive angles + double maxGap = 0.0; + int maxGapIndex = 0; + for (size_t i = 0; i < sorted.size(); i++) { + double next = sorted[(i + 1) % sorted.size()]; + double curr = sorted[i]; + + // Calculate gap (wrapping around at 360°/2*Pi) + double gap = (i == sorted.size() - 1) ? (M_TWOPI - curr + next) : (next - curr); + + if (gap > maxGap) { + maxGap = gap; + maxGapIndex = i; + } + } + + // The range is on the opposite side of the largest gap + // Min is after the gap, max is before it + rngMin = sorted[(maxGapIndex + 1) % sorted.size()]; + rngMax = sorted[maxGapIndex]; } // Explicitly instantiate class with required data types to avoid linker errors template class Chart; -template class Chart; // --- Class Chart --------------- diff --git a/lib/obp60task/OBPcharts.h b/lib/obp60task/OBPcharts.h index 2494213..c33f52e 100644 --- a/lib/obp60task/OBPcharts.h +++ b/lib/obp60task/OBPcharts.h @@ -1,7 +1,5 @@ // Function lib for display of boat data in various chart formats #pragma once -#include -#include #include "Pagedata.h" struct Pos { @@ -20,7 +18,7 @@ protected: RingBuffer &dataBuf; // Buffer to display int8_t chrtDir; // Chart timeline direction: [0] = horizontal, [1] = vertical int8_t chrtSz; // Chart size: [0] = full size, [1] = half size left/top, [2] half size right/bottom - int dfltRng; // Default range of chart, e.g. 30 = [0..30] + double dfltRng; // Default range of chart, e.g. 30 = [0..30] uint16_t fgColor; // color code for any screen writing uint16_t bgColor; // color code for screen background bool useSimuData; // flag to indicate if simulation data is active @@ -34,12 +32,18 @@ protected: int dHeight; // Display height int timAxis, valAxis; // size of time and value chart axis Pos cStart; // start point of chart area - int chrtRng; // Range of buffer values from min to max value + double chrtRng; // Range of buffer values from min to max value + double chrtMin; // Range low end value + double chrtMax; // Range high end value + double chrtMid; // Range mid value + double rngStep; // Defines the step of adjustment (e.g. 10 m/s) for value axis range + bool recalcRngCntr = false; // Flag for re-calculation of mid value of chart for wind data types String dbName, dbFormat; // Name and format of data buffer - int16_t dbMAX_VAL; // Highest possible value of buffer of type -> indicates invalid value in buffer + int chrtDataFmt; // Data format of chart: [0] size values; [1] degree of course or wind; [2] rotational degrees + double dbMIN_VAL; // Lowest possible value of buffer of type + double dbMAX_VAL; // Highest possible value of buffer of type ; indicates invalid value in buffer size_t bufSize; // History buffer size: 1.920 values for 32 min. history chart - int intvBufSize; // Buffer size used for currently selected time interval int count; // current size of buffer int numBufVals; // number of wind values available for current interval selection int bufStart; // 1st data value in buffer to show @@ -49,14 +53,17 @@ protected: size_t lastAddedIdx = 0; // Last index of TWD history buffer when new data was added int oldChrtIntv = 0; // remember recent user selection of data interval - void calcChrtRng(); - void drawChrtValAxis(); + void drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue); // Draw chart line + double getRng(double center, size_t amount); // Calculate range between chart center and edges + void calcChrtBorders(double& rngMid, double& rngMin, double& rngMax, double& rng); // Calculate chart points for value axis and return range between and + void drawChrtTimeAxis(int8_t chrtIntv); // Draw time axis of chart, value and lines + void drawChrtValAxis(); // Draw value axis of chart, value and lines + void prntCurrValue(GwApi::BoatValue& currValue, Pos chrtPos); // Add current boat data value to chart + void getAngleMinMax(const std::vector& angles, double& rngMin, double& rngMax); // Identify Min and Max for course data with smallest gap public: - Chart(RingBuffer& dataBuf, int8_t chrtDir, int8_t chrtSz, int dfltRng, CommonData& common, bool useSimuData); + Chart(RingBuffer& dataBuf, int8_t chrtDir, int8_t chrtSz, double dfltRng, CommonData& common, bool useSimuData); // Chart object of data chart ~Chart(); - void drawChrtTimeAxis(int8_t chrtIntv); - void drawChrt(int8_t chrtIntv, GwApi::BoatValue currValue); - void prntCurrValue(GwApi::BoatValue* currValue, Pos chrtPos); + void showChrt(int8_t chrtIntv, GwApi::BoatValue currValue); // Perform all actions to draw chart }; \ No newline at end of file diff --git a/lib/obp60task/PageWindPlot.cpp b/lib/obp60task/PageWindPlot.cpp index eb2d079..4a115dc 100644 --- a/lib/obp60task/PageWindPlot.cpp +++ b/lib/obp60task/PageWindPlot.cpp @@ -184,22 +184,24 @@ public: GwConfigHandler* config = commonData->config; GwLog* logger = commonData->logger; - static RingBuffer* wdHstry; // Wind direction data buffer + static RingBuffer* wdHstry; // Wind direction data buffer static RingBuffer* wsHstry; // Wind speed data buffer static String wdName, wdFormat; // Wind direction name and format static String wsName, wsFormat; // Wind speed name and format static int16_t wdMAX_VAL; // Max. value of wd history buffer, indicating invalid values - static std::unique_ptr> twsFlChart; // chart object for wind speed chart - static std::unique_ptr> twdHfChart; // chart object for wind direction chart - static std::unique_ptr> twsHfChart; // chart object for wind speed chart - float wsValue; // Wind speed value in chart area - String wsUnit; // Wind speed unit in chart area + static std::unique_ptr> twdFlChart; // chart object for wind direction chart, full size + static std::unique_ptr> twsFlChart; // chart object for wind speed chart, full size + static std::unique_ptr> twdHfChart; // chart object for wind direction chart, half size + static std::unique_ptr> twsHfChart; // chart object for wind speed chart, half size +// float wsValue; // Wind speed value in chart area +// String wsUnit; // Wind speed unit in chart area + static GwApi::BoatValue* wdBVal = new GwApi::BoatValue("TWD"); // temp BoatValue for wind direction unit identification; required by OBP60Formater static GwApi::BoatValue* wsBVal = new GwApi::BoatValue("TWS"); // temp BoatValue for wind speed unit identification; required by OBP60Formater - // current boat data values; TWD/AWD only for validation test - const int numBoatData = 2; - GwApi::BoatValue* bvalue; - bool BDataValid[numBoatData]; + // current boat data values + const int numBoatData = 4; + GwApi::BoatValue* bvalue[numBoatData]; + // bool BDataValid[numBoatData]; static bool isInitialized = false; // Flag to indicate that page is initialized static bool wndDataValid = false; // Flag to indicate if wind data is valid @@ -224,7 +226,10 @@ public: static int wndCenter; // chart wind center value position static int wndLeft; // chart wind left value position static int wndRight; // chart wind right value position - static int chrtRng; // Range of wind values from mid wind value to min/max wind value in degrees + static int chrtRng; // Range of wind values from min to max wind value in degrees + double dfltRngWd; // default range for course chart from min to max value in degrees + double dfltRngWs; // defautl range for wind speed chart from min to max value in m/s + int diffRng; // Difference between mid and current wind value static const int dfltRng = 60; // Default range for chart int midWndDir; // New value for wndCenter after chart start / shift @@ -246,7 +251,7 @@ public: numNoData = 0; bufStart = 0; oldDataIntv = 0; - wsValue = 0; + // wsValue = 0; numAddedBufVals, currIdx, lastIdx = 0; wndCenter = INT_MAX; midWndDir = 0; @@ -256,10 +261,10 @@ public: isInitialized = true; // Set flag to indicate that page is now initialized } - // read boat data values; TWD/AWS only for validation test + // read boat data values for (int i = 0; i < numBoatData; i++) { - bvalue = pageData.values[i]; - BDataValid[i] = bvalue->valid; + bvalue[i] = pageData.values[i]; + // BDataValid[i] = bvalue->valid; } // Optical warning by limit violation (unused) @@ -280,13 +285,17 @@ public: wsHstry->getMetaData(wsName, wsFormat); wdMAX_VAL = wdHstry->getMaxVal(); bufSize = wdHstry->getCapacity(); - wsBVal->setFormat(wsHstry->getFormat()); + wdBVal->setFormat(wdFormat); + wsBVal->setFormat(wsFormat); lastAddedIdx = wdHstry->getLastIdx(); - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot twsChart: *wsHstry: %p", wsHstry); - twsFlChart = std::unique_ptr>(new Chart(*wsHstry, 0, 0, 15, *commonData, useSimuData)); - twdHfChart = std::unique_ptr>(new Chart(*wdHstry, 1, 1, 15, *commonData, useSimuData)); - twsHfChart = std::unique_ptr>(new Chart(*wsHstry, 1, 2, 15, *commonData, useSimuData)); + dfltRngWd = 60.0 * DEG_TO_RAD; // default range for course chart: 60° + dfltRngWs = 7.5; // default range for wind speed chart: 7.5 m/s + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: *wdHstry: %p, *wsHstry: %p", wdHstry, wsHstry); + twdFlChart = std::unique_ptr>(new Chart(*wdHstry, 1, 0, dfltRngWd, *commonData, useSimuData)); + twsFlChart = std::unique_ptr>(new Chart(*wsHstry, 0, 0, dfltRngWs, *commonData, useSimuData)); + twdHfChart = std::unique_ptr>(new Chart(*wdHstry, 1, 1, dfltRngWd, *commonData, useSimuData)); + twsHfChart = std::unique_ptr>(new Chart(*wsHstry, 1, 2, dfltRngWs, *commonData, useSimuData)); oldShowTruW = showTruW; } @@ -295,7 +304,7 @@ public: getdisplay().setPartialWindow(0, 0, width, height); // Set partial update getdisplay().setTextColor(commonData->fgcolor); - if (chrtMode == 'D') { +/* if (chrtMode == 'D') { // Identify buffer size and buffer start position for chart count = wdHstry->getCurrentSize(); currIdx = wdHstry->getLastIdx(); @@ -478,7 +487,7 @@ public: } else { getdisplay().printf("%4.1f", wsValue); // Value, round to 1 decimal } - } */ + } */ /* getdisplay().setFont(&Ubuntu_Bold12pt8b); getdisplay().setCursor(xPosTws + 82, yPosTws - 14); getdisplay().print(wsName); // Name @@ -512,19 +521,30 @@ public: } getdisplay().printf("%3d", chrtLbl); // Wind value label } +*/ + if (chrtMode == 'D') { + wdBVal->value = wdHstry->getLast(); + wdBVal->valid = wdBVal->value != wdHstry->getMaxVal(); +// twdFlChart->showChrt(dataIntv, *wdBVal); + twdFlChart->showChrt(dataIntv, *bvalue[0]); + } else if (chrtMode == 'S') { -// wsValue = wsHstry->getLast(); - twsFlChart->drawChrtTimeAxis(dataIntv); - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot chart: wsBVal.name: %s, format: %s, wsBVal.value: %.1f, address: %p", wsBVal->getName(), wsBVal->getFormat(), wsBVal->value, wsBVal); - twsFlChart->drawChrt(dataIntv, *wsBVal); + wsBVal->value = wsHstry->getLast(); + wsBVal->valid = wsBVal->value != wsHstry->getMaxVal(); +// twsFlChart->showChrt(dataIntv, *wsBVal); + twsFlChart->showChrt(dataIntv, *bvalue[1]); } else if (chrtMode == 'B') { -// wsValue = wsHstry->getLast(); - twdHfChart->drawChrtTimeAxis(dataIntv); - twsHfChart->drawChrtTimeAxis(dataIntv); - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot chart: wsBVal.name: %s, format: %s, wsBVal.value: %.1f, address: %p", wsBVal->getName(), wsBVal->getFormat(), wsBVal->value, wsBVal); - twdHfChart->drawChrt(dataIntv, *wsBVal); - twsHfChart->drawChrt(dataIntv, *wsBVal); + wdBVal->value = wdHstry->getLast(); + wdBVal->valid = wdBVal->value != wdHstry->getMaxVal(); + wsBVal->value = wsHstry->getLast(); + wsBVal->valid = wsBVal->value != wsHstry->getMaxVal(); + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot showChrt: wsBVal.name: %s, format: %s, wsBVal.value: %.1f, valid: %d, address: %p", wsBVal->getName(), wsBVal->getFormat(), wsBVal->value, + wsBVal->valid, wsBVal); +// twdHfChart->showChrt(dataIntv, *wdBVal); +// twsHfChart->showChrt(dataIntv, *wsBVal); + twdHfChart->showChrt(dataIntv, *bvalue[0]); + twsHfChart->showChrt(dataIntv, *bvalue[1]); } LOG_DEBUG(GwLog::DEBUG, "PageWindPlot time: %ld", millis() - timer); @@ -546,7 +566,7 @@ PageDescription registerPageWindPlot( "WindPlot", // Page name createPage, // Action 0, // Number of bus values depends on selection in Web configuration - { "TWD", "AWD" }, // Bus values we need in the page + { "TWD", "TWS", "AWD", "AWS" }, // Bus values we need in the page true // Show display header on/off ); diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index b1c0e01..75796ec 100644 --- a/lib/obp60task/obp60task.cpp +++ b/lib/obp60task/obp60task.cpp @@ -806,7 +806,7 @@ void OBP60Task(GwApi *api){ if (calcTrueWnds) { trueWind.addTrueWind(api, &boatValues, logger); } - // Handle history buffers for TWD, TWS for wind plot page and other usage + // Handle history buffers for certain boat data for windplot page and other usage hstryBufList.handleHstryBuf(useSimuData); // Clear display From 942ca28ab54a717e7cef952b20b6391c4d8b899c Mon Sep 17 00:00:00 2001 From: Ulrich Meine Date: Sat, 22 Nov 2025 19:59:43 +0100 Subject: [PATCH 053/121] Clean PageWindPlot to adjust to new OBPcharts setup --- lib/obp60task/PageWindPlot.cpp | 396 +++------------------------------ 1 file changed, 25 insertions(+), 371 deletions(-) diff --git a/lib/obp60task/PageWindPlot.cpp b/lib/obp60task/PageWindPlot.cpp index 4a115dc..4c5216c 100644 --- a/lib/obp60task/PageWindPlot.cpp +++ b/lib/obp60task/PageWindPlot.cpp @@ -1,83 +1,18 @@ #if defined BOARD_OBP60S3 || defined BOARD_OBP40S3 #include "Pagedata.h" -#include "BoatDataCalibration.h" #include "OBP60Extensions.h" -#include "OBPDataOperations.h" -#include "OBPRingBuffer.h" #include "OBPcharts.h" -#include - -static const double radToDeg = 180.0 / M_PI; // Conversion factor from radians to degrees - -// Get maximum difference of last of TWD ringbuffer values to center chart; returns "0" if data is not valid -int getCntr(const RingBuffer& windDirHstry, size_t amount) -{ - const int MAX_VAL = windDirHstry.getMaxVal(); - size_t count = windDirHstry.getCurrentSize(); - - if (windDirHstry.isEmpty() || amount <= 0) { - return 0; - } - if (amount > count) - amount = count; - - uint16_t midWndDir, minWndDir, maxWndDir = 0; - int wndCenter = 0; - - midWndDir = windDirHstry.getMid(amount); - if (midWndDir != MAX_VAL) { - midWndDir = midWndDir / 1000.0 * radToDeg; - wndCenter = int((midWndDir + (midWndDir >= 0 ? 5 : -5)) / 10) * 10; // Set new center value; round to nearest 10 degree value - minWndDir = windDirHstry.getMin(amount) / 1000.0 * radToDeg; - maxWndDir = windDirHstry.getMax(amount) / 1000.0 * radToDeg; - if ((maxWndDir - minWndDir) > 180 && !(minWndDir > maxWndDir)) { // if wind range is > 180 and no 0° crossover, adjust wndCenter to smaller wind range end - wndCenter = WindUtils::to360(wndCenter + 180); - } - } - - return wndCenter; -} - -// Get maximum difference of last of TWD ringbuffer values to center chart -int getRng(const RingBuffer& windDirHstry, int center, size_t amount) -{ - int minVal = windDirHstry.getMinVal(); - const int MAX_VAL = windDirHstry.getMaxVal(); - size_t count = windDirHstry.getCurrentSize(); - - if (windDirHstry.isEmpty() || amount <= 0) { - return MAX_VAL; - } - if (amount > count) - amount = count; - - int value = 0; - int rng = 0; - int maxRng = minVal; - // Start from the newest value (last) and go backwards x times - for (size_t i = 0; i < amount; i++) { - value = windDirHstry.get(count - 1 - i); - - if (value == MAX_VAL) { - continue; // ignore invalid values - } - - value = value / 1000.0 * radToDeg; - rng = abs(((value - center + 540) % 360) - 180); - if (rng > maxRng) - maxRng = rng; - } - if (maxRng > 180) { - maxRng = 180; - } - - return (maxRng != minVal ? maxRng : MAX_VAL); -} // **************************************************************** class PageWindPlot : public Page { +private: + GwLog* logger; + + int width; // Screen width + int height; // Screen height + bool keylock = false; // Keylock char chrtMode = 'D'; // Chart mode: 'D' for TWD, 'S' for TWS, 'B' for both bool showTruW = true; // Show true wind or apparant wind in chart area @@ -93,7 +28,8 @@ public: PageWindPlot(CommonData& common) { commonData = &common; - common.logger->logDebug(GwLog::LOG, "Instantiate PageWindPlot"); + logger = commonData->logger; + LOG_DEBUG(GwLog::LOG, "Instantiate PageWindPlot"); // Get config data useSimuData = common.config->getBool(common.config->useSimuData); @@ -174,97 +110,41 @@ public: } else { showTruW = false; // Wind source is apparant wind } - commonData->logger->logDebug(GwLog::LOG, "New PageWindPlot: wind source=%s", wndSrc); + LOG_DEBUG(GwLog::LOG, "New PageWindPlot; wind source=%s", wndSrc); + // commonData->logger->logDebug(GwLog::LOG, "New PageWindPlot: wind source=%s", wndSrc); #endif oldShowTruW = !showTruW; // makes wind source being initialized at initial page call + + width = getdisplay().width(); // Screen width + height = getdisplay().height(); // Screen height } int displayPage(PageData& pageData) { GwConfigHandler* config = commonData->config; - GwLog* logger = commonData->logger; static RingBuffer* wdHstry; // Wind direction data buffer static RingBuffer* wsHstry; // Wind speed data buffer static String wdName, wdFormat; // Wind direction name and format static String wsName, wsFormat; // Wind speed name and format - static int16_t wdMAX_VAL; // Max. value of wd history buffer, indicating invalid values static std::unique_ptr> twdFlChart; // chart object for wind direction chart, full size static std::unique_ptr> twsFlChart; // chart object for wind speed chart, full size static std::unique_ptr> twdHfChart; // chart object for wind direction chart, half size static std::unique_ptr> twsHfChart; // chart object for wind speed chart, half size -// float wsValue; // Wind speed value in chart area -// String wsUnit; // Wind speed unit in chart area static GwApi::BoatValue* wdBVal = new GwApi::BoatValue("TWD"); // temp BoatValue for wind direction unit identification; required by OBP60Formater - static GwApi::BoatValue* wsBVal = new GwApi::BoatValue("TWS"); // temp BoatValue for wind speed unit identification; required by OBP60Formater + static GwApi::BoatValue* wsBVal = new GwApi::BoatValue("TWS"); // temp BoatValue for wind speed unit identification; required by OBP60Formater */ + double dfltRngWd = 60.0 * DEG_TO_RAD; // default range for course chart from min to max value in RAD + double dfltRngWs = 7.5; // default range for wind speed chart from min to max value in m/s - // current boat data values const int numBoatData = 4; - GwApi::BoatValue* bvalue[numBoatData]; - // bool BDataValid[numBoatData]; - - static bool isInitialized = false; // Flag to indicate that page is initialized - static bool wndDataValid = false; // Flag to indicate if wind data is valid - static int numNoData; // Counter for multiple invalid data values in a row - - static int width; // Screen width - static int height; // Screen height - static int xCenter; // Center of screen in x direction - static const int yOffset = 48; // Offset for y coordinates of chart area - static int cHeight; // height of chart area - static int bufSize; // History buffer size: 1.920 values for 32 min. history chart - static int intvBufSize; // Buffer size used for currently selected time interval - int count; // current size of buffer - static int numWndVals; // number of wind values available for current interval selection - static int bufStart; // 1st data value in buffer to show - int numAddedBufVals; // Number of values added to buffer since last display - size_t currIdx; // Current index in TWD history buffer - static size_t lastIdx; // Last index of TWD history buffer - static size_t lastAddedIdx = 0; // Last index of TWD history buffer when new data was added - static int oldDataIntv; // remember recent user selection of data interval - - static int wndCenter; // chart wind center value position - static int wndLeft; // chart wind left value position - static int wndRight; // chart wind right value position - static int chrtRng; // Range of wind values from min to max wind value in degrees - double dfltRngWd; // default range for course chart from min to max value in degrees - double dfltRngWs; // defautl range for wind speed chart from min to max value in m/s - - int diffRng; // Difference between mid and current wind value - static const int dfltRng = 60; // Default range for chart - int midWndDir; // New value for wndCenter after chart start / shift - - int x, y; // x and y coordinates for drawing - static int prevX, prevY; // Last x and y coordinates for drawing - static float chrtScl; // Scale for wind values in pixels per degree - int chrtVal; // Current wind value - static int chrtPrevVal; // Last wind value in chart area for check if value crosses 180 degree line + GwApi::BoatValue* bvalue[numBoatData]; // current boat data values LOG_DEBUG(GwLog::LOG, "Display PageWindPlot"); - ulong timer = millis(); - - if (!isInitialized) { - width = getdisplay().width(); - height = getdisplay().height(); - xCenter = width / 2; - cHeight = height - yOffset - 22; - numNoData = 0; - bufStart = 0; - oldDataIntv = 0; - // wsValue = 0; - numAddedBufVals, currIdx, lastIdx = 0; - wndCenter = INT_MAX; - midWndDir = 0; - diffRng = dfltRng; - chrtRng = dfltRng; - - isInitialized = true; // Set flag to indicate that page is now initialized - } + ulong pageTime = millis(); // read boat data values for (int i = 0; i < numBoatData; i++) { bvalue[i] = pageData.values[i]; - // BDataValid[i] = bvalue->valid; } // Optical warning by limit violation (unused) @@ -283,14 +163,7 @@ public: } wdHstry->getMetaData(wdName, wdFormat); wsHstry->getMetaData(wsName, wsFormat); - wdMAX_VAL = wdHstry->getMaxVal(); - bufSize = wdHstry->getCapacity(); - wdBVal->setFormat(wdFormat); - wsBVal->setFormat(wsFormat); - lastAddedIdx = wdHstry->getLastIdx(); - dfltRngWd = 60.0 * DEG_TO_RAD; // default range for course chart: 60° - dfltRngWs = 7.5; // default range for wind speed chart: 7.5 m/s LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: *wdHstry: %p, *wsHstry: %p", wdHstry, wsHstry); twdFlChart = std::unique_ptr>(new Chart(*wdHstry, 1, 0, dfltRngWd, *commonData, useSimuData)); twsFlChart = std::unique_ptr>(new Chart(*wsHstry, 0, 0, dfltRngWs, *commonData, useSimuData)); @@ -300,238 +173,21 @@ public: oldShowTruW = showTruW; } + // Draw page + //*********************************************************** + // Set display in partial refresh mode getdisplay().setPartialWindow(0, 0, width, height); // Set partial update getdisplay().setTextColor(commonData->fgcolor); -/* if (chrtMode == 'D') { - // Identify buffer size and buffer start position for chart - count = wdHstry->getCurrentSize(); - currIdx = wdHstry->getLastIdx(); - numAddedBufVals = (currIdx - lastAddedIdx + bufSize) % bufSize; // Number of values added to buffer since last display - if (dataIntv != oldDataIntv || count == 1) { - // new data interval selected by user; this is only x * 230 values instead of 240 seconds (4 minutes) per interval step - intvBufSize = cHeight * dataIntv; - numWndVals = min(count, (cHeight - 60) * dataIntv); - bufStart = max(0, count - numWndVals); - lastAddedIdx = currIdx; - oldDataIntv = dataIntv; - } else { - numWndVals = numWndVals + numAddedBufVals; - lastAddedIdx = currIdx; - if (count == bufSize) { - bufStart = max(0, bufStart - numAddedBufVals); - } - } - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Dataset: count: %d, xWD: %.1f, xWS: %.2f, xWD_valid? %d, intvBufSize: %d, numWndVals: %d, bufStart: %d, numAddedBufVals: %d, lastIdx: %d, wind source: %s", - count, wdHstry->getLast() / 1000.0 * radToDeg, wsHstry->getLast() / 1000.0 * 1.94384, BDataValid[0], intvBufSize, numWndVals, bufStart, numAddedBufVals, wdHstry->getLastIdx(), - showTruW ? "True" : "App"); - - // Set wndCenter from 1st real buffer value - if (wndCenter == INT_MAX || (wndCenter == 0 && count == 1)) { - wndCenter = getCntr(*wdHstry, numWndVals); - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Range Init: count: %d, xWD: %.1f, wndCenter: %d, diffRng: %d, chrtRng: %d, Min: %.0f, Max: %.0f", count, wdHstry->getLast() / 1000.0 * radToDeg, - wndCenter, diffRng, chrtRng, wdHstry->getMin(numWndVals) / 1000.0 * radToDeg, wdHstry->getMax(numWndVals) / 1000.0 * radToDeg); - } else { - // check and adjust range between left, center, and right chart limit - diffRng = getRng(*wdHstry, wndCenter, numWndVals); - diffRng = (diffRng == wdMAX_VAL ? 0 : diffRng); - if (diffRng > chrtRng) { - chrtRng = int((diffRng + (diffRng >= 0 ? 9 : -1)) / 10) * 10; // Round up to next 10 degree value - } else if (diffRng + 10 < chrtRng) { // Reduce chart range for higher resolution if possible - chrtRng = max(dfltRng, int((diffRng + (diffRng >= 0 ? 9 : -1)) / 10) * 10); - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Range adjust: wndCenter: %d, diffRng: %d, chrtRng: %d, Min: %.0f, Max: %.0f", wndCenter, diffRng, chrtRng, - wdHstry->getMin(numWndVals) / 1000.0 * radToDeg, wdHstry->getMax(numWndVals) / 1000.0 * radToDeg); - } - } - chrtScl = float(width) / float(chrtRng) / 2.0; // Chart scale: pixels per degree - wndLeft = wndCenter - chrtRng; - if (wndLeft < 0) - wndLeft += 360; - wndRight = (chrtRng < 180 ? wndCenter + chrtRng : wndCenter + chrtRng - 1); - if (wndRight >= 360) - wndRight -= 360; - - // Draw page - //*********************************************************************** - - // chart lines - getdisplay().fillRect(0, yOffset, width, 2, commonData->fgcolor); - getdisplay().fillRect(xCenter, yOffset, 1, cHeight, commonData->fgcolor); - - // chart labels - char sWndLbl[4]; // char buffer for Wind angle label - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(xCenter - 88, yOffset - 3); - getdisplay().print(wdName); // Wind data name - snprintf(sWndLbl, size_t(sWndLbl), "%03d", (wndCenter < 0) ? (wndCenter + 360) : wndCenter); - drawTextCenter(xCenter, yOffset - 11, sWndLbl); - getdisplay().drawCircle(xCenter + 25, yOffset - 17, 2, commonData->fgcolor); // symbol - getdisplay().drawCircle(xCenter + 25, yOffset - 17, 3, commonData->fgcolor); // symbol - getdisplay().setCursor(1, yOffset - 3); - snprintf(sWndLbl, size_t(sWndLbl), "%03d", (wndLeft < 0) ? (wndLeft + 360) : wndLeft); - getdisplay().print(sWndLbl); // Wind left value - getdisplay().drawCircle(46, yOffset - 17, 2, commonData->fgcolor); // symbol - getdisplay().drawCircle(46, yOffset - 17, 3, commonData->fgcolor); // symbol - getdisplay().setCursor(width - 50, yOffset - 3); - snprintf(sWndLbl, size_t(sWndLbl), "%03d", (wndRight < 0) ? (wndRight + 360) : wndRight); - getdisplay().print(sWndLbl); // Wind right value - getdisplay().drawCircle(width - 5, yOffset - 17, 2, commonData->fgcolor); // symbol - getdisplay().drawCircle(width - 5, yOffset - 17, 3, commonData->fgcolor); // symbol - - if (wdHstry->getMax() == wdMAX_VAL) { - // only values in buffer -> no valid wind data available - wndDataValid = false; - } else if (!BDataValid[0] && !useSimuData) { - // currently no valid xWD data available and no simulation mode - numNoData++; - wndDataValid = true; - if (numNoData > 3) { - // If more than 4 invalid values in a row, send message - wndDataValid = false; - } - } else { - numNoData = 0; // reset data error counter - wndDataValid = true; // At least some wind data available - } - // Draw wind values in chart - //*********************************************************************** - if (wndDataValid) { - for (int i = 0; i < (numWndVals / dataIntv); i++) { - chrtVal = static_cast(wdHstry->get(bufStart + (i * dataIntv))); // show the latest wind values in buffer; keep 1st value constant in a rolling buffer - if (chrtVal == wdMAX_VAL) { - chrtPrevVal = wdMAX_VAL; - } else { - chrtVal = static_cast((chrtVal / 1000.0 * radToDeg) + 0.5); // Convert to degrees and round - x = ((chrtVal - wndLeft + 360) % 360) * chrtScl; - y = yOffset + cHeight - i; // Position in chart area - - if (i >= (numWndVals / dataIntv) - 1) // log chart data of 1 line (adjust for test purposes) - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Chart: i: %d, chrtVal: %d, bufStart: %d, count: %d, linesToShow: %d", i, chrtVal, bufStart, count, (numWndVals / dataIntv)); - - if ((i == 0) || (chrtPrevVal == wdMAX_VAL)) { - // just a dot for 1st chart point or after some invalid values - prevX = x; - prevY = y; - } else { - // cross borders check; shift values to [-180..0..180]; when crossing borders, range is 2x 180 degrees - int wndLeftDlt = -180 - ((wndLeft >= 180) ? (wndLeft - 360) : wndLeft); - int chrtVal180 = ((chrtVal + wndLeftDlt + 180) % 360 + 360) % 360 - 180; - int chrtPrevVal180 = ((chrtPrevVal + wndLeftDlt + 180) % 360 + 360) % 360 - 180; - if (((chrtPrevVal180 >= -180) && (chrtPrevVal180 < -90) && (chrtVal180 > 90)) || ((chrtPrevVal180 <= 179) && (chrtPrevVal180 > 90) && chrtVal180 <= -90)) { - // If current value crosses chart borders compared to previous value, split line - int xSplit = (((chrtPrevVal180 > 0 ? wndRight : wndLeft) - wndLeft + 360) % 360) * chrtScl; - getdisplay().drawLine(prevX, prevY, xSplit, y, commonData->fgcolor); - getdisplay().drawLine(prevX, prevY - 1, ((xSplit != prevX) ? xSplit : xSplit - 1), ((xSplit != prevX) ? y - 1 : y), commonData->fgcolor); - prevX = (((chrtVal180 > 0 ? wndRight : wndLeft) - wndLeft + 360) % 360) * chrtScl; - } - } - - // Draw line with 2 pixels width + make sure vertical line are drawn correctly - getdisplay().drawLine(prevX, prevY, x, y, commonData->fgcolor); - getdisplay().drawLine(prevX, prevY - 1, ((x != prevX) ? x : x - 1), ((x != prevX) ? y - 1 : y), commonData->fgcolor); - chrtPrevVal = chrtVal; - prevX = x; - prevY = y; - } - // Reaching chart area top end - if (i >= (cHeight - 1)) { - oldDataIntv = 0; // force reset of buffer start and number of values to show in next display loop - - int minWndDir = wdHstry->getMin(numWndVals) / 1000.0 * radToDeg; - int maxWndDir = wdHstry->getMax(numWndVals) / 1000.0 * radToDeg; - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot FreeTop: Minimum: %d, Maximum: %d, OldwndCenter: %d", minWndDir, maxWndDir, wndCenter); - // if (((minWndDir - wndCenter >= 0) && (minWndDir - wndCenter < 180)) || ((maxWndDir - wndCenter <= 0) && (maxWndDir - wndCenter >=180))) { - if ((wndRight > wndCenter && (minWndDir >= wndCenter && minWndDir <= wndRight)) || (wndRight <= wndCenter && (minWndDir >= wndCenter || minWndDir <= wndRight)) || (wndLeft < wndCenter && (maxWndDir <= wndCenter && maxWndDir >= wndLeft)) || (wndLeft >= wndCenter && (maxWndDir <= wndCenter || maxWndDir >= wndLeft))) { - // Check if all wind value are left or right of center value -> optimize chart center - wndCenter = getCntr(*wdHstry, numWndVals); - } - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot FreeTop: cHeight: %d, bufStart: %d, numWndVals: %d, wndCenter: %d", cHeight, bufStart, numWndVals, wndCenter); - break; - } - } - - // Print wind speed value - int currentZone; - static int lastZone = 0; - static bool flipTws = false; - int xPosTws; - static const int yPosTws = yOffset + 40; - - xPosTws = flipTws ? 20 : width - 145; - currentZone = (y >= yPosTws - 38) && (y <= yPosTws + 6) && (x >= xPosTws - 4) && (x <= xPosTws + 146) ? 1 : 0; // Define current zone for TWS value - if (currentZone != lastZone) { - // Only flip when x moves to a different zone - if ((y >= yPosTws - 38) && (y <= yPosTws + 6) && (x >= xPosTws - 4) && (x <= xPosTws + 146)) { - flipTws = !flipTws; - xPosTws = flipTws ? 20 : width - 145; - } - } - lastZone = currentZone; - - wsValue = wsHstry->getLast(); - wsBVal->value = wsValue / 1000.0; // temp variable to retreive data unit from OBP60Formater - wsBVal->valid = (static_cast(wsValue) != wsHstry->getMinVal()); - String swsValue = formatValue(wsBVal, *commonData).svalue; // value (string) - wsUnit = formatValue(wsBVal, *commonData).unit; // Unit of value - getdisplay().fillRect(xPosTws - 4, yPosTws - 38, 142, 44, commonData->bgcolor); // Clear area for TWS value - getdisplay().setFont(&DSEG7Classic_BoldItalic16pt7b); - getdisplay().setCursor(xPosTws, yPosTws); - getdisplay().print(swsValue); // Value - /* if (!wsBVal->valid) { - getdisplay().print("--.-"); - } else { - wsValue = wsValue / 1000.0 * 1.94384; // Wind speed value in knots - if (wsValue < 10.0) { - getdisplay().printf("!%3.1f", wsValue); // Value, round to 1 decimal - } else { - getdisplay().printf("%4.1f", wsValue); // Value, round to 1 decimal - } - } */ /* - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(xPosTws + 82, yPosTws - 14); - getdisplay().print(wsName); // Name - getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(xPosTws + 82, yPosTws + 1); - getdisplay().print(wsUnit); // Unit - - } else { - // No valid data available - LOG_DEBUG(GwLog::LOG, "PageWindPlot: No valid data available"); - getdisplay().setFont(&Ubuntu_Bold10pt8b); - getdisplay().fillRect(xCenter - 33, height / 2 - 20, 66, 24, commonData->bgcolor); // Clear area for message - drawTextCenter(xCenter, height / 2 - 10, "No data"); - } - - // chart Y axis labels; print at last to overwrite potential chart lines in label area - int yPos; - int chrtLbl; - getdisplay().setFont(&Ubuntu_Bold8pt8b); - for (int i = 1; i <= 3; i++) { - yPos = yOffset + (i * 60); - getdisplay().fillRect(0, yPos, width, 1, commonData->fgcolor); - getdisplay().fillRect(0, yPos - 8, 24, 16, commonData->bgcolor); // Clear small area to remove potential chart lines - getdisplay().setCursor(1, yPos + 4); - if (count >= intvBufSize) { - // Calculate minute value for label - chrtLbl = ((i - 1 + (prevY < yOffset + 30)) * dataIntv) * -1; // change label if last data point is more than 30 lines (= seconds) from chart line - } else { - int j = 3 - i; - chrtLbl = (int((((numWndVals / dataIntv) - 50) * dataIntv / 60) + 1) - (j * dataIntv)) * -1; // 50 lines left below last chart line - } - getdisplay().printf("%3d", chrtLbl); // Wind value label - } -*/ if (chrtMode == 'D') { wdBVal->value = wdHstry->getLast(); wdBVal->valid = wdBVal->value != wdHstry->getMaxVal(); -// twdFlChart->showChrt(dataIntv, *wdBVal); twdFlChart->showChrt(dataIntv, *bvalue[0]); } else if (chrtMode == 'S') { wsBVal->value = wsHstry->getLast(); wsBVal->valid = wsBVal->value != wsHstry->getMaxVal(); -// twsFlChart->showChrt(dataIntv, *wsBVal); twsFlChart->showChrt(dataIntv, *bvalue[1]); } else if (chrtMode == 'B') { @@ -539,17 +195,15 @@ public: wdBVal->valid = wdBVal->value != wdHstry->getMaxVal(); wsBVal->value = wsHstry->getLast(); wsBVal->valid = wsBVal->value != wsHstry->getMaxVal(); - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot showChrt: wsBVal.name: %s, format: %s, wsBVal.value: %.1f, valid: %d, address: %p", wsBVal->getName(), wsBVal->getFormat(), wsBVal->value, - wsBVal->valid, wsBVal); -// twdHfChart->showChrt(dataIntv, *wdBVal); -// twsHfChart->showChrt(dataIntv, *wsBVal); + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot showChrt: wsBVal.name: %s, format: %s, wsBVal.value: %.1f, valid: %d, address: %p", wsBVal->getName(), wsBVal->getFormat(), wsBVal->value, + wsBVal->valid, wsBVal); twdHfChart->showChrt(dataIntv, *bvalue[0]); twsHfChart->showChrt(dataIntv, *bvalue[1]); } - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot time: %ld", millis() - timer); + LOG_DEBUG(GwLog::LOG, "PageWindPlot: runtime: %ldms", millis() - pageTime); return PAGE_UPDATE; - }; + } }; static Page* createPage(CommonData& common) From f77107616d78e7f379f7b2b759b09d21b2b4c135 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Tue, 25 Nov 2025 18:02:40 +0100 Subject: [PATCH 054/121] Add new PageNavigation (not complete) --- lib/obp60task/PageNavigation.cpp | 142 +++++++++++++++++++++++++++++++ lib/obp60task/config.json | 10 +++ lib/obp60task/config_obp40.json | 10 +++ lib/obp60task/obp60task.cpp | 2 + lib/obp60task/platformio.ini | 4 +- 5 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 lib/obp60task/PageNavigation.cpp diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp new file mode 100644 index 0000000..9818838 --- /dev/null +++ b/lib/obp60task/PageNavigation.cpp @@ -0,0 +1,142 @@ +#if defined BOARD_OBP60S3 || defined BOARD_OBP40S3 + +#include "Pagedata.h" +#include "OBP60Extensions.h" + +class PageNavigation : public Page +{ +public: + PageNavigation(CommonData &common){ + commonData = &common; + common.logger->logDebug(GwLog::LOG,"Instantiate PageNavigation"); + } + + virtual int handleKey(int key){ + // Code for keylock + if(key == 11){ + commonData->keylock = !commonData->keylock; + return 0; // Commit the key + } + return key; + } + + int displayPage(PageData &pageData){ + GwConfigHandler *config = commonData->config; + GwLog *logger = commonData->logger; + + // Old values for hold function + static String svalue1old = ""; + static String unit1old = ""; + static String svalue2old = ""; + static String unit2old = ""; + static String svalue3old = ""; + static String unit3old = ""; + static String svalue4old = ""; + static String unit4old = ""; + + // Get config data + String lengthformat = config->getString(config->lengthFormat); + // bool simulation = config->getBool(config->useSimuData); + bool holdvalues = config->getBool(config->holdvalues); + String flashLED = config->getString(config->flashLED); + String backlightMode = config->getString(config->backlight); + + // Get boat values #1 + GwApi::BoatValue *bvalue1 = pageData.values[0]; // First element in list (only one value by PageOneValue) + String name1 = xdrDelete(bvalue1->getName()); // Value name + name1 = name1.substring(0, 6); // String length limit for value name + double value1 = bvalue1->value; // Value as double in SI unit + bool valid1 = bvalue1->valid; // Valid information + String svalue1 = formatValue(bvalue1, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places + String unit1 = formatValue(bvalue1, *commonData).unit; // Unit of value + + // Get boat values #2 + GwApi::BoatValue *bvalue2 = pageData.values[1]; // Second element in list (only one value by PageOneValue) + String name2 = xdrDelete(bvalue2->getName()); // Value name + name2 = name2.substring(0, 6); // String length limit for value name + double value2 = bvalue2->value; // Value as double in SI unit + bool valid2 = bvalue2->valid; // Valid information + String svalue2 = formatValue(bvalue2, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places + String unit2 = formatValue(bvalue2, *commonData).unit; // Unit of value + + // Get boat values #3 + GwApi::BoatValue *bvalue3 = pageData.values[2]; // Second element in list (only one value by PageOneValue) + String name3 = xdrDelete(bvalue3->getName()); // Value name + name3 = name3.substring(0, 6); // String length limit for value name + double value3 = bvalue3->value; // Value as double in SI unit + bool valid3 = bvalue3->valid; // Valid information + String svalue3 = formatValue(bvalue3, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places + String unit3 = formatValue(bvalue3, *commonData).unit; // Unit of value + + // Get boat values #4 + GwApi::BoatValue *bvalue4 = pageData.values[3]; // Second element in list (only one value by PageOneValue) + String name4 = xdrDelete(bvalue4->getName()); // Value name + name4 = name4.substring(0, 6); // String length limit for value name + double value4 = bvalue4->value; // Value as double in SI unit + bool valid4 = bvalue4->valid; // Valid information + String svalue4 = formatValue(bvalue4, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places + String unit4 = formatValue(bvalue4, *commonData).unit; // Unit of value + + // Optical warning by limit violation (unused) + if(String(flashLED) == "Limit Violation"){ + setBlinkingLED(false); + setFlashLED(false); + } + + // Logging boat values + if (bvalue1 == NULL) return PAGE_OK; // WTF why this statement? + LOG_DEBUG(GwLog::LOG,"Drawing at PageNavigation, %s: %f, %s: %f, %s: %f, %s: %f", name1.c_str(), value1, name2.c_str(), value2, name3.c_str(), value3, name4.c_str(), value4); + + // Draw page + //*********************************************************** + + // Set display in partial refresh mode + getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + + getdisplay().setTextColor(commonData->fgcolor); + + // ############### Draw Navigation Map ################ + getdisplay().setFont(&Ubuntu_Bold12pt8b); + + getdisplay().setCursor(20, 60); + getdisplay().print(name1); + getdisplay().setCursor(120, 60); + getdisplay().print(svalue1); + + getdisplay().setCursor(20, 80); + getdisplay().print(name2); + getdisplay().setCursor(120, 80); + getdisplay().print(svalue2); + + getdisplay().setCursor(20, 100); + getdisplay().print(name3); + getdisplay().setCursor(120, 100); + getdisplay().print(svalue3); + + getdisplay().setCursor(20, 120); + getdisplay().print(name4); + getdisplay().setCursor(120, 120); + getdisplay().print(svalue4); + + return PAGE_UPDATE; + }; +}; + +static Page *createPage(CommonData &common){ + return new PageNavigation(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 registerPageNavigation( + "Navigation", // Page name + createPage, // Action + 0, // Number of bus values depends on selection in Web configuration + {"LAT","LON","HDT","SOG"}, // Bus values we need in the page + true // Show display header on/off +); + +#endif diff --git a/lib/obp60task/config.json b/lib/obp60task/config.json index f633762..2de3c59 100644 --- a/lib/obp60task/config.json +++ b/lib/obp60task/config.json @@ -1231,6 +1231,7 @@ "FourValues2", "Generator", "KeelPosition", + "Navigation", "OneValue", "RollPitch", "RudderPosition", @@ -1529,6 +1530,7 @@ "FourValues2", "Generator", "KeelPosition", + "Navigation", "OneValue", "RollPitch", "RudderPosition", @@ -1819,6 +1821,7 @@ "FourValues2", "Generator", "KeelPosition", + "Navigation", "OneValue", "RollPitch", "RudderPosition", @@ -2101,6 +2104,7 @@ "FourValues2", "Generator", "KeelPosition", + "Navigation", "OneValue", "RollPitch", "RudderPosition", @@ -2375,6 +2379,7 @@ "FourValues2", "Generator", "KeelPosition", + "Navigation", "OneValue", "RollPitch", "RudderPosition", @@ -2641,6 +2646,7 @@ "FourValues2", "Generator", "KeelPosition", + "Navigation", "OneValue", "RollPitch", "RudderPosition", @@ -2899,6 +2905,7 @@ "FourValues2", "Generator", "KeelPosition", + "Navigation", "OneValue", "RollPitch", "RudderPosition", @@ -3149,6 +3156,7 @@ "FourValues2", "Generator", "KeelPosition", + "Navigation", "OneValue", "RollPitch", "RudderPosition", @@ -3391,6 +3399,7 @@ "FourValues2", "Generator", "KeelPosition", + "Navigation", "OneValue", "RollPitch", "RudderPosition", @@ -3625,6 +3634,7 @@ "FourValues2", "Generator", "KeelPosition", + "Navigation", "OneValue", "RollPitch", "RudderPosition", diff --git a/lib/obp60task/config_obp40.json b/lib/obp60task/config_obp40.json index 9addd67..30c7b2d 100644 --- a/lib/obp60task/config_obp40.json +++ b/lib/obp60task/config_obp40.json @@ -1254,6 +1254,7 @@ "FourValues2", "Generator", "KeelPosition", + "Navigation", "OneValue", "RollPitch", "RudderPosition", @@ -1582,6 +1583,7 @@ "FourValues2", "Generator", "KeelPosition", + "Navigation", "OneValue", "RollPitch", "RudderPosition", @@ -1901,6 +1903,7 @@ "FourValues2", "Generator", "KeelPosition", + "Navigation", "OneValue", "RollPitch", "RudderPosition", @@ -2211,6 +2214,7 @@ "FourValues2", "Generator", "KeelPosition", + "Navigation", "OneValue", "RollPitch", "RudderPosition", @@ -2512,6 +2516,7 @@ "FourValues2", "Generator", "KeelPosition", + "Navigation", "OneValue", "RollPitch", "RudderPosition", @@ -2804,6 +2809,7 @@ "FourValues2", "Generator", "KeelPosition", + "Navigation", "OneValue", "RollPitch", "RudderPosition", @@ -3087,6 +3093,7 @@ "FourValues2", "Generator", "KeelPosition", + "Navigation", "OneValue", "RollPitch", "RudderPosition", @@ -3361,6 +3368,7 @@ "FourValues2", "Generator", "KeelPosition", + "Navigation", "OneValue", "RollPitch", "RudderPosition", @@ -3626,6 +3634,7 @@ "FourValues2", "Generator", "KeelPosition", + "Navigation", "OneValue", "RollPitch", "RudderPosition", @@ -3882,6 +3891,7 @@ "FourValues2", "Generator", "KeelPosition", + "Navigation", "OneValue", "RollPitch", "RudderPosition", diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index b1c0e01..a8590e0 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 registerPageNavigation; + list.add(®isterPageNavigation); } // Undervoltage detection for shutdown display diff --git a/lib/obp60task/platformio.ini b/lib/obp60task/platformio.ini index 16cd23c..91cbfe8 100644 --- a/lib/obp60task/platformio.ini +++ b/lib/obp60task/platformio.ini @@ -99,8 +99,8 @@ build_flags= -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 ${env.build_flags} upload_port = /dev/ttyUSB0 #OBP40 download via external USB/Serail converter upload_protocol = esptool #firmware upload via USB OTG seriell, by first upload need to set the ESP32-S3 in the upload mode with shortcut GND to Pin27 From 16f9f9217d277b5d1ff18b48f74bf6485282e159 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Tue, 25 Nov 2025 22:38:08 +0100 Subject: [PATCH 055/121] HTTP request for PageNavigation --- lib/obp60task/ImageDecoder.cpp | 14 + lib/obp60task/ImageDecoder.h | 9 + lib/obp60task/NetworkClient.cpp | 168 +++++++ lib/obp60task/NetworkClient.h | 25 + lib/obp60task/PageNavigation.cpp | 71 ++- lib/obp60task/platformio.ini | 4 + lib/obp60task/puff.c | 840 +++++++++++++++++++++++++++++++ lib/obp60task/puff.h | 35 ++ 8 files changed, 1158 insertions(+), 8 deletions(-) create mode 100644 lib/obp60task/ImageDecoder.cpp create mode 100644 lib/obp60task/ImageDecoder.h create mode 100644 lib/obp60task/NetworkClient.cpp create mode 100644 lib/obp60task/NetworkClient.h create mode 100644 lib/obp60task/puff.c create mode 100644 lib/obp60task/puff.h diff --git a/lib/obp60task/ImageDecoder.cpp b/lib/obp60task/ImageDecoder.cpp new file mode 100644 index 0000000..545ace6 --- /dev/null +++ b/lib/obp60task/ImageDecoder.cpp @@ -0,0 +1,14 @@ +#include "ImageDecoder.h" +#include + +// Decoder for Base64 content +bool ImageDecoder::decodeBase64(const String& base64, uint8_t* outBuffer, size_t outSize, size_t& decodedSize) { + int ret = mbedtls_base64_decode( + outBuffer, + outSize, + &decodedSize, + (const unsigned char*)base64.c_str(), + base64.length() + ); + return (ret == 0); +} diff --git a/lib/obp60task/ImageDecoder.h b/lib/obp60task/ImageDecoder.h new file mode 100644 index 0000000..473b168 --- /dev/null +++ b/lib/obp60task/ImageDecoder.h @@ -0,0 +1,9 @@ + +#pragma once +#include +#include + +class ImageDecoder { +public: + bool decodeBase64(const String& base64, uint8_t* outBuffer, size_t outSize, size_t& decodedSize); +}; diff --git a/lib/obp60task/NetworkClient.cpp b/lib/obp60task/NetworkClient.cpp new file mode 100644 index 0000000..9e75ae5 --- /dev/null +++ b/lib/obp60task/NetworkClient.cpp @@ -0,0 +1,168 @@ +#include "NetworkClient.h" + +extern "C" { + #include "puff.h" +} + +// Constructor +NetworkClient::NetworkClient(size_t reserveSize) + : _doc(reserveSize), + _valid(false) +{ +} + +// Skip GZIP Header an goto DEFLATE content +int NetworkClient::skipGzipHeader(const uint8_t* data, size_t len) { + if (len < 10) return -1; + + if (data[0] != 0x1F || data[1] != 0x8B || data[2] != 8) { + return -1; + } + + size_t pos = 10; + uint8_t flags = data[3]; + + if (flags & 4) { + if (pos + 2 > len) return -1; + uint16_t xlen = data[pos] | (data[pos+1] << 8); + pos += 2 + xlen; + } + + if (flags & 8) { + while (pos < len && data[pos] != 0) pos++; + pos++; + } + + if (flags & 16) { + while (pos < len && data[pos] != 0) pos++; + pos++; + } + + if (flags & 2) pos += 2; + + if (pos >= len) return -1; + + return pos; +} + +// HTTP GET + GZIP Decompression (reading in chunks) +bool NetworkClient::httpGetGzip(const String& url, uint8_t*& outData, size_t& outLen) { + + const size_t capacity = READLIMIT; // limit (can be adjusted in NetworkClient.h) + uint8_t* buffer = (uint8_t*)malloc(capacity); + + if (!buffer) { + if (DEBUG) {Serial.println("Malloc failed (buffer)");} + return false; + } + + HTTPClient http; + http.begin(url); + http.addHeader("Accept-Encoding", "gzip"); + + int code = http.GET(); + if (code != HTTP_CODE_OK) { + Serial.printf("HTTP ERROR: %d\n", code); + free(buffer); + http.end(); + return false; + } + + WiFiClient* stream = http.getStreamPtr(); + + size_t len = 0; + uint32_t lastData = millis(); + const uint32_t READ_TIMEOUT = NETWORKTIMEOUT; // Network timeout for reading data (can be adjusted in NetworkClient.h) + + bool complete = false; + + while (http.connected() && !complete) { + + size_t avail = stream->available(); + + if (avail == 0) { + if (millis() - lastData > READ_TIMEOUT) { + Serial.println("TIMEOUT waiting for data!"); + break; + } + delay(1); + continue; + } + + if (len + avail > capacity) + avail = capacity - len; + + int read = stream->readBytes(buffer + len, avail); + len += read; + lastData = millis(); + + if (DEBUG) {Serial.printf("Read chunk: %d (total: %d)\n", read, (int)len);} + + if (len < 20) continue; // Not enough data for header + + int headerOffset = skipGzipHeader(buffer, len); + if (headerOffset < 0) continue; + + unsigned long testLen = len * 8; // Dynamic expansion + uint8_t* test = (uint8_t*)malloc(testLen); + + if (!test) continue; + + unsigned long srcLen = len - headerOffset; + + int res = puff(test, &testLen, buffer + headerOffset, &srcLen); + if (res == 0) { + if (DEBUG) {Serial.printf("Decompress OK! Size: %lu bytes\n", testLen);} + outData = test; + outLen = testLen; + complete = true; + break; + } + + free(test); + } + + http.end(); + free(buffer); + + if (!complete) { + Serial.println("Failed to complete decompress."); + return false; + } + + return true; +} + +// Decompress JSON +bool NetworkClient::fetchAndDecompressJson(const String& url) { + + _valid = false; + + uint8_t* raw = nullptr; + size_t rawLen = 0; + + if (!httpGetGzip(url, raw, rawLen)) { + Serial.println("GZIP download/decompress failed."); + return false; + } + + DeserializationError err = deserializeJson(_doc, raw, rawLen); + free(raw); + + if (err) { + Serial.printf("JSON ERROR: %s\n", err.c_str()); + return false; + } + + if (DEBUG) {Serial.println("JSON OK!");} + _valid = true; + return true; +} + +JsonDocument& NetworkClient::json() { + return _doc; +} + +bool NetworkClient::isValid() const { + return _valid; +} diff --git a/lib/obp60task/NetworkClient.h b/lib/obp60task/NetworkClient.h new file mode 100644 index 0000000..1a29178 --- /dev/null +++ b/lib/obp60task/NetworkClient.h @@ -0,0 +1,25 @@ +#pragma once +#include +#include +#include + +#define DEBUG false // Debug flag for NetworkClient for more live information +#define READLIMIT 200000 // HTTP read limit in byte for gzip content (can be adjusted) +#define NETWORKTIMEOUT 8000 // 8s Network timeout + +class NetworkClient { +public: + NetworkClient(size_t reserveSize = 0); + + bool fetchAndDecompressJson(const String& url); + JsonDocument& json(); + bool isValid() const; + +private: + DynamicJsonDocument _doc; + bool _valid; + + int skipGzipHeader(const uint8_t* data, size_t len); + bool httpGetGzip(const String& url, uint8_t*& outData, size_t& outLen); +}; + diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp index 9818838..06b44a4 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -2,6 +2,16 @@ #include "Pagedata.h" #include "OBP60Extensions.h" +#include "NetworkClient.h" // Network connection +#include "ImageDecoder.h" // Image decoder for navigation map + +#include "Logo_OBP_400x300_sw.h" + +// Limits +#define JSON_BUFFER 30000 // Max buffer size for JSON content (30 kB picture + values) + +NetworkClient net(JSON_BUFFER); +ImageDecoder decoder; class PageNavigation : public Page { @@ -87,35 +97,80 @@ public: if (bvalue1 == NULL) return PAGE_OK; // WTF why this statement? LOG_DEBUG(GwLog::LOG,"Drawing at PageNavigation, %s: %f, %s: %f, %s: %f, %s: %f", name1.c_str(), value1, name2.c_str(), value2, name3.c_str(), value3, name4.c_str(), value4); + // Load navigation map + //*********************************************************** + // Rotate the picture in 1° steps + int angle = 25; + + // Server settings + String server = "norbert-walter.dnshome.de"; + int port = 80; + + // URL to OBP Maps Converter + // For more details see: https://github.com/norbert-walter/maps-converter + String url = String("http://") + server + ":" + port + // OBP Server + String("/get_image_json?") + // Service: Output B&W picture as JSON (Base64 + gzip) + "zoom=15" + // Zoom level: 15 + "&lat=53.9028" + // Latitude + "&lon=11.4441" + // Longitude + "&mrot=" + angle + // Rotation angle navigation map + "&mtype=9" + // Free Nautical Charts with depth + "&dtype=1" + // Dithering type: Threshold dithering + "&width=400" + // With navigation map + "&height=250" + // Height navigation map + "&cutout=0" + // No picture cutouts + "&tab=0" + // No tab size + "&border=2" + // Border line size: 2 pixel + "&symbol=2" + // Symbol: Triangle + "&srot=" + angle + // Symbol rotation angle + "&ssize=15" + // Symbole size: 15 pixel + "&grid=1" // Show grid: On + ; + + // Set display in partial refresh mode + getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + getdisplay().setTextColor(commonData->fgcolor); + + // If a network connection to URL + if (net.fetchAndDecompressJson(url)) { // Connect to URL, read gzip answare and deflate JSON content + + auto& json = net.json(); // Parse JSON content + int numPix = json["number_pixels"] | 0; // Read number of picture pixels + String b64 = json["picture_base64"] | ""; // Read the Base64 bit steram content (picture) + static uint8_t imageData[400 * 300]; // Set picture buffer + size_t decodedSize = 0; // Reset decoded size of Basse64 bit stream content +/* + decoder.decodeBase64(b64, imageData, sizeof(imageData), decodedSize); // Decode Base64 bit stream content + getdisplay().drawBitmap(0, 25, imageData, getdisplay().width(), getdisplay().height(), GxEPD_BLACK); // Show picture with Y offset 25 pixel +*/ + getdisplay().drawBitmap(0, 0, gImage_Logo_OBP_400x300_sw, getdisplay().width(), getdisplay().height(), GxEPD_BLACK); // Show picture with Y offset 25 pixel + } + // Draw page //*********************************************************** - // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update - - getdisplay().setTextColor(commonData->fgcolor); // ############### Draw Navigation Map ################ getdisplay().setFont(&Ubuntu_Bold12pt8b); getdisplay().setCursor(20, 60); getdisplay().print(name1); - getdisplay().setCursor(120, 60); + getdisplay().setCursor(80, 60); getdisplay().print(svalue1); getdisplay().setCursor(20, 80); getdisplay().print(name2); - getdisplay().setCursor(120, 80); + getdisplay().setCursor(80, 80); getdisplay().print(svalue2); getdisplay().setCursor(20, 100); getdisplay().print(name3); - getdisplay().setCursor(120, 100); + getdisplay().setCursor(80, 100); getdisplay().print(svalue3); getdisplay().setCursor(20, 120); getdisplay().print(name4); - getdisplay().setCursor(120, 120); + getdisplay().setCursor(80, 120); getdisplay().print(svalue4); return PAGE_UPDATE; diff --git a/lib/obp60task/platformio.ini b/lib/obp60task/platformio.ini index 91cbfe8..0f40d7e 100644 --- a/lib/obp60task/platformio.ini +++ b/lib/obp60task/platformio.ini @@ -22,6 +22,8 @@ lib_deps = Wire SPI ESP32time + HTTPClient + WiFiClientSecure esphome/AsyncTCP-esphome@2.0.1 robtillaart/PCF8574@0.3.9 adafruit/Adafruit Unified Sensor @ 1.1.13 @@ -74,6 +76,8 @@ lib_deps = SPI SD ESP32time + HTTPClient + WiFiClientSecure esphome/AsyncTCP-esphome@2.0.1 robtillaart/PCF8574@0.3.9 adafruit/Adafruit Unified Sensor @ 1.1.13 diff --git a/lib/obp60task/puff.c b/lib/obp60task/puff.c new file mode 100644 index 0000000..d759825 --- /dev/null +++ b/lib/obp60task/puff.c @@ -0,0 +1,840 @@ +/* + * puff.c + * Copyright (C) 2002-2013 Mark Adler + * For conditions of distribution and use, see copyright notice in puff.h + * version 2.3, 21 Jan 2013 + * + * puff.c is a simple inflate written to be an unambiguous way to specify the + * deflate format. It is not written for speed but rather simplicity. As a + * side benefit, this code might actually be useful when small code is more + * important than speed, such as bootstrap applications. For typical deflate + * data, zlib's inflate() is about four times as fast as puff(). zlib's + * inflate compiles to around 20K on my machine, whereas puff.c compiles to + * around 4K on my machine (a PowerPC using GNU cc). If the faster decode() + * function here is used, then puff() is only twice as slow as zlib's + * inflate(). + * + * All dynamically allocated memory comes from the stack. The stack required + * is less than 2K bytes. This code is compatible with 16-bit int's and + * assumes that long's are at least 32 bits. puff.c uses the short data type, + * assumed to be 16 bits, for arrays in order to conserve memory. The code + * works whether integers are stored big endian or little endian. + * + * In the comments below are "Format notes" that describe the inflate process + * and document some of the less obvious aspects of the format. This source + * code is meant to supplement RFC 1951, which formally describes the deflate + * format: + * + * http://www.zlib.org/rfc-deflate.html + */ + +/* + * Change history: + * + * 1.0 10 Feb 2002 - First version + * 1.1 17 Feb 2002 - Clarifications of some comments and notes + * - Update puff() dest and source pointers on negative + * errors to facilitate debugging deflators + * - Remove longest from struct huffman -- not needed + * - Simplify offs[] index in construct() + * - Add input size and checking, using longjmp() to + * maintain easy readability + * - Use short data type for large arrays + * - Use pointers instead of long to specify source and + * destination sizes to avoid arbitrary 4 GB limits + * 1.2 17 Mar 2002 - Add faster version of decode(), doubles speed (!), + * but leave simple version for readability + * - Make sure invalid distances detected if pointers + * are 16 bits + * - Fix fixed codes table error + * - Provide a scanning mode for determining size of + * uncompressed data + * 1.3 20 Mar 2002 - Go back to lengths for puff() parameters [Gailly] + * - Add a puff.h file for the interface + * - Add braces in puff() for else do [Gailly] + * - Use indexes instead of pointers for readability + * 1.4 31 Mar 2002 - Simplify construct() code set check + * - Fix some comments + * - Add FIXLCODES #define + * 1.5 6 Apr 2002 - Minor comment fixes + * 1.6 7 Aug 2002 - Minor format changes + * 1.7 3 Mar 2003 - Added test code for distribution + * - Added zlib-like license + * 1.8 9 Jan 2004 - Added some comments on no distance codes case + * 1.9 21 Feb 2008 - Fix bug on 16-bit integer architectures [Pohland] + * - Catch missing end-of-block symbol error + * 2.0 25 Jul 2008 - Add #define to permit distance too far back + * - Add option in TEST code for puff to write the data + * - Add option in TEST code to skip input bytes + * - Allow TEST code to read from piped stdin + * 2.1 4 Apr 2010 - Avoid variable initialization for happier compilers + * - Avoid unsigned comparisons for even happier compilers + * 2.2 25 Apr 2010 - Fix bug in variable initializations [Oberhumer] + * - Add const where appropriate [Oberhumer] + * - Split if's and ?'s for coverage testing + * - Break out test code to separate file + * - Move NIL to puff.h + * - Allow incomplete code only if single code length is 1 + * - Add full code coverage test to Makefile + * 2.3 21 Jan 2013 - Check for invalid code length codes in dynamic blocks + */ + +#include /* for setjmp(), longjmp(), and jmp_buf */ +#include "puff.h" /* prototype for puff() */ + +#define local static /* for local function definitions */ + +/* + * Maximums for allocations and loops. It is not useful to change these -- + * they are fixed by the deflate format. + */ +#define MAXBITS 15 /* maximum bits in a code */ +#define MAXLCODES 286 /* maximum number of literal/length codes */ +#define MAXDCODES 30 /* maximum number of distance codes */ +#define MAXCODES (MAXLCODES+MAXDCODES) /* maximum codes lengths to read */ +#define FIXLCODES 288 /* number of fixed literal/length codes */ + +/* input and output state */ +struct state { + /* output state */ + unsigned char *out; /* output buffer */ + unsigned long outlen; /* available space at out */ + unsigned long outcnt; /* bytes written to out so far */ + + /* input state */ + const unsigned char *in; /* input buffer */ + unsigned long inlen; /* available input at in */ + unsigned long incnt; /* bytes read so far */ + int bitbuf; /* bit buffer */ + int bitcnt; /* number of bits in bit buffer */ + + /* input limit error return state for bits() and decode() */ + jmp_buf env; +}; + +/* + * Return need bits from the input stream. This always leaves less than + * eight bits in the buffer. bits() works properly for need == 0. + * + * Format notes: + * + * - Bits are stored in bytes from the least significant bit to the most + * significant bit. Therefore bits are dropped from the bottom of the bit + * buffer, using shift right, and new bytes are appended to the top of the + * bit buffer, using shift left. + */ +local int bits(struct state *s, int need) +{ + long val; /* bit accumulator (can use up to 20 bits) */ + + /* load at least need bits into val */ + val = s->bitbuf; + while (s->bitcnt < need) { + if (s->incnt == s->inlen) + longjmp(s->env, 1); /* out of input */ + val |= (long)(s->in[s->incnt++]) << s->bitcnt; /* load eight bits */ + s->bitcnt += 8; + } + + /* drop need bits and update buffer, always zero to seven bits left */ + s->bitbuf = (int)(val >> need); + s->bitcnt -= need; + + /* return need bits, zeroing the bits above that */ + return (int)(val & ((1L << need) - 1)); +} + +/* + * Process a stored block. + * + * Format notes: + * + * - After the two-bit stored block type (00), the stored block length and + * stored bytes are byte-aligned for fast copying. Therefore any leftover + * bits in the byte that has the last bit of the type, as many as seven, are + * discarded. The value of the discarded bits are not defined and should not + * be checked against any expectation. + * + * - The second inverted copy of the stored block length does not have to be + * checked, but it's probably a good idea to do so anyway. + * + * - A stored block can have zero length. This is sometimes used to byte-align + * subsets of the compressed data for random access or partial recovery. + */ +local int stored(struct state *s) +{ + unsigned len; /* length of stored block */ + + /* discard leftover bits from current byte (assumes s->bitcnt < 8) */ + s->bitbuf = 0; + s->bitcnt = 0; + + /* get length and check against its one's complement */ + if (s->incnt + 4 > s->inlen) + return 2; /* not enough input */ + len = s->in[s->incnt++]; + len |= s->in[s->incnt++] << 8; + if (s->in[s->incnt++] != (~len & 0xff) || + s->in[s->incnt++] != ((~len >> 8) & 0xff)) + return -2; /* didn't match complement! */ + + /* copy len bytes from in to out */ + if (s->incnt + len > s->inlen) + return 2; /* not enough input */ + if (s->out != NIL) { + if (s->outcnt + len > s->outlen) + return 1; /* not enough output space */ + while (len--) + s->out[s->outcnt++] = s->in[s->incnt++]; + } + else { /* just scanning */ + s->outcnt += len; + s->incnt += len; + } + + /* done with a valid stored block */ + return 0; +} + +/* + * Huffman code decoding tables. count[1..MAXBITS] is the number of symbols of + * each length, which for a canonical code are stepped through in order. + * symbol[] are the symbol values in canonical order, where the number of + * entries is the sum of the counts in count[]. The decoding process can be + * seen in the function decode() below. + */ +struct huffman { + short *count; /* number of symbols of each length */ + short *symbol; /* canonically ordered symbols */ +}; + +/* + * Decode a code from the stream s using huffman table h. Return the symbol or + * a negative value if there is an error. If all of the lengths are zero, i.e. + * an empty code, or if the code is incomplete and an invalid code is received, + * then -10 is returned after reading MAXBITS bits. + * + * Format notes: + * + * - The codes as stored in the compressed data are bit-reversed relative to + * a simple integer ordering of codes of the same lengths. Hence below the + * bits are pulled from the compressed data one at a time and used to + * build the code value reversed from what is in the stream in order to + * permit simple integer comparisons for decoding. A table-based decoding + * scheme (as used in zlib) does not need to do this reversal. + * + * - The first code for the shortest length is all zeros. Subsequent codes of + * the same length are simply integer increments of the previous code. When + * moving up a length, a zero bit is appended to the code. For a complete + * code, the last code of the longest length will be all ones. + * + * - Incomplete codes are handled by this decoder, since they are permitted + * in the deflate format. See the format notes for fixed() and dynamic(). + */ +#ifdef SLOW +local int decode(struct state *s, const struct huffman *h) +{ + int len; /* current number of bits in code */ + int code; /* len bits being decoded */ + int first; /* first code of length len */ + int count; /* number of codes of length len */ + int index; /* index of first code of length len in symbol table */ + + code = first = index = 0; + for (len = 1; len <= MAXBITS; len++) { + code |= bits(s, 1); /* get next bit */ + count = h->count[len]; + if (code - count < first) /* if length len, return symbol */ + return h->symbol[index + (code - first)]; + index += count; /* else update for next length */ + first += count; + first <<= 1; + code <<= 1; + } + return -10; /* ran out of codes */ +} + +/* + * A faster version of decode() for real applications of this code. It's not + * as readable, but it makes puff() twice as fast. And it only makes the code + * a few percent larger. + */ +#else /* !SLOW */ +local int decode(struct state *s, const struct huffman *h) +{ + int len; /* current number of bits in code */ + int code; /* len bits being decoded */ + int first; /* first code of length len */ + int count; /* number of codes of length len */ + int index; /* index of first code of length len in symbol table */ + int bitbuf; /* bits from stream */ + int left; /* bits left in next or left to process */ + short *next; /* next number of codes */ + + bitbuf = s->bitbuf; + left = s->bitcnt; + code = first = index = 0; + len = 1; + next = h->count + 1; + while (1) { + while (left--) { + code |= bitbuf & 1; + bitbuf >>= 1; + count = *next++; + if (code - count < first) { /* if length len, return symbol */ + s->bitbuf = bitbuf; + s->bitcnt = (s->bitcnt - len) & 7; + return h->symbol[index + (code - first)]; + } + index += count; /* else update for next length */ + first += count; + first <<= 1; + code <<= 1; + len++; + } + left = (MAXBITS+1) - len; + if (left == 0) + break; + if (s->incnt == s->inlen) + longjmp(s->env, 1); /* out of input */ + bitbuf = s->in[s->incnt++]; + if (left > 8) + left = 8; + } + return -10; /* ran out of codes */ +} +#endif /* SLOW */ + +/* + * Given the list of code lengths length[0..n-1] representing a canonical + * Huffman code for n symbols, construct the tables required to decode those + * codes. Those tables are the number of codes of each length, and the symbols + * sorted by length, retaining their original order within each length. The + * return value is zero for a complete code set, negative for an over- + * subscribed code set, and positive for an incomplete code set. The tables + * can be used if the return value is zero or positive, but they cannot be used + * if the return value is negative. If the return value is zero, it is not + * possible for decode() using that table to return an error--any stream of + * enough bits will resolve to a symbol. If the return value is positive, then + * it is possible for decode() using that table to return an error for received + * codes past the end of the incomplete lengths. + * + * Not used by decode(), but used for error checking, h->count[0] is the number + * of the n symbols not in the code. So n - h->count[0] is the number of + * codes. This is useful for checking for incomplete codes that have more than + * one symbol, which is an error in a dynamic block. + * + * Assumption: for all i in 0..n-1, 0 <= length[i] <= MAXBITS + * This is assured by the construction of the length arrays in dynamic() and + * fixed() and is not verified by construct(). + * + * Format notes: + * + * - Permitted and expected examples of incomplete codes are one of the fixed + * codes and any code with a single symbol which in deflate is coded as one + * bit instead of zero bits. See the format notes for fixed() and dynamic(). + * + * - Within a given code length, the symbols are kept in ascending order for + * the code bits definition. + */ +local int construct(struct huffman *h, const short *length, int n) +{ + int symbol; /* current symbol when stepping through length[] */ + int len; /* current length when stepping through h->count[] */ + int left; /* number of possible codes left of current length */ + short offs[MAXBITS+1]; /* offsets in symbol table for each length */ + + /* count number of codes of each length */ + for (len = 0; len <= MAXBITS; len++) + h->count[len] = 0; + for (symbol = 0; symbol < n; symbol++) + (h->count[length[symbol]])++; /* assumes lengths are within bounds */ + if (h->count[0] == n) /* no codes! */ + return 0; /* complete, but decode() will fail */ + + /* check for an over-subscribed or incomplete set of lengths */ + left = 1; /* one possible code of zero length */ + for (len = 1; len <= MAXBITS; len++) { + left <<= 1; /* one more bit, double codes left */ + left -= h->count[len]; /* deduct count from possible codes */ + if (left < 0) + return left; /* over-subscribed--return negative */ + } /* left > 0 means incomplete */ + + /* generate offsets into symbol table for each length for sorting */ + offs[1] = 0; + for (len = 1; len < MAXBITS; len++) + offs[len + 1] = offs[len] + h->count[len]; + + /* + * put symbols in table sorted by length, by symbol order within each + * length + */ + for (symbol = 0; symbol < n; symbol++) + if (length[symbol] != 0) + h->symbol[offs[length[symbol]]++] = symbol; + + /* return zero for complete set, positive for incomplete set */ + return left; +} + +/* + * Decode literal/length and distance codes until an end-of-block code. + * + * Format notes: + * + * - Compressed data that is after the block type if fixed or after the code + * description if dynamic is a combination of literals and length/distance + * pairs terminated by and end-of-block code. Literals are simply Huffman + * coded bytes. A length/distance pair is a coded length followed by a + * coded distance to represent a string that occurs earlier in the + * uncompressed data that occurs again at the current location. + * + * - Literals, lengths, and the end-of-block code are combined into a single + * code of up to 286 symbols. They are 256 literals (0..255), 29 length + * symbols (257..285), and the end-of-block symbol (256). + * + * - There are 256 possible lengths (3..258), and so 29 symbols are not enough + * to represent all of those. Lengths 3..10 and 258 are in fact represented + * by just a length symbol. Lengths 11..257 are represented as a symbol and + * some number of extra bits that are added as an integer to the base length + * of the length symbol. The number of extra bits is determined by the base + * length symbol. These are in the static arrays below, lens[] for the base + * lengths and lext[] for the corresponding number of extra bits. + * + * - The reason that 258 gets its own symbol is that the longest length is used + * often in highly redundant files. Note that 258 can also be coded as the + * base value 227 plus the maximum extra value of 31. While a good deflate + * should never do this, it is not an error, and should be decoded properly. + * + * - If a length is decoded, including its extra bits if any, then it is + * followed a distance code. There are up to 30 distance symbols. Again + * there are many more possible distances (1..32768), so extra bits are added + * to a base value represented by the symbol. The distances 1..4 get their + * own symbol, but the rest require extra bits. The base distances and + * corresponding number of extra bits are below in the static arrays dist[] + * and dext[]. + * + * - Literal bytes are simply written to the output. A length/distance pair is + * an instruction to copy previously uncompressed bytes to the output. The + * copy is from distance bytes back in the output stream, copying for length + * bytes. + * + * - Distances pointing before the beginning of the output data are not + * permitted. + * + * - Overlapped copies, where the length is greater than the distance, are + * allowed and common. For example, a distance of one and a length of 258 + * simply copies the last byte 258 times. A distance of four and a length of + * twelve copies the last four bytes three times. A simple forward copy + * ignoring whether the length is greater than the distance or not implements + * this correctly. You should not use memcpy() since its behavior is not + * defined for overlapped arrays. You should not use memmove() or bcopy() + * since though their behavior -is- defined for overlapping arrays, it is + * defined to do the wrong thing in this case. + */ +local int codes(struct state *s, + const struct huffman *lencode, + const struct huffman *distcode) +{ + int symbol; /* decoded symbol */ + int len; /* length for copy */ + unsigned dist; /* distance for copy */ + static const short lens[29] = { /* Size base for length codes 257..285 */ + 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, + 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258}; + static const short lext[29] = { /* Extra bits for length codes 257..285 */ + 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, + 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0}; + static const short dists[30] = { /* Offset base for distance codes 0..29 */ + 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, + 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, + 8193, 12289, 16385, 24577}; + static const short dext[30] = { /* Extra bits for distance codes 0..29 */ + 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, + 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, + 12, 12, 13, 13}; + + /* decode literals and length/distance pairs */ + do { + symbol = decode(s, lencode); + if (symbol < 0) + return symbol; /* invalid symbol */ + if (symbol < 256) { /* literal: symbol is the byte */ + /* write out the literal */ + if (s->out != NIL) { + if (s->outcnt == s->outlen) + return 1; + s->out[s->outcnt] = symbol; + } + s->outcnt++; + } + else if (symbol > 256) { /* length */ + /* get and compute length */ + symbol -= 257; + if (symbol >= 29) + return -10; /* invalid fixed code */ + len = lens[symbol] + bits(s, lext[symbol]); + + /* get and check distance */ + symbol = decode(s, distcode); + if (symbol < 0) + return symbol; /* invalid symbol */ + dist = dists[symbol] + bits(s, dext[symbol]); +#ifndef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR + if (dist > s->outcnt) + return -11; /* distance too far back */ +#endif + + /* copy length bytes from distance bytes back */ + if (s->out != NIL) { + if (s->outcnt + len > s->outlen) + return 1; + while (len--) { + s->out[s->outcnt] = +#ifdef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR + dist > s->outcnt ? + 0 : +#endif + s->out[s->outcnt - dist]; + s->outcnt++; + } + } + else + s->outcnt += len; + } + } while (symbol != 256); /* end of block symbol */ + + /* done with a valid fixed or dynamic block */ + return 0; +} + +/* + * Process a fixed codes block. + * + * Format notes: + * + * - This block type can be useful for compressing small amounts of data for + * which the size of the code descriptions in a dynamic block exceeds the + * benefit of custom codes for that block. For fixed codes, no bits are + * spent on code descriptions. Instead the code lengths for literal/length + * codes and distance codes are fixed. The specific lengths for each symbol + * can be seen in the "for" loops below. + * + * - The literal/length code is complete, but has two symbols that are invalid + * and should result in an error if received. This cannot be implemented + * simply as an incomplete code since those two symbols are in the "middle" + * of the code. They are eight bits long and the longest literal/length\ + * code is nine bits. Therefore the code must be constructed with those + * symbols, and the invalid symbols must be detected after decoding. + * + * - The fixed distance codes also have two invalid symbols that should result + * in an error if received. Since all of the distance codes are the same + * length, this can be implemented as an incomplete code. Then the invalid + * codes are detected while decoding. + */ +local int fixed(struct state *s) +{ + static int virgin = 1; + static short lencnt[MAXBITS+1], lensym[FIXLCODES]; + static short distcnt[MAXBITS+1], distsym[MAXDCODES]; + static struct huffman lencode, distcode; + + /* build fixed huffman tables if first call (may not be thread safe) */ + if (virgin) { + int symbol; + short lengths[FIXLCODES]; + + /* construct lencode and distcode */ + lencode.count = lencnt; + lencode.symbol = lensym; + distcode.count = distcnt; + distcode.symbol = distsym; + + /* literal/length table */ + for (symbol = 0; symbol < 144; symbol++) + lengths[symbol] = 8; + for (; symbol < 256; symbol++) + lengths[symbol] = 9; + for (; symbol < 280; symbol++) + lengths[symbol] = 7; + for (; symbol < FIXLCODES; symbol++) + lengths[symbol] = 8; + construct(&lencode, lengths, FIXLCODES); + + /* distance table */ + for (symbol = 0; symbol < MAXDCODES; symbol++) + lengths[symbol] = 5; + construct(&distcode, lengths, MAXDCODES); + + /* do this just once */ + virgin = 0; + } + + /* decode data until end-of-block code */ + return codes(s, &lencode, &distcode); +} + +/* + * Process a dynamic codes block. + * + * Format notes: + * + * - A dynamic block starts with a description of the literal/length and + * distance codes for that block. New dynamic blocks allow the compressor to + * rapidly adapt to changing data with new codes optimized for that data. + * + * - The codes used by the deflate format are "canonical", which means that + * the actual bits of the codes are generated in an unambiguous way simply + * from the number of bits in each code. Therefore the code descriptions + * are simply a list of code lengths for each symbol. + * + * - The code lengths are stored in order for the symbols, so lengths are + * provided for each of the literal/length symbols, and for each of the + * distance symbols. + * + * - If a symbol is not used in the block, this is represented by a zero as the + * code length. This does not mean a zero-length code, but rather that no + * code should be created for this symbol. There is no way in the deflate + * format to represent a zero-length code. + * + * - The maximum number of bits in a code is 15, so the possible lengths for + * any code are 1..15. + * + * - The fact that a length of zero is not permitted for a code has an + * interesting consequence. Normally if only one symbol is used for a given + * code, then in fact that code could be represented with zero bits. However + * in deflate, that code has to be at least one bit. So for example, if + * only a single distance base symbol appears in a block, then it will be + * represented by a single code of length one, in particular one 0 bit. This + * is an incomplete code, since if a 1 bit is received, it has no meaning, + * and should result in an error. So incomplete distance codes of one symbol + * should be permitted, and the receipt of invalid codes should be handled. + * + * - It is also possible to have a single literal/length code, but that code + * must be the end-of-block code, since every dynamic block has one. This + * is not the most efficient way to create an empty block (an empty fixed + * block is fewer bits), but it is allowed by the format. So incomplete + * literal/length codes of one symbol should also be permitted. + * + * - If there are only literal codes and no lengths, then there are no distance + * codes. This is represented by one distance code with zero bits. + * + * - The list of up to 286 length/literal lengths and up to 30 distance lengths + * are themselves compressed using Huffman codes and run-length encoding. In + * the list of code lengths, a 0 symbol means no code, a 1..15 symbol means + * that length, and the symbols 16, 17, and 18 are run-length instructions. + * Each of 16, 17, and 18 are followed by extra bits to define the length of + * the run. 16 copies the last length 3 to 6 times. 17 represents 3 to 10 + * zero lengths, and 18 represents 11 to 138 zero lengths. Unused symbols + * are common, hence the special coding for zero lengths. + * + * - The symbols for 0..18 are Huffman coded, and so that code must be + * described first. This is simply a sequence of up to 19 three-bit values + * representing no code (0) or the code length for that symbol (1..7). + * + * - A dynamic block starts with three fixed-size counts from which is computed + * the number of literal/length code lengths, the number of distance code + * lengths, and the number of code length code lengths (ok, you come up with + * a better name!) in the code descriptions. For the literal/length and + * distance codes, lengths after those provided are considered zero, i.e. no + * code. The code length code lengths are received in a permuted order (see + * the order[] array below) to make a short code length code length list more + * likely. As it turns out, very short and very long codes are less likely + * to be seen in a dynamic code description, hence what may appear initially + * to be a peculiar ordering. + * + * - Given the number of literal/length code lengths (nlen) and distance code + * lengths (ndist), then they are treated as one long list of nlen + ndist + * code lengths. Therefore run-length coding can and often does cross the + * boundary between the two sets of lengths. + * + * - So to summarize, the code description at the start of a dynamic block is + * three counts for the number of code lengths for the literal/length codes, + * the distance codes, and the code length codes. This is followed by the + * code length code lengths, three bits each. This is used to construct the + * code length code which is used to read the remainder of the lengths. Then + * the literal/length code lengths and distance lengths are read as a single + * set of lengths using the code length codes. Codes are constructed from + * the resulting two sets of lengths, and then finally you can start + * decoding actual compressed data in the block. + * + * - For reference, a "typical" size for the code description in a dynamic + * block is around 80 bytes. + */ +local int dynamic(struct state *s) +{ + int nlen, ndist, ncode; /* number of lengths in descriptor */ + int index; /* index of lengths[] */ + int err; /* construct() return value */ + short lengths[MAXCODES]; /* descriptor code lengths */ + short lencnt[MAXBITS+1], lensym[MAXLCODES]; /* lencode memory */ + short distcnt[MAXBITS+1], distsym[MAXDCODES]; /* distcode memory */ + struct huffman lencode, distcode; /* length and distance codes */ + static const short order[19] = /* permutation of code length codes */ + {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15}; + + /* construct lencode and distcode */ + lencode.count = lencnt; + lencode.symbol = lensym; + distcode.count = distcnt; + distcode.symbol = distsym; + + /* get number of lengths in each table, check lengths */ + nlen = bits(s, 5) + 257; + ndist = bits(s, 5) + 1; + ncode = bits(s, 4) + 4; + if (nlen > MAXLCODES || ndist > MAXDCODES) + return -3; /* bad counts */ + + /* read code length code lengths (really), missing lengths are zero */ + for (index = 0; index < ncode; index++) + lengths[order[index]] = bits(s, 3); + for (; index < 19; index++) + lengths[order[index]] = 0; + + /* build huffman table for code lengths codes (use lencode temporarily) */ + err = construct(&lencode, lengths, 19); + if (err != 0) /* require complete code set here */ + return -4; + + /* read length/literal and distance code length tables */ + index = 0; + while (index < nlen + ndist) { + int symbol; /* decoded value */ + int len; /* last length to repeat */ + + symbol = decode(s, &lencode); + if (symbol < 0) + return symbol; /* invalid symbol */ + if (symbol < 16) /* length in 0..15 */ + lengths[index++] = symbol; + else { /* repeat instruction */ + len = 0; /* assume repeating zeros */ + if (symbol == 16) { /* repeat last length 3..6 times */ + if (index == 0) + return -5; /* no last length! */ + len = lengths[index - 1]; /* last length */ + symbol = 3 + bits(s, 2); + } + else if (symbol == 17) /* repeat zero 3..10 times */ + symbol = 3 + bits(s, 3); + else /* == 18, repeat zero 11..138 times */ + symbol = 11 + bits(s, 7); + if (index + symbol > nlen + ndist) + return -6; /* too many lengths! */ + while (symbol--) /* repeat last or zero symbol times */ + lengths[index++] = len; + } + } + + /* check for end-of-block code -- there better be one! */ + if (lengths[256] == 0) + return -9; + + /* build huffman table for literal/length codes */ + err = construct(&lencode, lengths, nlen); + if (err && (err < 0 || nlen != lencode.count[0] + lencode.count[1])) + return -7; /* incomplete code ok only for single length 1 code */ + + /* build huffman table for distance codes */ + err = construct(&distcode, lengths + nlen, ndist); + if (err && (err < 0 || ndist != distcode.count[0] + distcode.count[1])) + return -8; /* incomplete code ok only for single length 1 code */ + + /* decode data until end-of-block code */ + return codes(s, &lencode, &distcode); +} + +/* + * Inflate source to dest. On return, destlen and sourcelen are updated to the + * size of the uncompressed data and the size of the deflate data respectively. + * On success, the return value of puff() is zero. If there is an error in the + * source data, i.e. it is not in the deflate format, then a negative value is + * returned. If there is not enough input available or there is not enough + * output space, then a positive error is returned. In that case, destlen and + * sourcelen are not updated to facilitate retrying from the beginning with the + * provision of more input data or more output space. In the case of invalid + * inflate data (a negative error), the dest and source pointers are updated to + * facilitate the debugging of deflators. + * + * puff() also has a mode to determine the size of the uncompressed output with + * no output written. For this dest must be (unsigned char *)0. In this case, + * the input value of *destlen is ignored, and on return *destlen is set to the + * size of the uncompressed output. + * + * The return codes are: + * + * 2: available inflate data did not terminate + * 1: output space exhausted before completing inflate + * 0: successful inflate + * -1: invalid block type (type == 3) + * -2: stored block length did not match one's complement + * -3: dynamic block code description: too many length or distance codes + * -4: dynamic block code description: code lengths codes incomplete + * -5: dynamic block code description: repeat lengths with no first length + * -6: dynamic block code description: repeat more than specified lengths + * -7: dynamic block code description: invalid literal/length code lengths + * -8: dynamic block code description: invalid distance code lengths + * -9: dynamic block code description: missing end-of-block code + * -10: invalid literal/length or distance code in fixed or dynamic block + * -11: distance is too far back in fixed or dynamic block + * + * Format notes: + * + * - Three bits are read for each block to determine the kind of block and + * whether or not it is the last block. Then the block is decoded and the + * process repeated if it was not the last block. + * + * - The leftover bits in the last byte of the deflate data after the last + * block (if it was a fixed or dynamic block) are undefined and have no + * expected values to check. + */ +int puff(unsigned char *dest, /* pointer to destination pointer */ + unsigned long *destlen, /* amount of output space */ + const unsigned char *source, /* pointer to source data pointer */ + unsigned long *sourcelen) /* amount of input available */ +{ + struct state s; /* input/output state */ + int last, type; /* block information */ + int err; /* return value */ + + /* initialize output state */ + s.out = dest; + s.outlen = *destlen; /* ignored if dest is NIL */ + s.outcnt = 0; + + /* initialize input state */ + s.in = source; + s.inlen = *sourcelen; + s.incnt = 0; + s.bitbuf = 0; + s.bitcnt = 0; + + /* return if bits() or decode() tries to read past available input */ + if (setjmp(s.env) != 0) /* if came back here via longjmp() */ + err = 2; /* then skip do-loop, return error */ + else { + /* process blocks until last block or error */ + do { + last = bits(&s, 1); /* one if last block */ + type = bits(&s, 2); /* block type 0..3 */ + err = type == 0 ? + stored(&s) : + (type == 1 ? + fixed(&s) : + (type == 2 ? + dynamic(&s) : + -1)); /* type == 3, invalid */ + if (err != 0) + break; /* return with error */ + } while (!last); + } + + /* update the lengths and return */ + if (err <= 0) { + *destlen = s.outcnt; + *sourcelen = s.incnt; + } + return err; +} diff --git a/lib/obp60task/puff.h b/lib/obp60task/puff.h new file mode 100644 index 0000000..e23a245 --- /dev/null +++ b/lib/obp60task/puff.h @@ -0,0 +1,35 @@ +/* puff.h + Copyright (C) 2002-2013 Mark Adler, all rights reserved + version 2.3, 21 Jan 2013 + + This software is provided 'as-is', without any express or implied + warranty. In no event will the author be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + Mark Adler madler@alumni.caltech.edu + */ + + +/* + * See puff.c for purpose and usage. + */ +#ifndef NIL +# define NIL ((unsigned char *)0) /* for no output option */ +#endif + +int puff(unsigned char *dest, /* pointer to destination pointer */ + unsigned long *destlen, /* amount of output space */ + const unsigned char *source, /* pointer to source data pointer */ + unsigned long *sourcelen); /* amount of input available */ From e02ca265ae3477743c18ab436e6708dc2d5f3996 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Wed, 26 Nov 2025 14:29:20 +0100 Subject: [PATCH 056/121] First working version for PageNavigation, use PSRAM --- lib/obp60task/PageNavigation.cpp | 59 +++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp index 06b44a4..cefb19b 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -7,11 +7,10 @@ #include "Logo_OBP_400x300_sw.h" -// Limits -#define JSON_BUFFER 30000 // Max buffer size for JSON content (30 kB picture + values) - -NetworkClient net(JSON_BUFFER); -ImageDecoder decoder; +// Defines for reading of navigation map +#define JSON_BUFFER 30000 // Max buffer size for JSON content (30 kB picture + values) +NetworkClient net(JSON_BUFFER); // Define network client +ImageDecoder decoder; // Define image decoder class PageNavigation : public Page { @@ -132,19 +131,45 @@ public: getdisplay().setTextColor(commonData->fgcolor); // If a network connection to URL - if (net.fetchAndDecompressJson(url)) { // Connect to URL, read gzip answare and deflate JSON content + if (net.fetchAndDecompressJson(url)) { - auto& json = net.json(); // Parse JSON content - int numPix = json["number_pixels"] | 0; // Read number of picture pixels - String b64 = json["picture_base64"] | ""; // Read the Base64 bit steram content (picture) - static uint8_t imageData[400 * 300]; // Set picture buffer - size_t decodedSize = 0; // Reset decoded size of Basse64 bit stream content -/* - decoder.decodeBase64(b64, imageData, sizeof(imageData), decodedSize); // Decode Base64 bit stream content - getdisplay().drawBitmap(0, 25, imageData, getdisplay().width(), getdisplay().height(), GxEPD_BLACK); // Show picture with Y offset 25 pixel -*/ - getdisplay().drawBitmap(0, 0, gImage_Logo_OBP_400x300_sw, getdisplay().width(), getdisplay().height(), GxEPD_BLACK); // Show picture with Y offset 25 pixel - } + auto& json = net.json(); // Extract JSON content + int numPix = json["number_pixels"] | 0; // Read number of pixels + int imgWidth = json["width"] | 0; // Read width of image + int imgHeight = json["height"] | 0; // Read height og image + + const char* b64src = json["picture_base64"].as(); // Read picture as Base64 content + size_t b64len = strlen(b64src); // Calculate length of Base64 content + // Copy Base64 content in PSRAM + char* b64 = (char*) heap_caps_malloc(b64len + 1, MALLOC_CAP_SPIRAM); // Allcate PSRAM for Base64 content + if (!b64) { + Serial.println("ERROR: PSRAM alloc base64 failed"); + return PAGE_UPDATE; + } + memcpy(b64, b64src, b64len + 1); // Copy Base64 content in PSRAM + + // Set image buffer in PSRAM + //size_t imgSize = getdisplay().width() * getdisplay().height(); // Calculate image size + size_t imgSize = numPix; // Calculate image size + uint8_t* imageData = (uint8_t*) heap_caps_malloc(imgSize, MALLOC_CAP_SPIRAM); // Allocate PSRAM for image + if (!imageData) { + Serial.println("ERROR: PSRAM alloc image buffer failed"); + free(b64); + return PAGE_UPDATE; + } + + // Decode Base64 content to image + size_t decodedSize = 0; + decoder.decodeBase64(b64, imageData, imgSize, decodedSize); + + // Show image (navigation map) + getdisplay().drawBitmap(0, 25, imageData, imgWidth, imgHeight, commonData->fgcolor); + + // Clean PSRAM + free(b64); + free(imageData); + } + // Draw page //*********************************************************** From dd3a4f5093810267052197fb2b6eecce56fe6743 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Wed, 26 Nov 2025 18:02:41 +0100 Subject: [PATCH 057/121] fix a bug in the actisense reader, version 20251126 --- Readme.md | 4 ++++ platformio.ini | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 25eb111..5d2a20d 100644 --- a/Readme.md +++ b/Readme.md @@ -174,6 +174,10 @@ For details refer to the [example description](lib/exampletask/Readme.md). Changelog --------- +[20251126](../../releases/tag/20251126) +* fix a bug in the Actisense reader that could lead to an endless loop (making the device completely non responsive) +* upgrade to 4.24.1 of the NMEA2000 library (2025/11/01) - refer to the [changes](https://github.com/ttlappalainen/NMEA2000/blob/master/Documents/src/changes.md) - Especially UTF8 support +********* [20251007](../../releases/tag/20251007) ********* * add AIS Aton translations (PGN 129041 <-> Ais class 21) diff --git a/platformio.ini b/platformio.ini index 0025382..341083c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -18,7 +18,7 @@ extra_configs= [basedeps] lib_deps = - ttlappalainen_NMEA2000=https://github.com/wellenvogel/NMEA2000.git#20250926 + ttlappalainen_NMEA2000=https://github.com/wellenvogel/NMEA2000.git#20251126 ttlappalainen/NMEA0183 @ 1.10.1 ArduinoJson @ 6.18.5 AsyncTCP-esphome @ 2.0.1 From f8378c3a2b942d82a4c84c0f09c81a104142be3a Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Wed, 26 Nov 2025 18:57:58 +0100 Subject: [PATCH 058/121] Next working version --- lib/obp60task/PageNavigation.cpp | 177 ++++++++++++++++++++++++------- 1 file changed, 137 insertions(+), 40 deletions(-) diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp index cefb19b..d493a29 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -14,6 +14,10 @@ ImageDecoder decoder; // Define image decoder class PageNavigation : public Page { +// Values for buttons +int zoom = 15; // Zoom level 1...17 +bool showValues = false; // Show values COG, SOG, DBT in navigation map + public: PageNavigation(CommonData &common){ commonData = &common; @@ -26,6 +30,26 @@ public: commonData->keylock = !commonData->keylock; return 0; // Commit the key } + // Cood for zoom - + if(key == 1){ + zoom --; // Zoom - + if(zoom <7){ + zoom = 7; + } + return 0; // Commit the key + } + // Cood for zoom - + if(key == 2){ + zoom ++; // Zoom + + if(zoom >17){ + zoom = 17; + } + return 0; // Commit the key + } + if(key == 5){ + showValues = !showValues; // Toggle show values + return 0; // Commit the key + } return key; } @@ -34,14 +58,27 @@ public: GwLog *logger = commonData->logger; // Old values for hold function + static double value1old = 0; static String svalue1old = ""; static String unit1old = ""; + static double value2old = 0; static String svalue2old = ""; static String unit2old = ""; + static double value3old = 0; static String svalue3old = ""; static String unit3old = ""; + static double value4old = 0; static String svalue4old = ""; static String unit4old = ""; + static double value5old = 0; + static String svalue5old = ""; + static String unit5old = ""; + + static double latitude = 0; + static double longitude = 0; + static double courseOverGround = 0; + static double speedOverGround = 0; + static double depthBelowTransducer = 0; // Get config data String lengthformat = config->getString(config->lengthFormat); @@ -50,7 +87,7 @@ public: String flashLED = config->getString(config->flashLED); String backlightMode = config->getString(config->backlight); - // Get boat values #1 + // Get boat values #1 Latitude GwApi::BoatValue *bvalue1 = pageData.values[0]; // First element in list (only one value by PageOneValue) String name1 = xdrDelete(bvalue1->getName()); // Value name name1 = name1.substring(0, 6); // String length limit for value name @@ -59,7 +96,7 @@ public: String svalue1 = formatValue(bvalue1, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places String unit1 = formatValue(bvalue1, *commonData).unit; // Unit of value - // Get boat values #2 + // Get boat values #2 Longitude GwApi::BoatValue *bvalue2 = pageData.values[1]; // Second element in list (only one value by PageOneValue) String name2 = xdrDelete(bvalue2->getName()); // Value name name2 = name2.substring(0, 6); // String length limit for value name @@ -68,7 +105,7 @@ public: String svalue2 = formatValue(bvalue2, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places String unit2 = formatValue(bvalue2, *commonData).unit; // Unit of value - // Get boat values #3 + // Get boat values #3 COG GwApi::BoatValue *bvalue3 = pageData.values[2]; // Second element in list (only one value by PageOneValue) String name3 = xdrDelete(bvalue3->getName()); // Value name name3 = name3.substring(0, 6); // String length limit for value name @@ -77,7 +114,7 @@ public: String svalue3 = formatValue(bvalue3, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places String unit3 = formatValue(bvalue3, *commonData).unit; // Unit of value - // Get boat values #4 + // Get boat values #4 SOG GwApi::BoatValue *bvalue4 = pageData.values[3]; // Second element in list (only one value by PageOneValue) String name4 = xdrDelete(bvalue4->getName()); // Value name name4 = name4.substring(0, 6); // String length limit for value name @@ -86,6 +123,15 @@ public: String svalue4 = formatValue(bvalue4, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places String unit4 = formatValue(bvalue4, *commonData).unit; // Unit of value + // Get boat values #5 DBT + GwApi::BoatValue *bvalue5 = pageData.values[4]; // Second element in list (only one value by PageOneValue) + String name5 = xdrDelete(bvalue5->getName()); // Value name + name5 = name5.substring(0, 6); // String length limit for value name + double value5 = bvalue5->value; // Value as double in SI unit + bool valid5 = bvalue5->valid; // Valid information + String svalue5 = formatValue(bvalue5, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places + String unit5 = formatValue(bvalue5, *commonData).unit; // Unit of value + // Optical warning by limit violation (unused) if(String(flashLED) == "Limit Violation"){ setBlinkingLED(false); @@ -98,8 +144,46 @@ public: // Load navigation map //*********************************************************** - // Rotate the picture in 1° steps - int angle = 25; + // Latitude + if(valid1){ + latitude = value1; + value3old = value1; + } + else{ + latitude = value1old; + } + // Longitude + if(valid2){ + longitude = value2; + value2old = value2; + } + else{ + longitude = value2old; + } + // COG value (Course Over Ground) + if(valid3){ + courseOverGround = value3; + value3old = value3; + } + else{ + courseOverGround = value3old; + } + // SOG value (Speed Over Ground) + if(valid4){ + speedOverGround = value4; + value4old = value4; + } + else{ + speedOverGround = value4old; + } + // DBT value (Depth Below Transducer) + if(valid5){ + depthBelowTransducer = value5; + value5old = value5; + } + else{ + depthBelowTransducer = value5old; + } // Server settings String server = "norbert-walter.dnshome.de"; @@ -109,28 +193,33 @@ public: // For more details see: https://github.com/norbert-walter/maps-converter String url = String("http://") + server + ":" + port + // OBP Server String("/get_image_json?") + // Service: Output B&W picture as JSON (Base64 + gzip) - "zoom=15" + // Zoom level: 15 - "&lat=53.9028" + // Latitude - "&lon=11.4441" + // Longitude - "&mrot=" + angle + // Rotation angle navigation map - "&mtype=9" + // Free Nautical Charts with depth - "&dtype=1" + // Dithering type: Threshold dithering + "zoom=" + zoom + // Zoom level: 15 + "&lat=" + latitude + // Latitude + "&lon=" + longitude + // Longitude + "&mrot=" + int(courseOverGround) + // Rotation angle navigation map + "&mtype=5" + // Open Topo Map + "&dtype=4" + // Dithering type: Atkinson dithering "&width=400" + // With navigation map "&height=250" + // Height navigation map "&cutout=0" + // No picture cutouts "&tab=0" + // No tab size "&border=2" + // Border line size: 2 pixel "&symbol=2" + // Symbol: Triangle - "&srot=" + angle + // Symbol rotation angle + "&srot=" + int(courseOverGround) + // Symbol rotation angle "&ssize=15" + // Symbole size: 15 pixel - "&grid=1" // Show grid: On + "&grid=0" // Show grid: On ; + // Draw page + //*********************************************************** + + // ############### Draw Navigation Map ################ + // Set display in partial refresh mode getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update getdisplay().setTextColor(commonData->fgcolor); - // If a network connection to URL + // If a network connection to URL then load the navigation map if (net.fetchAndDecompressJson(url)) { auto& json = net.json(); // Extract JSON content @@ -143,7 +232,7 @@ public: // Copy Base64 content in PSRAM char* b64 = (char*) heap_caps_malloc(b64len + 1, MALLOC_CAP_SPIRAM); // Allcate PSRAM for Base64 content if (!b64) { - Serial.println("ERROR: PSRAM alloc base64 failed"); + LOG_DEBUG(GwLog::ERROR,"Error PageNavigation: PSRAM alloc base64 failed"); return PAGE_UPDATE; } memcpy(b64, b64src, b64len + 1); // Copy Base64 content in PSRAM @@ -153,7 +242,7 @@ public: size_t imgSize = numPix; // Calculate image size uint8_t* imageData = (uint8_t*) heap_caps_malloc(imgSize, MALLOC_CAP_SPIRAM); // Allocate PSRAM for image if (!imageData) { - Serial.println("ERROR: PSRAM alloc image buffer failed"); + LOG_DEBUG(GwLog::ERROR,"Error PageNavigation: PPSRAM alloc image buffer failed"); free(b64); return PAGE_UPDATE; } @@ -169,34 +258,42 @@ public: free(b64); free(imageData); } - - - // Draw page - //*********************************************************** - // ############### Draw Navigation Map ################ + // ############### Draw Values ################ getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(20, 60); - getdisplay().print(name1); - getdisplay().setCursor(80, 60); - getdisplay().print(svalue1); + // Show zoom level + getdisplay().fillRect(355, 25 , 45, 25, commonData->fgcolor); // Black rect + getdisplay().fillRect(357, 27 , 41, 21, commonData->bgcolor); // White rect + getdisplay().setCursor(364, 45); + getdisplay().print(zoom); - getdisplay().setCursor(20, 80); - getdisplay().print(name2); - getdisplay().setCursor(80, 80); - getdisplay().print(svalue2); + if(showValues == true){ + // Frame + getdisplay().fillRect(0, 25 , 130, 70, commonData->fgcolor); // Black rect + getdisplay().fillRect(2, 27 , 126, 66, commonData->bgcolor); // White rect + // COG + getdisplay().setCursor(10, 45); + getdisplay().print(name3); + getdisplay().setCursor(70, 45); + getdisplay().print(svalue3); + // SOG + getdisplay().setCursor(10, 65); + getdisplay().print(name4); + getdisplay().setCursor(70, 65); + getdisplay().print(svalue4); + // DBT + getdisplay().setCursor(10, 85); + getdisplay().print(name5); + getdisplay().setCursor(70, 85); + getdisplay().print(svalue5); + } - getdisplay().setCursor(20, 100); - getdisplay().print(name3); - getdisplay().setCursor(80, 100); - getdisplay().print(svalue3); - - getdisplay().setCursor(20, 120); - getdisplay().print(name4); - getdisplay().setCursor(80, 120); - getdisplay().print(svalue4); + // Set botton labels + commonData->keydata[0].label = "ZOOM -"; + commonData->keydata[1].label = "ZOOM +"; + commonData->keydata[4].label = "VALUES"; return PAGE_UPDATE; }; @@ -215,7 +312,7 @@ PageDescription registerPageNavigation( "Navigation", // Page name createPage, // Action 0, // Number of bus values depends on selection in Web configuration - {"LAT","LON","HDT","SOG"}, // Bus values we need in the page + {"LAT","LON","COG","SOG","DBT"}, // Bus values we need in the page true // Show display header on/off ); From 0972f12b9e7b55cc97c09e4bb9bb8313470d0bc2 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Wed, 26 Nov 2025 19:33:14 +0100 Subject: [PATCH 059/121] Fix for better GPS accuracy --- lib/obp60task/PageNavigation.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp index d493a29..61fae93 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -194,10 +194,10 @@ public: String url = String("http://") + server + ":" + port + // OBP Server String("/get_image_json?") + // Service: Output B&W picture as JSON (Base64 + gzip) "zoom=" + zoom + // Zoom level: 15 - "&lat=" + latitude + // Latitude - "&lon=" + longitude + // Longitude + "&lat=" + String(latitude, 6) + // Latitude + "&lon=" + String(longitude, 6) + // Longitude "&mrot=" + int(courseOverGround) + // Rotation angle navigation map - "&mtype=5" + // Open Topo Map + "&mtype=1" + // Open Street Map "&dtype=4" + // Dithering type: Atkinson dithering "&width=400" + // With navigation map "&height=250" + // Height navigation map From b31addf852ca1c1298d48b33b3d187c479d90621 Mon Sep 17 00:00:00 2001 From: Ulrich Meine Date: Fri, 28 Nov 2025 23:45:44 +0100 Subject: [PATCH 060/121] Fixed typo in config.json files --- lib/obp60task/config.json | 2 +- lib/obp60task/config_obp40.json | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/obp60task/config.json b/lib/obp60task/config.json index f633762..ed476b6 100644 --- a/lib/obp60task/config.json +++ b/lib/obp60task/config.json @@ -224,7 +224,7 @@ "label": "Calculate True Wind", "type": "boolean", "default": "false", - "description": "If not available, calculate true wind data from appearant wind and other boat data", + "description": "If not available, calculate true wind data from apparent wind and other boat data", "category": "OBP60 Settings", "capabilities": { "obp60": "true" diff --git a/lib/obp60task/config_obp40.json b/lib/obp60task/config_obp40.json index 9addd67..5172874 100644 --- a/lib/obp60task/config_obp40.json +++ b/lib/obp60task/config_obp40.json @@ -224,7 +224,7 @@ "label": "Calculate True Wind", "type": "boolean", "default": "false", - "description": "If not available, calculate true wind data from appearant wind and other boat data", + "description": "If not available, calculate true wind data from apparent wind and other boat data", "category": "OBP40 Settings", "capabilities": { "obp40": "true" @@ -1542,7 +1542,7 @@ "description": "Wind source for page 1: [true|apparent]", "list": [ "True wind", - "Apparant wind" + "apparent wind" ], "category": "OBP40 Page 1", "capabilities": { @@ -1862,7 +1862,7 @@ "description": "Wind source for page 2: [true|apparent]", "list": [ "True wind", - "Apparant wind" + "apparent wind" ], "category": "OBP40 Page 2", "capabilities": { @@ -2173,7 +2173,7 @@ "description": "Wind source for page 3: [true|apparent]", "list": [ "True wind", - "Apparant wind" + "apparent wind" ], "category": "OBP40 Page 3", "capabilities": { @@ -2475,7 +2475,7 @@ "description": "Wind source for page 4: [true|apparent]", "list": [ "True wind", - "Apparant wind" + "apparent wind" ], "category": "OBP40 Page 4", "capabilities": { @@ -2768,7 +2768,7 @@ "description": "Wind source for page 5: [true|apparent]", "list": [ "True wind", - "Apparant wind" + "apparent wind" ], "category": "OBP40 Page 5", "capabilities": { @@ -3052,7 +3052,7 @@ "description": "Wind source for page 6: [true|apparent]", "list": [ "True wind", - "Apparant wind" + "apparent wind" ], "category": "OBP40 Page 6", "capabilities": { @@ -3327,7 +3327,7 @@ "description": "Wind source for page 7: [true|apparent]", "list": [ "True wind", - "Apparant wind" + "apparent wind" ], "category": "OBP40 Page 7", "capabilities": { @@ -3593,7 +3593,7 @@ "description": "Wind source for page 8: [true|apparent]", "list": [ "True wind", - "Apparant wind" + "apparent wind" ], "category": "OBP40 Page 8", "capabilities": { @@ -3850,7 +3850,7 @@ "description": "Wind source for page 9: [true|apparent]", "list": [ "True wind", - "Apparant wind" + "apparent wind" ], "category": "OBP40 Page 9", "capabilities": { @@ -4098,7 +4098,7 @@ "description": "Wind source for page 10: [true|apparent]", "list": [ "True wind", - "Apparant wind" + "apparent wind" ], "category": "OBP40 Page 10", "capabilities": { From 3fa7ca5e99890bf9123e9001aa4b1b1adcad7438 Mon Sep 17 00:00:00 2001 From: Ulrich Meine Date: Fri, 28 Nov 2025 23:47:39 +0100 Subject: [PATCH 061/121] Optimized buffer change for T/A wind; pixel and font size adjustments; cleaned #includes --- lib/obp60task/OBPDataOperations.h | 5 +- lib/obp60task/OBPRingBuffer.h | 3 +- lib/obp60task/OBPRingBuffer.tpp | 1 + lib/obp60task/OBPcharts.cpp | 40 ++++++++-------- lib/obp60task/OBPcharts.h | 2 +- lib/obp60task/PageWindPlot.cpp | 77 +++++++++++++++++++++++-------- lib/obp60task/gen_set.py | 2 +- 7 files changed, 85 insertions(+), 45 deletions(-) diff --git a/lib/obp60task/OBPDataOperations.h b/lib/obp60task/OBPDataOperations.h index 5ee604b..8422894 100644 --- a/lib/obp60task/OBPDataOperations.h +++ b/lib/obp60task/OBPDataOperations.h @@ -1,6 +1,5 @@ // Function lib for history buffer handling, true wind calculation, and other operations on boat data #pragma once -#include #include "OBPRingBuffer.h" #include "obp60task.h" @@ -17,8 +16,8 @@ private: RingBuffer twdHstry; // Circular buffer to store true wind direction values RingBuffer twsHstry; // Circular buffer to store true wind speed values (TWS) - RingBuffer awdHstry; // Circular buffer to store apparant wind direction values - RingBuffer awsHstry; // Circular buffer to store apparant xwind speed values (AWS) + RingBuffer awdHstry; // Circular buffer to store apparent wind direction values + RingBuffer awsHstry; // Circular buffer to store apparent xwind speed values (AWS) double twdHstryMin; // Min value for wind direction (TWD) in history buffer double twdHstryMax; // Max value for wind direction (TWD) in history buffer double twsHstryMin; diff --git a/lib/obp60task/OBPRingBuffer.h b/lib/obp60task/OBPRingBuffer.h index 15ad5c1..970245e 100644 --- a/lib/obp60task/OBPRingBuffer.h +++ b/lib/obp60task/OBPRingBuffer.h @@ -1,7 +1,8 @@ #pragma once -//#include "FreeRTOS.h" +#include "FreeRTOS.h" #include "GwSynchronized.h" #include +#include template struct PSRAMAllocator { diff --git a/lib/obp60task/OBPRingBuffer.tpp b/lib/obp60task/OBPRingBuffer.tpp index 281e89d..7d73f46 100644 --- a/lib/obp60task/OBPRingBuffer.tpp +++ b/lib/obp60task/OBPRingBuffer.tpp @@ -1,6 +1,7 @@ #include "OBPRingBuffer.h" #include #include +#include template void RingBuffer::initCommon() diff --git a/lib/obp60task/OBPcharts.cpp b/lib/obp60task/OBPcharts.cpp index 96954dc..be3fd43 100644 --- a/lib/obp60task/OBPcharts.cpp +++ b/lib/obp60task/OBPcharts.cpp @@ -17,7 +17,7 @@ Chart::Chart(RingBuffer& dataBuf, int8_t chrtDir, int8_t chrtSz, double df fgColor = commonData->fgcolor; bgColor = commonData->bgcolor; - LOG_DEBUG(GwLog::DEBUG, "Chart Init: dataBuf: %p", (void*)&dataBuf); + // LOG_DEBUG(GwLog::DEBUG, "Chart Init: Chart::dataBuf: %p, passed dataBuf: %p", (void*)&this->dataBuf, (void*)&dataBuf); dWidth = getdisplay().width(); dHeight = getdisplay().height(); @@ -104,8 +104,8 @@ Chart::~Chart() template void Chart::showChrt(int8_t chrtIntv, GwApi::BoatValue currValue) { - drawChrtTimeAxis(chrtIntv); drawChrt(chrtIntv, currValue); + drawChrtTimeAxis(chrtIntv); drawChrtValAxis(); } @@ -185,8 +185,8 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) } } - if (i >= (numBufVals / chrtIntv) - 4) // log chart data of 1 line (adjust for test purposes) - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Chart: i: %d, chrtVal: %.4f, {x,y} {%d,%d}", i, chrtVal, x, y); + // if (i >= (numBufVals / chrtIntv) - 4) // log chart data of 1 line (adjust for test purposes) + // LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Chart: i: %d, chrtVal: %.4f, {x,y} {%d,%d}", i, chrtVal, x, y); if ((i == 0) || (chrtPrevVal == dbMAX_VAL)) { // just a dot for 1st chart point or after some invalid values @@ -194,7 +194,6 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) prevY = y; } else if (chrtDataFmt != 0) { // cross borders check for degree values; shift values to [-PI..0..PI]; when crossing borders, range is 2x PI degrees - // Normalize both values relative to chrtMin (shift range to start at 0) double normCurr = WindUtils::to2PI(chrtVal - chrtMin); double normPrev = WindUtils::to2PI(chrtPrevVal - chrtMin); @@ -202,7 +201,7 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) bool crossedBorders = std::abs(normCurr - normPrev) > (chrtRng / 2.0); if (crossedBorders) { // If current value crosses chart borders compared to previous value, split line - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Chart: crossedBorders: %d, chrtVal: %.2f, chrtPrevVal: %.2f", crossedBorders, chrtVal, chrtPrevVal); + // LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Chart: crossedBorders: %d, chrtVal: %.2f, chrtPrevVal: %.2f", crossedBorders, chrtVal, chrtPrevVal); bool wrappingFromHighToLow = normCurr < normPrev; // Determine which edge we're crossing int xSplit = wrappingFromHighToLow ? (cStart.x + valAxis) : cStart.x; getdisplay().drawLine(prevX, prevY, xSplit, y, fgColor); @@ -215,11 +214,9 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) if (chrtDir == 0 || x == prevX) { // vertical line getdisplay().drawLine(prevX, prevY, x, y, fgColor); getdisplay().drawLine(prevX - 1, prevY, x - 1, y, fgColor); -// getdisplay().drawLine(prevX + 1, prevY, x - 1, y, fgColor); } else if (chrtDir == 1 || x != prevX) { // line with some horizontal trend -> normal state getdisplay().drawLine(prevX, prevY, x, y, fgColor); getdisplay().drawLine(prevX, prevY - 1, x, y - 1, fgColor); -// getdisplay().drawLine(prevX, prevY + 1, x, y - 1, fgColor); } chrtPrevVal = chrtVal; prevX = x; @@ -480,15 +477,15 @@ void Chart::drawChrtValAxis() intv = static_cast(round(cchrtRng / slots)); i = intv; - getdisplay().setFont(&Ubuntu_Bold10pt8b); + getdisplay().setFont(&Ubuntu_Bold12pt8b); for (int j = 60; j < valAxis - 30; j += 60) { LOG_DEBUG(GwLog::DEBUG, "ChartValAxis: chrtRng: %.2f, cchrtRng: %.2f, intv: %d, slots: %.1f, valAxis: %d, i: %d, j: %d", chrtRng, cchrtRng, intv, slots, valAxis, i, j); getdisplay().drawLine(cStart.x, cStart.y + j, cStart.x + timAxis, cStart.y + j, fgColor); - getdisplay().fillRect(cStart.x, cStart.y + j - 9, cStart.x + 32, 18, bgColor); // Clear small area to remove potential chart lines + getdisplay().fillRect(cStart.x, cStart.y + j - 11, cStart.x + 39, 21, bgColor); // Clear small area to remove potential chart lines String sVal = String(i); - getdisplay().setCursor((3 - sVal.length()) * 8, cStart.y + j + 6); // value right-formated + getdisplay().setCursor((3 - sVal.length()) * 10, cStart.y + j + 7); // value right-formated getdisplay().printf("%s", sVal); // Range value i += intv; @@ -498,9 +495,14 @@ void Chart::drawChrtValAxis() drawTextRalign(cStart.x + timAxis, cStart.y - 3, dbName); // buffer data name } else { // chrtDir == 1; vertical chart - getdisplay().setFont(&Ubuntu_Bold10pt8b); - + if (chrtSz == 0) { // full size chart -> use larger font + getdisplay().setFont(&Ubuntu_Bold12pt8b); + drawTextCenter(cStart.x + (valAxis / 4) + 25, cStart.y - 10, dbName); // buffer data name + } else { + getdisplay().setFont(&Ubuntu_Bold10pt8b); + } getdisplay().fillRect(cStart.x, top, valAxis, 2, fgColor); // top chart line + getdisplay().setCursor(cStart.x, cStart.y - 2); tmpBVal->value = chrtMin; cVal = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) @@ -515,16 +517,16 @@ void Chart::drawChrtValAxis() tmpBVal->value = chrtMax; cVal = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); - drawTextRalign(cStart.x + valAxis - 1, cStart.y - 2, sVal); // Range high end + drawTextRalign(cStart.x + valAxis - 2, cStart.y - 2, sVal); // Range high end for (int j = 0; j <= valAxis + 2; j += ((valAxis + 2) / 2)) { getdisplay().drawLine(cStart.x + j, cStart.y, cStart.x + j, cStart.y + timAxis, fgColor); } - if (chrtSz == 0) { - getdisplay().setFont(&Ubuntu_Bold12pt8b); - drawTextCenter(cStart.x + (valAxis / 4) + 5, cStart.y - 11, dbName); // buffer data name - } +// if (chrtSz == 0) { +// getdisplay().setFont(&Ubuntu_Bold12pt8b); +// drawTextCenter(cStart.x + (valAxis / 4) + 15, cStart.y - 11, dbName); // buffer data name +// } LOG_DEBUG(GwLog::DEBUG, "ChartGrd: chrtRng: %.2f, intv: %d, slots: %.1f, valAxis: %d, i: %d", chrtRng, intv, slots, valAxis, i); } } @@ -547,7 +549,7 @@ void Chart::prntCurrValue(GwApi::BoatValue& currValue, const Pos chrtPos) LOG_DEBUG(GwLog::DEBUG, "Chart CurrValue: dbValue: %.2f, sdbValue: %s, fmrtDbValue: %.2f, dbFormat: %s, dbUnit: %s, Valid: %d, Name: %s, Address: %p", currValue.value, sdbValue, testdbValue, currValue.getFormat(), dbUnit, currValue.valid, currValue.getName(), currValue); - getdisplay().fillRect(xPosVal, yPosVal - 34, 121, 40, bgColor); // Clear area for TWS value + getdisplay().fillRect(xPosVal, yPosVal - 34, 122, 40, bgColor); // Clear area for TWS value getdisplay().setFont(&DSEG7Classic_BoldItalic16pt7b); getdisplay().setCursor(xPosVal + 1, yPosVal); if (useSimuData) { diff --git a/lib/obp60task/OBPcharts.h b/lib/obp60task/OBPcharts.h index c33f52e..581ab85 100644 --- a/lib/obp60task/OBPcharts.h +++ b/lib/obp60task/OBPcharts.h @@ -1,4 +1,4 @@ -// Function lib for display of boat data in various chart formats +// Function lib for display of boat data in various graphical chart formats #pragma once #include "Pagedata.h" diff --git a/lib/obp60task/PageWindPlot.cpp b/lib/obp60task/PageWindPlot.cpp index 4c5216c..b9fbc67 100644 --- a/lib/obp60task/PageWindPlot.cpp +++ b/lib/obp60task/PageWindPlot.cpp @@ -15,7 +15,7 @@ private: bool keylock = false; // Keylock char chrtMode = 'D'; // Chart mode: 'D' for TWD, 'S' for TWS, 'B' for both - bool showTruW = true; // Show true wind or apparant wind in chart area + bool showTruW = true; // Show true wind or apparent wind in chart area bool oldShowTruW = false; // remember recent user selection of wind data type int dataIntv = 1; // Update interval for wind history chart: @@ -102,16 +102,15 @@ public: virtual void displayNew(PageData& pageData) { #ifdef BOARD_OBP40S3 - String wndSrc; // Wind source true/apparant wind - preselection for OBP40 + String wndSrc; // Wind source true/apparent wind - preselection for OBP40 wndSrc = commonData->config->getString("page" + String(pageData.pageNumber) + "wndsrc"); if (wndSrc == "True wind") { showTruW = true; } else { - showTruW = false; // Wind source is apparant wind + showTruW = false; // Wind source is apparent wind } LOG_DEBUG(GwLog::LOG, "New PageWindPlot; wind source=%s", wndSrc); - // commonData->logger->logDebug(GwLog::LOG, "New PageWindPlot: wind source=%s", wndSrc); #endif oldShowTruW = !showTruW; // makes wind source being initialized at initial page call @@ -127,10 +126,18 @@ public: static RingBuffer* wsHstry; // Wind speed data buffer static String wdName, wdFormat; // Wind direction name and format static String wsName, wsFormat; // Wind speed name and format - static std::unique_ptr> twdFlChart; // chart object for wind direction chart, full size - static std::unique_ptr> twsFlChart; // chart object for wind speed chart, full size - static std::unique_ptr> twdHfChart; // chart object for wind direction chart, half size - static std::unique_ptr> twsHfChart; // chart object for wind speed chart, half size + + // Separate chart objects for true wind and apparent wind + static std::unique_ptr> twdFlChart, awdFlChart; // chart object for wind direction chart, full size + static std::unique_ptr> twsFlChart, awsFlChart; // chart object for wind speed chart, full size + static std::unique_ptr> twdHfChart, awdHfChart; // chart object for wind direction chart, half size + static std::unique_ptr> twsHfChart, awsHfChart; // chart object for wind speed chart, half size + // Pointers to the currently active charts + static Chart* wdFlChart; + static Chart* wsFlChart; + static Chart* wdHfChart; + static Chart* wsHfChart; + static GwApi::BoatValue* wdBVal = new GwApi::BoatValue("TWD"); // temp BoatValue for wind direction unit identification; required by OBP60Formater static GwApi::BoatValue* wsBVal = new GwApi::BoatValue("TWS"); // temp BoatValue for wind speed unit identification; required by OBP60Formater */ double dfltRngWd = 60.0 * DEG_TO_RAD; // default range for course chart from min to max value in RAD @@ -154,22 +161,52 @@ public: } if (showTruW != oldShowTruW) { + if (!twdFlChart) { // Create true wind charts if they don't exist + + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Creating true wind charts"); + auto* twdHstry = pageData.boatHstry->hstryBufList.twdHstry; + auto* twsHstry = pageData.boatHstry->hstryBufList.twsHstry; + // LOG_DEBUG(GwLog::DEBUG,"History Buffer addresses PageWindPlot: twdBuf: %p, twsBuf: %p", (void*)pageData.boatHstry->hstryBufList.twdHstry, + // (void*)pageData.boatHstry->hstryBufList.twsHstry); + + twdFlChart = std::unique_ptr>(new Chart(*twdHstry, 1, 0, dfltRngWd, *commonData, useSimuData)); + twsFlChart = std::unique_ptr>(new Chart(*twsHstry, 0, 0, dfltRngWs, *commonData, useSimuData)); + twdHfChart = std::unique_ptr>(new Chart(*twdHstry, 1, 1, dfltRngWd, *commonData, useSimuData)); + twsHfChart = std::unique_ptr>(new Chart(*twsHstry, 1, 2, dfltRngWs, *commonData, useSimuData)); + // LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: twdHstry: %p, twsHstry: %p", (void*)twdHstry, (void*)twsHstry); + } + + if (!awdFlChart) { // Create apparent wind charts if they don't exist + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Creating apparent wind charts"); + auto* awdHstry = pageData.boatHstry->hstryBufList.awdHstry; + auto* awsHstry = pageData.boatHstry->hstryBufList.awsHstry; + + awdFlChart = std::unique_ptr>(new Chart(*awdHstry, 1, 0, dfltRngWd, *commonData, useSimuData)); + awsFlChart = std::unique_ptr>(new Chart(*awsHstry, 0, 0, dfltRngWs, *commonData, useSimuData)); + awdHfChart = std::unique_ptr>(new Chart(*awdHstry, 1, 1, dfltRngWd, *commonData, useSimuData)); + awsHfChart = std::unique_ptr>(new Chart(*awsHstry, 1, 2, dfltRngWs, *commonData, useSimuData)); + } + + // Switch active charts based on showTruW if (showTruW) { wdHstry = pageData.boatHstry->hstryBufList.twdHstry; wsHstry = pageData.boatHstry->hstryBufList.twsHstry; + wdFlChart = twdFlChart.get(); + wsFlChart = twsFlChart.get(); + wdHfChart = twdHfChart.get(); + wsHfChart = twsHfChart.get(); } else { wdHstry = pageData.boatHstry->hstryBufList.awdHstry; wsHstry = pageData.boatHstry->hstryBufList.awsHstry; + wdFlChart = awdFlChart.get(); + wsFlChart = awsFlChart.get(); + wdHfChart = awdHfChart.get(); + wsHfChart = awsHfChart.get(); } + wdHstry->getMetaData(wdName, wdFormat); wsHstry->getMetaData(wsName, wsFormat); - - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: *wdHstry: %p, *wsHstry: %p", wdHstry, wsHstry); - twdFlChart = std::unique_ptr>(new Chart(*wdHstry, 1, 0, dfltRngWd, *commonData, useSimuData)); - twsFlChart = std::unique_ptr>(new Chart(*wsHstry, 0, 0, dfltRngWs, *commonData, useSimuData)); - twdHfChart = std::unique_ptr>(new Chart(*wdHstry, 1, 1, dfltRngWd, *commonData, useSimuData)); - twsHfChart = std::unique_ptr>(new Chart(*wsHstry, 1, 2, dfltRngWs, *commonData, useSimuData)); - + oldShowTruW = showTruW; } @@ -183,12 +220,12 @@ public: if (chrtMode == 'D') { wdBVal->value = wdHstry->getLast(); wdBVal->valid = wdBVal->value != wdHstry->getMaxVal(); - twdFlChart->showChrt(dataIntv, *bvalue[0]); + wdFlChart->showChrt(dataIntv, *bvalue[0]); } else if (chrtMode == 'S') { wsBVal->value = wsHstry->getLast(); wsBVal->valid = wsBVal->value != wsHstry->getMaxVal(); - twsFlChart->showChrt(dataIntv, *bvalue[1]); + wsFlChart->showChrt(dataIntv, *bvalue[1]); } else if (chrtMode == 'B') { wdBVal->value = wdHstry->getLast(); @@ -197,11 +234,11 @@ public: wsBVal->valid = wsBVal->value != wsHstry->getMaxVal(); LOG_DEBUG(GwLog::DEBUG, "PageWindPlot showChrt: wsBVal.name: %s, format: %s, wsBVal.value: %.1f, valid: %d, address: %p", wsBVal->getName(), wsBVal->getFormat(), wsBVal->value, wsBVal->valid, wsBVal); - twdHfChart->showChrt(dataIntv, *bvalue[0]); - twsHfChart->showChrt(dataIntv, *bvalue[1]); + wdHfChart->showChrt(dataIntv, *bvalue[0]); + wsHfChart->showChrt(dataIntv, *bvalue[1]); } - LOG_DEBUG(GwLog::LOG, "PageWindPlot: runtime: %ldms", millis() - pageTime); + LOG_DEBUG(GwLog::LOG, "PageWindPlot: page time %ldms", millis() - pageTime); return PAGE_UPDATE; } }; diff --git a/lib/obp60task/gen_set.py b/lib/obp60task/gen_set.py index 9d1b0ff..97ca0cf 100755 --- a/lib/obp60task/gen_set.py +++ b/lib/obp60task/gen_set.py @@ -157,7 +157,7 @@ def create_json(device, no_of_pages, pagedata): "description": f"Wind source for page {page_no}: [true|apparent]", "list": [ "True wind", - "Apparant wind" + "Apparent wind" ], "category": category, "capabilities": capabilities, From 625f9c087e0212cbab4dc8761863214f8b701a19 Mon Sep 17 00:00:00 2001 From: Ulrich Meine Date: Sat, 29 Nov 2025 01:21:45 +0100 Subject: [PATCH 062/121] Fixed OBP60Formatter issue with speeds of 9.9999 knots --- lib/obp60task/OBP60Formatter.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/obp60task/OBP60Formatter.cpp b/lib/obp60task/OBP60Formatter.cpp index e4e73e0..8dcf50f 100644 --- a/lib/obp60task/OBP60Formatter.cpp +++ b/lib/obp60task/OBP60Formatter.cpp @@ -305,10 +305,13 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ snprintf(buffer, bsize, "%2.0f", speed); } else{ - if (speed < 10){ + speed = std::round(speed * 100) / 100; // in rare cases, speed could be 9.9999 kn instead of 10.0 kn + LOG_DEBUG(GwLog::DEBUG,"OBPFormatter-formatValue: value->value: %.3f speed: %.15f speed<10: %d", value->value, speed, speed < 10.0); + + if (speed < 10.0){ snprintf(buffer, bsize, fmt_dec_1, speed); } - else if (speed < 100){ + else if (speed < 100.0){ snprintf(buffer, bsize, fmt_dec_10, speed); } else { From 1b5543913526c660e7d186d7d482fb1c52388ea2 Mon Sep 17 00:00:00 2001 From: Ulrich Meine Date: Thu, 4 Dec 2025 23:31:20 +0100 Subject: [PATCH 063/121] Few more pixel adjustments for horizontal half screen charts --- lib/obp60task/OBP60Formatter.cpp | 2 - lib/obp60task/OBPcharts.cpp | 168 +++++++++++++++---------------- lib/obp60task/OBPcharts.h | 6 +- lib/obp60task/PageWindPlot.cpp | 2 + 4 files changed, 84 insertions(+), 94 deletions(-) diff --git a/lib/obp60task/OBP60Formatter.cpp b/lib/obp60task/OBP60Formatter.cpp index 8dcf50f..f51c5fa 100644 --- a/lib/obp60task/OBP60Formatter.cpp +++ b/lib/obp60task/OBP60Formatter.cpp @@ -306,8 +306,6 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ } else{ speed = std::round(speed * 100) / 100; // in rare cases, speed could be 9.9999 kn instead of 10.0 kn - LOG_DEBUG(GwLog::DEBUG,"OBPFormatter-formatValue: value->value: %.3f speed: %.15f speed<10: %d", value->value, speed, speed < 10.0); - if (speed < 10.0){ snprintf(buffer, bsize, fmt_dec_1, speed); } diff --git a/lib/obp60task/OBPcharts.cpp b/lib/obp60task/OBPcharts.cpp index be3fd43..6ba5a83 100644 --- a/lib/obp60task/OBPcharts.cpp +++ b/lib/obp60task/OBPcharts.cpp @@ -30,12 +30,12 @@ Chart::Chart(RingBuffer& dataBuf, int8_t chrtDir, int8_t chrtSz, double df cStart = { 0, top }; break; case 1: - valAxis = (dHeight - top - bottom) / 2 - gap; + valAxis = (dHeight - top - bottom) / 2 - hGap; cStart = { 0, top }; break; case 2: - valAxis = (dHeight - top - bottom) / 2 - gap; - cStart = { 0, top + (valAxis + gap) + gap }; + valAxis = (dHeight - top - bottom) / 2 - hGap; + cStart = { 0, top + (valAxis + hGap) + hGap }; break; default: LOG_DEBUG(GwLog::ERROR, "displayChart: wrong init parameter"); @@ -50,12 +50,12 @@ Chart::Chart(RingBuffer& dataBuf, int8_t chrtDir, int8_t chrtSz, double df cStart = { 0, top }; break; case 1: - valAxis = dWidth / 2 - gap - 1; + valAxis = dWidth / 2 - vGap - 1; cStart = { 0, top }; break; case 2: - valAxis = dWidth / 2 - gap - 1; - cStart = { dWidth / 2 + gap, top }; + valAxis = dWidth / 2 - vGap - 1; + cStart = { dWidth / 2 + vGap, top }; break; default: LOG_DEBUG(GwLog::ERROR, "displayChart: wrong init parameter"); @@ -185,7 +185,7 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) } } - // if (i >= (numBufVals / chrtIntv) - 4) // log chart data of 1 line (adjust for test purposes) + // if (i >= (numBufVals / chrtIntv) - 5) // log chart data of 1 line (adjust for test purposes) // LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Chart: i: %d, chrtVal: %.4f, {x,y} {%d,%d}", i, chrtVal, x, y); if ((i == 0) || (chrtPrevVal == dbMAX_VAL)) { @@ -193,8 +193,8 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) prevX = x; prevY = y; - } else if (chrtDataFmt != 0) { // cross borders check for degree values; shift values to [-PI..0..PI]; when crossing borders, range is 2x PI degrees - // Normalize both values relative to chrtMin (shift range to start at 0) + } else if (chrtDataFmt != 0) { + // cross borders check for degree values; shift values to [-PI..0..PI]; when crossing borders, range is 2x PI degrees double normCurr = WindUtils::to2PI(chrtVal - chrtMin); double normPrev = WindUtils::to2PI(chrtPrevVal - chrtMin); // Check if pixel positions are far apart (crossing chart boundary); happens when one value is near chrtMax and the other near chrtMin @@ -203,18 +203,31 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) if (crossedBorders) { // If current value crosses chart borders compared to previous value, split line // LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Chart: crossedBorders: %d, chrtVal: %.2f, chrtPrevVal: %.2f", crossedBorders, chrtVal, chrtPrevVal); bool wrappingFromHighToLow = normCurr < normPrev; // Determine which edge we're crossing - int xSplit = wrappingFromHighToLow ? (cStart.x + valAxis) : cStart.x; - getdisplay().drawLine(prevX, prevY, xSplit, y, fgColor); - getdisplay().drawLine(prevX, prevY - 1, ((xSplit != prevX) ? xSplit : xSplit - 1), ((xSplit != prevX) ? y - 1 : y), fgColor); - prevX = wrappingFromHighToLow ? cStart.x : (cStart.x + valAxis); + if (chrtDir == 0) { + int ySplit = wrappingFromHighToLow ? (cStart.y + valAxis) : cStart.y; + getdisplay().drawLine(prevX, prevY, x, ySplit, fgColor); + if (x != prevX) { // line with some horizontal trend + getdisplay().drawLine(prevX, prevY - 1, x, ySplit - 1, fgColor); + } else { + getdisplay().drawLine(prevX, prevY - 1, x - 1, ySplit, fgColor); + } + prevY = wrappingFromHighToLow ? cStart.y : (cStart.y + valAxis); + } else { // vertical chart + int xSplit = wrappingFromHighToLow ? (cStart.x + valAxis) : cStart.x; + getdisplay().drawLine(prevX, prevY, xSplit, y, fgColor); + getdisplay().drawLine(prevX, prevY - 1, ((xSplit != prevX) ? xSplit : xSplit - 1), ((xSplit != prevX) ? y - 1 : y), fgColor); + prevX = wrappingFromHighToLow ? cStart.x : (cStart.x + valAxis); + } } } // Draw line with 2 pixels width + make sure vertical lines are drawn correctly - if (chrtDir == 0 || x == prevX) { // vertical line + if (chrtDir == 0 || x == prevX) { // horizontal chart & vertical line +// if (x == prevX) { // vertical line getdisplay().drawLine(prevX, prevY, x, y, fgColor); getdisplay().drawLine(prevX - 1, prevY, x - 1, y, fgColor); - } else if (chrtDir == 1 || x != prevX) { // line with some horizontal trend -> normal state + } else if (chrtDir == 1 || x != prevX) { // vertical chart & line with some horizontal trend -> normal state +// } else { // line with some horizontal trend -> normal state getdisplay().drawLine(prevX, prevY, x, y, fgColor); getdisplay().drawLine(prevX, prevY - 1, x, y - 1, fgColor); } @@ -239,7 +252,7 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) // doesn't work unfortunately when 'simulation data' is active, because OBP60Formatter generates own simulation value in that case currValue.value = dataBuf.getLast(); currValue.valid = currValue.value != dbMAX_VAL; - Chart::prntCurrValue(currValue, { x, y }); + Chart::prntCurrValue(currValue); LOG_DEBUG(GwLog::DEBUG, "Chart drawChrt: currValue-value: %.1f, Valid: %d, Name: %s, Address: %p", currValue.value, currValue.valid, currValue.getName(), (void*)&currValue); } else { @@ -403,7 +416,7 @@ void Chart::drawChrtTimeAxis(int8_t chrtIntv) getdisplay().setTextColor(fgColor); if (chrtDir == 0) { // horizontal chart - getdisplay().fillRect(0, top, dWidth, 2, fgColor); + getdisplay().fillRect(0, cStart.y, dWidth, 2, fgColor); timeRng = chrtIntv * 4; // Chart time interval: [1] 4 min., [2] 8 min., [3] 12 min., [4] 16 min., [8] 32 min. slots = timAxis / 80.0; // number of axis labels @@ -429,7 +442,7 @@ void Chart::drawChrtTimeAxis(int8_t chrtIntv) i -= intv; } - } else { // chrtDir == 1; vertical chart + } else { // vertical chart timeRng = chrtIntv * 4; // chart time interval: [1] 4 min., [2] 8 min., [3] 12 min., [4] 16 min., [8] 32 min. slots = timAxis / 75.0; // number of axis labels intv = timeRng / slots; // minutes per chart axis interval @@ -465,6 +478,7 @@ void Chart::drawChrtValAxis() int i, intv; double cVal, cchrtRng, crngMin; char sVal[6]; + int sLen; std::unique_ptr tmpBVal; // Temp variable to get formatted and converted data value from OBP60Formatter tmpBVal = std::unique_ptr(new GwApi::BoatValue(dataBuf.getName())); tmpBVal->setFormat(dataBuf.getFormat()); @@ -477,24 +491,48 @@ void Chart::drawChrtValAxis() intv = static_cast(round(cchrtRng / slots)); i = intv; - getdisplay().setFont(&Ubuntu_Bold12pt8b); + if (chrtSz == 0) { // full size chart -> print multiple value lines + getdisplay().setFont(&Ubuntu_Bold12pt8b); + for (int j = 60; j < valAxis - 30; j += 60) { + getdisplay().drawLine(cStart.x, cStart.y + j, cStart.x + timAxis, cStart.y + j, fgColor); - for (int j = 60; j < valAxis - 30; j += 60) { - LOG_DEBUG(GwLog::DEBUG, "ChartValAxis: chrtRng: %.2f, cchrtRng: %.2f, intv: %d, slots: %.1f, valAxis: %d, i: %d, j: %d", chrtRng, cchrtRng, intv, slots, valAxis, i, j); - getdisplay().drawLine(cStart.x, cStart.y + j, cStart.x + timAxis, cStart.y + j, fgColor); + getdisplay().fillRect(cStart.x, cStart.y + j - 11, 42, 21, bgColor); // Clear small area to remove potential chart lines + String sVal = String(i); + getdisplay().setCursor((3 - sVal.length()) * 10, cStart.y + j + 7); // value right-formated + getdisplay().printf("%s", sVal); // Range value - getdisplay().fillRect(cStart.x, cStart.y + j - 11, cStart.x + 39, 21, bgColor); // Clear small area to remove potential chart lines - String sVal = String(i); - getdisplay().setCursor((3 - sVal.length()) * 10, cStart.y + j + 7); // value right-formated - getdisplay().printf("%s", sVal); // Range value + i += intv; + } + } else { // half size chart -> print just edge values + middle chart line + getdisplay().setFont(&Ubuntu_Bold10pt8b); - i += intv; + tmpBVal->value = chrtMin; + cVal = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) + sLen = snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); + getdisplay().fillRect(cStart.x, cStart.y + 2, 42, 16, bgColor); // Clear small area to remove potential chart lines + getdisplay().setCursor(cStart.x + ((3 - sLen) * 10), cStart.y + 16); + getdisplay().printf("%s", sVal); // Range low end + + tmpBVal->value = chrtMid; + cVal = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) + sLen = snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); + getdisplay().fillRect(cStart.x, cStart.y + (valAxis / 2) - 9, 42, 16, bgColor); // Clear small area to remove potential chart lines + getdisplay().setCursor(cStart.x + ((3 - sLen) * 10), cStart.y + (valAxis / 2) + 5); + getdisplay().printf("%s", sVal); // Range mid value + getdisplay().drawLine(cStart.x + 43, cStart.y + (valAxis / 2), cStart.x + timAxis, cStart.y + (valAxis / 2), fgColor); + + tmpBVal->value = chrtMax; + cVal = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) + sLen = snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); + getdisplay().fillRect(cStart.x, cStart.y + valAxis - 16, 42, 16, bgColor); // Clear small area to remove potential chart lines + getdisplay().setCursor(cStart.x + ((3 - sLen) * 10), cStart.y + valAxis - 1); + getdisplay().printf("%s", sVal); // Range high end } getdisplay().setFont(&Ubuntu_Bold12pt8b); drawTextRalign(cStart.x + timAxis, cStart.y - 3, dbName); // buffer data name - } else { // chrtDir == 1; vertical chart + } else { // vertical chart if (chrtSz == 0) { // full size chart -> use larger font getdisplay().setFont(&Ubuntu_Bold12pt8b); drawTextCenter(cStart.x + (valAxis / 4) + 25, cStart.y - 10, dbName); // buffer data name @@ -503,10 +541,10 @@ void Chart::drawChrtValAxis() } getdisplay().fillRect(cStart.x, top, valAxis, 2, fgColor); // top chart line - getdisplay().setCursor(cStart.x, cStart.y - 2); tmpBVal->value = chrtMin; cVal = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); + getdisplay().setCursor(cStart.x, cStart.y - 2); getdisplay().printf("%s", sVal); // Range low end tmpBVal->value = chrtMid; @@ -523,33 +561,29 @@ void Chart::drawChrtValAxis() getdisplay().drawLine(cStart.x + j, cStart.y, cStart.x + j, cStart.y + timAxis, fgColor); } -// if (chrtSz == 0) { -// getdisplay().setFont(&Ubuntu_Bold12pt8b); -// drawTextCenter(cStart.x + (valAxis / 4) + 15, cStart.y - 11, dbName); // buffer data name -// } - LOG_DEBUG(GwLog::DEBUG, "ChartGrd: chrtRng: %.2f, intv: %d, slots: %.1f, valAxis: %d, i: %d", chrtRng, intv, slots, valAxis, i); + // if (chrtSz == 0) { + // getdisplay().setFont(&Ubuntu_Bold12pt8b); + // drawTextCenter(cStart.x + (valAxis / 4) + 15, cStart.y - 11, dbName); // buffer data name + // } } } // Print current data value template -void Chart::prntCurrValue(GwApi::BoatValue& currValue, const Pos chrtPos) +void Chart::prntCurrValue(GwApi::BoatValue& currValue) { - int currentZone; - static int lastZone = 0; - static bool flipVal = false; - int xPosVal; - static const int yPosVal = (chrtDir == 0) ? cStart.y + valAxis - 5 : cStart.y + timAxis - 5; - xPosVal = cStart.x + 1; + const int xPosVal = (chrtDir == 0) ? cStart.x + (timAxis / 2) - 56 : cStart.x + 32; + const int yPosVal = (chrtDir == 0) ? cStart.y + valAxis - 7 : cStart.y + timAxis - 7; FormattedData frmtDbData = formatValue(&currValue, *commonData); double testdbValue = frmtDbData.value; String sdbValue = frmtDbData.svalue; // value (string) String dbUnit = frmtDbData.unit; // Unit of value - LOG_DEBUG(GwLog::DEBUG, "Chart CurrValue: dbValue: %.2f, sdbValue: %s, fmrtDbValue: %.2f, dbFormat: %s, dbUnit: %s, Valid: %d, Name: %s, Address: %p", currValue.value, sdbValue, - testdbValue, currValue.getFormat(), dbUnit, currValue.valid, currValue.getName(), currValue); + // LOG_DEBUG(GwLog::DEBUG, "Chart CurrValue: dbValue: %.2f, sdbValue: %s, fmrtDbValue: %.2f, dbFormat: %s, dbUnit: %s, Valid: %d, Name: %s, Address: %p", currValue.value, sdbValue, + // testdbValue, currValue.getFormat(), dbUnit, currValue.valid, currValue.getName(), currValue); - getdisplay().fillRect(xPosVal, yPosVal - 34, 122, 40, bgColor); // Clear area for TWS value + getdisplay().fillRect(xPosVal - 1, yPosVal - 34, 125, 42, bgColor); // Clear area for TWS value + getdisplay().drawRect(xPosVal, yPosVal - 33, 123, 40, fgColor); // Draw box for TWS value getdisplay().setFont(&DSEG7Classic_BoldItalic16pt7b); getdisplay().setCursor(xPosVal + 1, yPosVal); if (useSimuData) { @@ -563,54 +597,10 @@ void Chart::prntCurrValue(GwApi::BoatValue& currValue, const Pos chrtPos) getdisplay().print(dbName); // Name getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(xPosVal + 76, yPosVal + 1); + getdisplay().setCursor(xPosVal + 76, yPosVal + 0); getdisplay().print(dbUnit); // Unit } -// Identify Min and Max values of range for course data and select them considering smallest gap -// E.g., Min=30°, Max=270° will be converted to smaller range of Min=270° and Max=30° -// obsolete; creates random results by purpose with large data arrays when data is equally distributed -template -void Chart::getAngleMinMax(const std::vector& angles, double& rngMin, double& rngMax) -{ - if (angles.empty()) { - rngMin = 0; - rngMax = 0; - return; - } - - if (angles.size() == 1) { - rngMin = angles[0]; - rngMax = angles[0]; - return; - } - - // Sort angles - std::vector sorted = angles; - std::sort(sorted.begin(), sorted.end()); - - // Find the largest gap between consecutive angles - double maxGap = 0.0; - int maxGapIndex = 0; - for (size_t i = 0; i < sorted.size(); i++) { - double next = sorted[(i + 1) % sorted.size()]; - double curr = sorted[i]; - - // Calculate gap (wrapping around at 360°/2*Pi) - double gap = (i == sorted.size() - 1) ? (M_TWOPI - curr + next) : (next - curr); - - if (gap > maxGap) { - maxGap = gap; - maxGapIndex = i; - } - } - - // The range is on the opposite side of the largest gap - // Min is after the gap, max is before it - rngMin = sorted[(maxGapIndex + 1) % sorted.size()]; - rngMax = sorted[maxGapIndex]; -} - // Explicitly instantiate class with required data types to avoid linker errors template class Chart; // --- Class Chart --------------- diff --git a/lib/obp60task/OBPcharts.h b/lib/obp60task/OBPcharts.h index 581ab85..677be04 100644 --- a/lib/obp60task/OBPcharts.h +++ b/lib/obp60task/OBPcharts.h @@ -25,7 +25,8 @@ protected: int top = 48; // display top header lines int bottom = 22; // display bottom lines - int gap = 20; // gap between 2 charts; actual gap is 2x + int hGap = 10; // gap between 2 horizontal charts; actual gap is 2x + int vGap = 20; // gap between 2 vertical charts; actual gap is 2x int xOffset = 33; // offset for horizontal axis (time/value), because of space for left vertical axis labeling int yOffset = 10; // offset for vertical axis (time/value), because of space for top horizontal axis labeling int dWidth; // Display width @@ -58,8 +59,7 @@ protected: void calcChrtBorders(double& rngMid, double& rngMin, double& rngMax, double& rng); // Calculate chart points for value axis and return range between and void drawChrtTimeAxis(int8_t chrtIntv); // Draw time axis of chart, value and lines void drawChrtValAxis(); // Draw value axis of chart, value and lines - void prntCurrValue(GwApi::BoatValue& currValue, Pos chrtPos); // Add current boat data value to chart - void getAngleMinMax(const std::vector& angles, double& rngMin, double& rngMax); // Identify Min and Max for course data with smallest gap + void prntCurrValue(GwApi::BoatValue& currValue); // Add current boat data value to chart public: Chart(RingBuffer& dataBuf, int8_t chrtDir, int8_t chrtSz, double dfltRng, CommonData& common, bool useSimuData); // Chart object of data chart diff --git a/lib/obp60task/PageWindPlot.cpp b/lib/obp60task/PageWindPlot.cpp index b9fbc67..0b4e884 100644 --- a/lib/obp60task/PageWindPlot.cpp +++ b/lib/obp60task/PageWindPlot.cpp @@ -173,6 +173,8 @@ public: twsFlChart = std::unique_ptr>(new Chart(*twsHstry, 0, 0, dfltRngWs, *commonData, useSimuData)); twdHfChart = std::unique_ptr>(new Chart(*twdHstry, 1, 1, dfltRngWd, *commonData, useSimuData)); twsHfChart = std::unique_ptr>(new Chart(*twsHstry, 1, 2, dfltRngWs, *commonData, useSimuData)); + // twdHfChart = std::unique_ptr>(new Chart(*twdHstry, 0, 1, dfltRngWd, *commonData, useSimuData)); + // twsHfChart = std::unique_ptr>(new Chart(*twsHstry, 0, 2, dfltRngWs, *commonData, useSimuData)); // LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: twdHstry: %p, twsHstry: %p", (void*)twdHstry, (void*)twsHstry); } From 0f50b614eb510e1abb1e49d42daefcb1274386f3 Mon Sep 17 00:00:00 2001 From: Ulrich Meine Date: Fri, 5 Dec 2025 00:10:31 +0100 Subject: [PATCH 064/121] Add lower chart line for horizontal half chart; write current value after chart lines --- lib/obp60task/OBPcharts.cpp | 23 +++++++++++++---------- lib/obp60task/OBPcharts.h | 3 ++- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/lib/obp60task/OBPcharts.cpp b/lib/obp60task/OBPcharts.cpp index 6ba5a83..17caf75 100644 --- a/lib/obp60task/OBPcharts.cpp +++ b/lib/obp60task/OBPcharts.cpp @@ -107,6 +107,15 @@ void Chart::showChrt(int8_t chrtIntv, GwApi::BoatValue currValue) drawChrt(chrtIntv, currValue); drawChrtTimeAxis(chrtIntv); drawChrtValAxis(); + + if (bufDataValid) { + // uses BoatValue temp variable to format latest buffer value + // doesn't work unfortunately when 'simulation data' is active, because OBP60Formatter generates own simulation value in that case + currValue.value = dataBuf.getLast(); + currValue.valid = currValue.value != dbMAX_VAL; + Chart::prntCurrValue(currValue); + LOG_DEBUG(GwLog::DEBUG, "Chart drawChrt: currValue-value: %.1f, Valid: %d, Name: %s, Address: %p", currValue.value, currValue.valid, currValue.getName(), (void*)&currValue); + } } // draw chart @@ -116,7 +125,7 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) double chrtVal; // Current data value double chrtScl; // Scale for data values in pixels per value static double chrtPrevVal; // Last data value in chart area - bool bufDataValid = false; // Flag to indicate if buffer data is valid + // bool bufDataValid = false; // Flag to indicate if buffer data is valid static int numNoData; // Counter for multiple invalid data values in a row int x, y; // x and y coordinates for drawing @@ -248,13 +257,6 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) } } - // uses BoatValue temp variable to format latest buffer value - // doesn't work unfortunately when 'simulation data' is active, because OBP60Formatter generates own simulation value in that case - currValue.value = dataBuf.getLast(); - currValue.valid = currValue.value != dbMAX_VAL; - Chart::prntCurrValue(currValue); - LOG_DEBUG(GwLog::DEBUG, "Chart drawChrt: currValue-value: %.1f, Valid: %d, Name: %s, Address: %p", currValue.value, currValue.valid, currValue.getName(), (void*)&currValue); - } else { // No valid data available getdisplay().setFont(&Ubuntu_Bold10pt8b); @@ -527,6 +529,7 @@ void Chart::drawChrtValAxis() getdisplay().fillRect(cStart.x, cStart.y + valAxis - 16, 42, 16, bgColor); // Clear small area to remove potential chart lines getdisplay().setCursor(cStart.x + ((3 - sLen) * 10), cStart.y + valAxis - 1); getdisplay().printf("%s", sVal); // Range high end + getdisplay().drawLine(cStart.x + 43, cStart.y + valAxis, cStart.x + timAxis, cStart.y + valAxis, fgColor); } getdisplay().setFont(&Ubuntu_Bold12pt8b); @@ -582,8 +585,8 @@ void Chart::prntCurrValue(GwApi::BoatValue& currValue) // LOG_DEBUG(GwLog::DEBUG, "Chart CurrValue: dbValue: %.2f, sdbValue: %s, fmrtDbValue: %.2f, dbFormat: %s, dbUnit: %s, Valid: %d, Name: %s, Address: %p", currValue.value, sdbValue, // testdbValue, currValue.getFormat(), dbUnit, currValue.valid, currValue.getName(), currValue); - getdisplay().fillRect(xPosVal - 1, yPosVal - 34, 125, 42, bgColor); // Clear area for TWS value - getdisplay().drawRect(xPosVal, yPosVal - 33, 123, 40, fgColor); // Draw box for TWS value + getdisplay().fillRect(xPosVal - 1, yPosVal - 34, 125, 41, bgColor); // Clear area for TWS value + getdisplay().drawRect(xPosVal, yPosVal - 33, 123, 39, fgColor); // Draw box for TWS value getdisplay().setFont(&DSEG7Classic_BoldItalic16pt7b); getdisplay().setCursor(xPosVal + 1, yPosVal); if (useSimuData) { diff --git a/lib/obp60task/OBPcharts.h b/lib/obp60task/OBPcharts.h index 677be04..db298a5 100644 --- a/lib/obp60task/OBPcharts.h +++ b/lib/obp60task/OBPcharts.h @@ -25,7 +25,7 @@ protected: int top = 48; // display top header lines int bottom = 22; // display bottom lines - int hGap = 10; // gap between 2 horizontal charts; actual gap is 2x + int hGap = 11; // gap between 2 horizontal charts; actual gap is 2x int vGap = 20; // gap between 2 vertical charts; actual gap is 2x int xOffset = 33; // offset for horizontal axis (time/value), because of space for left vertical axis labeling int yOffset = 10; // offset for vertical axis (time/value), because of space for top horizontal axis labeling @@ -52,6 +52,7 @@ protected: size_t currIdx; // Current index in TWD history buffer size_t lastIdx; // Last index of TWD history buffer size_t lastAddedIdx = 0; // Last index of TWD history buffer when new data was added + bool bufDataValid = false; // Flag to indicate if buffer data is valid int oldChrtIntv = 0; // remember recent user selection of data interval void drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue); // Draw chart line From eab7d74aef6d697b3f1aaac5768fc3c4f1f65b20 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 5 Dec 2025 12:27:47 +0100 Subject: [PATCH 065/121] More robust HTTP connection for data reading --- lib/obp60task/NetworkClient.cpp | 21 +++++++++++++++++---- lib/obp60task/NetworkClient.h | 8 +++++--- lib/obp60task/OBP60Extensions.h | 2 ++ lib/obp60task/PageNavigation.cpp | 8 ++++---- 4 files changed, 28 insertions(+), 11 deletions(-) diff --git a/lib/obp60task/NetworkClient.cpp b/lib/obp60task/NetworkClient.cpp index 9e75ae5..66b973d 100644 --- a/lib/obp60task/NetworkClient.cpp +++ b/lib/obp60task/NetworkClient.cpp @@ -48,23 +48,33 @@ int NetworkClient::skipGzipHeader(const uint8_t* data, size_t len) { // HTTP GET + GZIP Decompression (reading in chunks) bool NetworkClient::httpGetGzip(const String& url, uint8_t*& outData, size_t& outLen) { - const size_t capacity = READLIMIT; // limit (can be adjusted in NetworkClient.h) + const size_t capacity = READLIMIT; // Read limit for data (can be adjusted in NetworkClient.h) uint8_t* buffer = (uint8_t*)malloc(capacity); if (!buffer) { - if (DEBUG) {Serial.println("Malloc failed (buffer)");} + if (DEBUG) {Serial.println("Malloc failed (buffer");} return false; } HTTPClient http; + + // Timeouts to prevent hanging connections + http.setConnectTimeout(CONNECTIONTIMEOUT); // Connect timeout in ms (can be adjusted in NetworkClient.h) + http.setTimeout(TCPREADTIMEOUT); // Read timeout in ms (can be adjusted in NetworkClient.h) + http.begin(url); http.addHeader("Accept-Encoding", "gzip"); int code = http.GET(); if (code != HTTP_CODE_OK) { Serial.printf("HTTP ERROR: %d\n", code); - free(buffer); + + // Hard reset HTTP + socket + WiFiClient* tmp = http.getStreamPtr(); + if (tmp) tmp->stop(); // Force close TCP socket http.end(); + + free(buffer); return false; } @@ -72,7 +82,7 @@ bool NetworkClient::httpGetGzip(const String& url, uint8_t*& outData, size_t& ou size_t len = 0; uint32_t lastData = millis(); - const uint32_t READ_TIMEOUT = NETWORKTIMEOUT; // Network timeout for reading data (can be adjusted in NetworkClient.h) + const uint32_t READ_TIMEOUT = READDATATIMEOUT; // Timeout for reading data (can be adjusted in NetworkClient.h) bool complete = false; @@ -122,6 +132,9 @@ bool NetworkClient::httpGetGzip(const String& url, uint8_t*& outData, size_t& ou free(test); } + // --- Added: Force-close connection in all cases to avoid stuck TCP sockets --- + if (stream) stream->stop(); + http.end(); free(buffer); diff --git a/lib/obp60task/NetworkClient.h b/lib/obp60task/NetworkClient.h index 1a29178..84d7a87 100644 --- a/lib/obp60task/NetworkClient.h +++ b/lib/obp60task/NetworkClient.h @@ -3,9 +3,11 @@ #include #include -#define DEBUG false // Debug flag for NetworkClient for more live information -#define READLIMIT 200000 // HTTP read limit in byte for gzip content (can be adjusted) -#define NETWORKTIMEOUT 8000 // 8s Network timeout +#define DEBUG false // Debug flag for NetworkClient for more live information +#define READLIMIT 200000 // HTTP read limit in byte for gzip content (can be adjusted) +#define CONNECTIONTIMEOUT 3000 // Timeout in ms for HTTP connection +#define TCPREADTIMEOUT 2000 // Timeout in ms for read HTTP client stack +#define READDATATIMEOUT 2000 // Timeout in ms for read data class NetworkClient { public: diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index 15d2396..33b9218 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -30,6 +30,8 @@ #define FRAM_BAROGRAPH_START 0x0400 #define FRAM_BAROGRAPH_END 0x13FF +#define PI 3.1415926535897932384626433832795 + extern Adafruit_FRAM_I2C fram; extern bool hasFRAM; extern bool hasSDCard; diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp index 61fae93..43803fa 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -64,7 +64,7 @@ public: static double value2old = 0; static String svalue2old = ""; static String unit2old = ""; - static double value3old = 0; + static double value3old = 0; // Deg static String svalue3old = ""; static String unit3old = ""; static double value4old = 0; @@ -162,8 +162,8 @@ public: } // COG value (Course Over Ground) if(valid3){ - courseOverGround = value3; - value3old = value3; + courseOverGround = (value3 * 360) / (2 * PI); + value3old = courseOverGround; } else{ courseOverGround = value3old; @@ -196,7 +196,7 @@ public: "zoom=" + zoom + // Zoom level: 15 "&lat=" + String(latitude, 6) + // Latitude "&lon=" + String(longitude, 6) + // Longitude - "&mrot=" + int(courseOverGround) + // Rotation angle navigation map + "&mrot=" + int(courseOverGround) + // Rotation angle navigation map in degree "&mtype=1" + // Open Street Map "&dtype=4" + // Dithering type: Atkinson dithering "&width=400" + // With navigation map From ae2b7047f5afb5ecefca2fa2c11179b1898baa7a Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 5 Dec 2025 18:33:42 +0100 Subject: [PATCH 066/121] Add settings for PageNavigation (multi map) --- lib/obp60task/PageNavigation.cpp | 116 +++++++++++++++++++++++++++---- lib/obp60task/config.json | 111 +++++++++++++++++++++++++++++ lib/obp60task/config_obp40.json | 111 +++++++++++++++++++++++++++++ 3 files changed, 324 insertions(+), 14 deletions(-) diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp index 43803fa..b27f830 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -15,7 +15,8 @@ ImageDecoder decoder; // Define image decoder class PageNavigation : public Page { // Values for buttons -int zoom = 15; // Zoom level 1...17 +bool firstRun = true; // Detect the first page run +int zoom = 15; // Default zoom level bool showValues = false; // Show values COG, SOG, DBT in navigation map public: @@ -57,6 +58,31 @@ public: GwConfigHandler *config = commonData->config; GwLog *logger = commonData->logger; + // Get config data + String mapsource = config->getString(config->mapsource); + String ipAddress = config->getString(config->ipAddress); + int localPort = config->getInt(config->localPort); + String mapType = config->getString(config->maptype); + int zoomLevel = config->getInt(config->zoomlevel); + bool grid = config->getBool(config->grid); + String orientation = config->getString(config->orientation); + int refreshDistance = config->getInt(config->refreshDistance); + + if(firstRun == true){ + zoom = zoomLevel; // Over write zoom level with setup value + firstRun = false; // Restet variable + } + + // Local variables + String server = "norbert-walter.dnshome.de"; + int port = 80; + int mType = 1; + int dType = 1; + int mapRot = 0; + int symbolRot = 0; + int mapGrid = 0; + + // Old values for hold function static double value1old = 0; static String svalue1old = ""; @@ -185,29 +211,91 @@ public: depthBelowTransducer = value5old; } - // Server settings - String server = "norbert-walter.dnshome.de"; - int port = 80; + // Prepare config values for URL + //************************************************* + + // Server settings + if(mapsource == "OBP Service"){ + server = "norbert-walter.dnshome.de"; + port = 80; + } + else if(mapsource == "Local Service"){ + server = String(ipAddress); + port = localPort; + } + else{ + server = "norbert-walter.dnshome.de"; + port = 80; + } + + // Type of navigation map + if(mapType == "Open Street Map"){ + mType = 1; + dType = 1; + } + else if(mapType == "Google Street"){ + mType = 3; + dType = 2; + } + else if(mapType == "Open Topo Map"){ + mType = 5; + dType = 2; + } + else if(mapType == "Stadimaps Toner"){ + mType = 7; + dType = 1; + } + else if(mapType == "Free Nautical Chart"){ + mType = 9; + dType = 1; + } + else{ + mType = 1; + dType = 1; + } + + // Map grid on/off + if(grid == true){ + mapGrid = 1; + } + else{ + mapGrid = 0; + } + + // Map orientation + if(orientation == "North Direction"){ + mapRot = 0; + symbolRot = courseOverGround;mapGrid = 0; + } + else if(orientation == "Travel Direction"){ + mapRot = courseOverGround; + symbolRot = courseOverGround; + } + else{ + mapRot = 0; + symbolRot = courseOverGround; + } + // URL to OBP Maps Converter // For more details see: https://github.com/norbert-walter/maps-converter String url = String("http://") + server + ":" + port + // OBP Server String("/get_image_json?") + // Service: Output B&W picture as JSON (Base64 + gzip) - "zoom=" + zoom + // Zoom level: 15 + "zoom=" + zoom + // Default zoom level: 15 "&lat=" + String(latitude, 6) + // Latitude "&lon=" + String(longitude, 6) + // Longitude - "&mrot=" + int(courseOverGround) + // Rotation angle navigation map in degree - "&mtype=1" + // Open Street Map - "&dtype=4" + // Dithering type: Atkinson dithering + "&mrot=" + mapRot + // Rotation angle navigation map in degree + "&mtype=" + mType + // Default Map: Open Street Map + "&dtype=" + dType + // Dithering type: Atkinson dithering "&width=400" + // With navigation map "&height=250" + // Height navigation map "&cutout=0" + // No picture cutouts "&tab=0" + // No tab size "&border=2" + // Border line size: 2 pixel "&symbol=2" + // Symbol: Triangle - "&srot=" + int(courseOverGround) + // Symbol rotation angle + "&srot=" + symbolRot + // Symbol rotation angle "&ssize=15" + // Symbole size: 15 pixel - "&grid=0" // Show grid: On + "&grid=" + mapGrid // Show grid: On ; // Draw page @@ -238,9 +326,9 @@ public: memcpy(b64, b64src, b64len + 1); // Copy Base64 content in PSRAM // Set image buffer in PSRAM - //size_t imgSize = getdisplay().width() * getdisplay().height(); // Calculate image size + //size_t imgSize = getdisplay().width() * getdisplay().height(); size_t imgSize = numPix; // Calculate image size - uint8_t* imageData = (uint8_t*) heap_caps_malloc(imgSize, MALLOC_CAP_SPIRAM); // Allocate PSRAM for image + uint8_t* imageData = (uint8_t*) heap_caps_malloc(imgSize, MALLOC_CAP_SPIRAM); // Allocate PSRAM for image if (!imageData) { LOG_DEBUG(GwLog::ERROR,"Error PageNavigation: PPSRAM alloc image buffer failed"); free(b64); @@ -271,8 +359,8 @@ public: if(showValues == true){ // Frame - getdisplay().fillRect(0, 25 , 130, 70, commonData->fgcolor); // Black rect - getdisplay().fillRect(2, 27 , 126, 66, commonData->bgcolor); // White rect + getdisplay().fillRect(0, 25 , 130, 65, commonData->fgcolor); // Black rect + getdisplay().fillRect(2, 27 , 126, 61, commonData->bgcolor); // White rect // COG getdisplay().setCursor(10, 45); getdisplay().print(name3); diff --git a/lib/obp60task/config.json b/lib/obp60task/config.json index 2de3c59..a0080a4 100644 --- a/lib/obp60task/config.json +++ b/lib/obp60task/config.json @@ -913,6 +913,117 @@ { "calInstance3": ["AWA", "AWS", "COG", "DBT", "HDM", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } ] }, + { + "name": "mapsource", + "label": "Map Source", + "type": "list", + "default": "OBP Service", + "description": "Type of map source, cloud service or local service", + "list": [ + "OBP Service", + "Local Service" + ], + "category": "OBP60 Navigation", + "capabilities": { + "obp60":"true" + } + }, + { + "name": "ipAddress", + "label": "IP Address", + "type": "string", + "default": "192.168.15.10", + "check": "checkIpAddress", + "description": "IP address only for local map service e.g. 192.168.15.10\nor an MDNS name like Raspi.local", + "category": "OBP60 Navigation", + "capabilities": { + "obp60":"true" + } + }, + { + "name": "localPort", + "label": "Port", + "type": "number", + "default": "8080", + "check":"checkPort", + "description": "TCP port for local map server", + "category": "TCP client", + "capabilities": { + "obp60":"true" + } + }, + { + "name": "maptype", + "label": "Map Type", + "type": "list", + "default": "Open Street Map", + "description": "Type of base navigation map with sea marks overlay", + "list": [ + "Open Street Map", + "Google Street", + "Open Topo Map", + "Stadimaps Toner", + "Free Nautical Chart" + ], + "category": "OBP60 Navigation", + "capabilities": { + "obp60":"true" + } + }, + { + "name": "zoomlevel", + "label": "Default Zool Level", + "type": "number", + "default": "15", + "check": "checkMinMax", + "min": 7, + "max": 17, + "description": "Zoom level for map [7..17]; 15 = default", + "category": "OBP60 Navigation", + "capabilities": { + "obp60":"true" + } + }, + { + "name": "grid", + "label": "Show Grid", + "type": "boolean", + "default": "false", + "description": "Show the grid for latutude and longitude", + "category": "OBP60 Navigation", + "capabilities": { + "obp60": "true" + } + }, + { + "name": "orientation", + "label": "Map Orientation", + "type": "list", + "default": "North Dirirection", + "description": "Map orientation for navigation", + "list": [ + "North Direction", + "Travel Direction" + ], + "category": "OBP60 Navigation", + "capabilities": { + "obp60":"true" + } + }, + { + "name": "refreshDistance", + "label": "Refresh Distance [m]", + "type": "number", + "default": "15", + "check": "checkMinMax", + "min": 1, + "max": 50, + "description": "Refresh distance between updates [1..50 m], 15 m = default", + "category": "OBP60 Navigation", + "capabilities": { + "obp60":"true" + } + }, { "name": "display", "label": "Display Mode", diff --git a/lib/obp60task/config_obp40.json b/lib/obp60task/config_obp40.json index 30c7b2d..ff84a89 100644 --- a/lib/obp60task/config_obp40.json +++ b/lib/obp60task/config_obp40.json @@ -924,6 +924,117 @@ { "calInstance3": ["AWA", "AWS", "COG", "DBT", "HDM", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } ] }, + { + "name": "mapsource", + "label": "Map Source", + "type": "list", + "default": "OBP Service", + "description": "Type of map source, cloud service or local service", + "list": [ + "OBP Service", + "Local Service" + ], + "category": "OBP60 Navigation", + "capabilities": { + "obp40":"true" + } + }, + { + "name": "ipAddress", + "label": "IP Address", + "type": "string", + "default": "192.168.15.10", + "check": "checkIpAddress", + "description": "IP address only for local map service e.g. 192.168.15.10\nor an MDNS name like Raspi.local", + "category": "OBP60 Navigation", + "capabilities": { + "obp40":"true" + } + }, + { + "name": "localPort", + "label": "Port", + "type": "number", + "default": "8080", + "check":"checkPort", + "description": "TCP port for local map server", + "category": "TCP client", + "capabilities": { + "obp40":"true" + } + }, + { + "name": "maptype", + "label": "Map Type", + "type": "list", + "default": "Open Street Map", + "description": "Type of base navigation map with sea marks overlay", + "list": [ + "Open Street Map", + "Google Street", + "Open Topo Map", + "Stadimaps Toner", + "Free Nautical Chart" + ], + "category": "OBP60 Navigation", + "capabilities": { + "obp40":"true" + } + }, + { + "name": "zoomlevel", + "label": "Default Zool Level", + "type": "number", + "default": "15", + "check": "checkMinMax", + "min": 7, + "max": 17, + "description": "Zoom level for map [7..17]; 15 = default", + "category": "OBP60 Navigation", + "capabilities": { + "obp40":"true" + } + }, + { + "name": "grid", + "label": "Show Grid", + "type": "boolean", + "default": "false", + "description": "Show the grid for latutude and longitude", + "category": "OBP60 Navigation", + "capabilities": { + "obp40": "true" + } + }, + { + "name": "orientation", + "label": "Map Orientation", + "type": "list", + "default": "North Dirirection", + "description": "Map orientation for navigation", + "list": [ + "North Direction", + "Travel Direction" + ], + "category": "OBP60 Navigation", + "capabilities": { + "obp40":"true" + } + }, + { + "name": "refreshDistance", + "label": "Refresh Distance [m]", + "type": "number", + "default": "15", + "check": "checkMinMax", + "min": 1, + "max": 50, + "description": "Refresh distance between updates [1..50 m], 15 m = default", + "category": "OBP60 Navigation", + "capabilities": { + "obp40":"true" + } + }, { "name": "display", "label": "Display Mode", From f08a119f4086f66053b6821ae010a97ac91851b7 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 5 Dec 2025 18:41:21 +0100 Subject: [PATCH 067/121] Code cleaning --- lib/obp60task/PageNavigation.cpp | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp index b27f830..9b15f30 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -59,6 +59,11 @@ public: GwLog *logger = commonData->logger; // Get config data + String lengthformat = config->getString(config->lengthFormat); + // bool simulation = config->getBool(config->useSimuData); + bool holdvalues = config->getBool(config->holdvalues); + String flashLED = config->getString(config->flashLED); + String backlightMode = config->getString(config->backlight); String mapsource = config->getString(config->mapsource); String ipAddress = config->getString(config->ipAddress); int localPort = config->getInt(config->localPort); @@ -106,13 +111,6 @@ public: static double speedOverGround = 0; static double depthBelowTransducer = 0; - // Get config data - String lengthformat = config->getString(config->lengthFormat); - // bool simulation = config->getBool(config->useSimuData); - bool holdvalues = config->getBool(config->holdvalues); - String flashLED = config->getString(config->flashLED); - String backlightMode = config->getString(config->backlight); - // Get boat values #1 Latitude GwApi::BoatValue *bvalue1 = pageData.values[0]; // First element in list (only one value by PageOneValue) String name1 = xdrDelete(bvalue1->getName()); // Value name @@ -168,8 +166,9 @@ public: if (bvalue1 == NULL) return PAGE_OK; // WTF why this statement? LOG_DEBUG(GwLog::LOG,"Drawing at PageNavigation, %s: %f, %s: %f, %s: %f, %s: %f", name1.c_str(), value1, name2.c_str(), value2, name3.c_str(), value3, name4.c_str(), value4); - // Load navigation map + // Set variables //*********************************************************** + // Latitude if(valid1){ latitude = value1; @@ -212,7 +211,7 @@ public: } // Prepare config values for URL - //************************************************* + //*********************************************************** // Server settings if(mapsource == "OBP Service"){ @@ -230,8 +229,8 @@ public: // Type of navigation map if(mapType == "Open Street Map"){ - mType = 1; - dType = 1; + mType = 1; // Map type + dType = 1; // Dithering type } else if(mapType == "Google Street"){ mType = 3; @@ -265,7 +264,7 @@ public: // Map orientation if(orientation == "North Direction"){ mapRot = 0; - symbolRot = courseOverGround;mapGrid = 0; + symbolRot = courseOverGround; } else if(orientation == "Travel Direction"){ mapRot = courseOverGround; @@ -276,6 +275,8 @@ public: symbolRot = courseOverGround; } + // Load navigation map + //*********************************************************** // URL to OBP Maps Converter // For more details see: https://github.com/norbert-walter/maps-converter From fc097b09febf0ea74bf7adc9865bd34e5569d8ac Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Sat, 6 Dec 2025 17:25:34 +0100 Subject: [PATCH 068/121] Change SOG to HDT --- lib/obp60task/PageNavigation.cpp | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp index 9b15f30..d22ea85 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -17,7 +17,7 @@ class PageNavigation : public Page // Values for buttons bool firstRun = true; // Detect the first page run int zoom = 15; // Default zoom level -bool showValues = false; // Show values COG, SOG, DBT in navigation map +bool showValues = false; // Show values HDT, SOG, DBT in navigation map public: PageNavigation(CommonData &common){ @@ -107,7 +107,7 @@ public: static double latitude = 0; static double longitude = 0; - static double courseOverGround = 0; + static double trueHeading = 0; static double speedOverGround = 0; static double depthBelowTransducer = 0; @@ -129,7 +129,7 @@ public: String svalue2 = formatValue(bvalue2, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places String unit2 = formatValue(bvalue2, *commonData).unit; // Unit of value - // Get boat values #3 COG + // Get boat values #3 HDT GwApi::BoatValue *bvalue3 = pageData.values[2]; // Second element in list (only one value by PageOneValue) String name3 = xdrDelete(bvalue3->getName()); // Value name name3 = name3.substring(0, 6); // String length limit for value name @@ -185,13 +185,13 @@ public: else{ longitude = value2old; } - // COG value (Course Over Ground) + // HDT value (Course Over Ground) if(valid3){ - courseOverGround = (value3 * 360) / (2 * PI); - value3old = courseOverGround; + trueHeading = (value3 * 360) / (2 * PI); + value3old = trueHeading; } else{ - courseOverGround = value3old; + trueHeading = value3old; } // SOG value (Speed Over Ground) if(valid4){ @@ -264,15 +264,15 @@ public: // Map orientation if(orientation == "North Direction"){ mapRot = 0; - symbolRot = courseOverGround; + symbolRot = trueHeading; } else if(orientation == "Travel Direction"){ - mapRot = courseOverGround; - symbolRot = courseOverGround; + mapRot = trueHeading; + symbolRot = trueHeading; } else{ mapRot = 0; - symbolRot = courseOverGround; + symbolRot = trueHeading; } // Load navigation map @@ -362,7 +362,7 @@ public: // Frame getdisplay().fillRect(0, 25 , 130, 65, commonData->fgcolor); // Black rect getdisplay().fillRect(2, 27 , 126, 61, commonData->bgcolor); // White rect - // COG + // HDT getdisplay().setCursor(10, 45); getdisplay().print(name3); getdisplay().setCursor(70, 45); @@ -401,7 +401,7 @@ PageDescription registerPageNavigation( "Navigation", // Page name createPage, // Action 0, // Number of bus values depends on selection in Web configuration - {"LAT","LON","COG","SOG","DBT"}, // Bus values we need in the page + {"LAT","LON","HDT","SOG","DBT"}, // Bus values we need in the page true // Show display header on/off ); From fe78fb434b76283a8478095809a66ef534350206 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Sat, 6 Dec 2025 17:51:43 +0100 Subject: [PATCH 069/121] Add HDM as fallback for HDT --- lib/obp60task/PageNavigation.cpp | 97 ++++++++++++++++++++++++-------- 1 file changed, 73 insertions(+), 24 deletions(-) diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp index d22ea85..ee6ea7e 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -104,10 +104,14 @@ public: static double value5old = 0; static String svalue5old = ""; static String unit5old = ""; + static double value6old = 0; + static String svalue6old = ""; + static String unit6old = ""; static double latitude = 0; static double longitude = 0; static double trueHeading = 0; + static double magneticHeading = 0; static double speedOverGround = 0; static double depthBelowTransducer = 0; @@ -138,7 +142,7 @@ public: String svalue3 = formatValue(bvalue3, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places String unit3 = formatValue(bvalue3, *commonData).unit; // Unit of value - // Get boat values #4 SOG + // Get boat values #4 HDM GwApi::BoatValue *bvalue4 = pageData.values[3]; // Second element in list (only one value by PageOneValue) String name4 = xdrDelete(bvalue4->getName()); // Value name name4 = name4.substring(0, 6); // String length limit for value name @@ -147,7 +151,7 @@ public: String svalue4 = formatValue(bvalue4, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places String unit4 = formatValue(bvalue4, *commonData).unit; // Unit of value - // Get boat values #5 DBT + // Get boat values #5 SOG GwApi::BoatValue *bvalue5 = pageData.values[4]; // Second element in list (only one value by PageOneValue) String name5 = xdrDelete(bvalue5->getName()); // Value name name5 = name5.substring(0, 6); // String length limit for value name @@ -156,6 +160,15 @@ public: String svalue5 = formatValue(bvalue5, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places String unit5 = formatValue(bvalue5, *commonData).unit; // Unit of value + // Get boat values #6 DBT + GwApi::BoatValue *bvalue6 = pageData.values[5]; // Second element in list (only one value by PageOneValue) + String name6 = xdrDelete(bvalue6->getName()); // Value name + name6 = name6.substring(0, 6); // String length limit for value name + double value6 = bvalue6->value; // Value as double in SI unit + bool valid6 = bvalue6->valid; // Valid information + String svalue6 = formatValue(bvalue6, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places + String unit6 = formatValue(bvalue6, *commonData).unit; // Unit of value + // Optical warning by limit violation (unused) if(String(flashLED) == "Limit Violation"){ setBlinkingLED(false); @@ -164,7 +177,7 @@ public: // Logging boat values if (bvalue1 == NULL) return PAGE_OK; // WTF why this statement? - LOG_DEBUG(GwLog::LOG,"Drawing at PageNavigation, %s: %f, %s: %f, %s: %f, %s: %f", name1.c_str(), value1, name2.c_str(), value2, name3.c_str(), value3, name4.c_str(), value4); + LOG_DEBUG(GwLog::LOG,"Drawing at PageNavigation, %s: %f, %s: %f, %s: %f, %s: %f, %s: %f, %s: %f", name1.c_str(), value1, name2.c_str(), value2, name3.c_str(), value3, name4.c_str(), value4, name5.c_str(), value5, name6.c_str(), value6); // Set variables //*********************************************************** @@ -185,7 +198,7 @@ public: else{ longitude = value2old; } - // HDT value (Course Over Ground) + // HDT value (True Heading, GPS) if(valid3){ trueHeading = (value3 * 360) / (2 * PI); value3old = trueHeading; @@ -193,21 +206,29 @@ public: else{ trueHeading = value3old; } - // SOG value (Speed Over Ground) + // HDM value (Magnetic Heading) if(valid4){ - speedOverGround = value4; + magneticHeading = value4; value4old = value4; } else{ speedOverGround = value4old; } - // DBT value (Depth Below Transducer) + // SOG value (Speed Over Ground) if(valid5){ - depthBelowTransducer = value5; + speedOverGround = value5; value5old = value5; } else{ - depthBelowTransducer = value5old; + speedOverGround = value5old; + } + // DBT value (Depth Below Transducer) + if(valid6){ + depthBelowTransducer = value6; + value6old = value6; + } + else{ + depthBelowTransducer = value6old; } // Prepare config values for URL @@ -264,15 +285,34 @@ public: // Map orientation if(orientation == "North Direction"){ mapRot = 0; - symbolRot = trueHeading; + // If true heading available then use HDT oterwise HDM + if(valid3 == true){ + symbolRot = trueHeading; + } + else{ + symbolRot = magneticHeading; + } } else if(orientation == "Travel Direction"){ - mapRot = trueHeading; - symbolRot = trueHeading; + // If true heading available then use HDT oterwise HDM + if(valid3 == true){ + mapRot = trueHeading; + symbolRot = trueHeading; + } + else{ + mapRot = magneticHeading; + symbolRot = magneticHeading; + } } else{ mapRot = 0; - symbolRot = trueHeading; + // If true heading available then use HDT oterwise HDM + if(valid3 == true){ + symbolRot = trueHeading; + } + else{ + symbolRot = magneticHeading; + } } // Load navigation map @@ -357,26 +397,35 @@ public: getdisplay().fillRect(357, 27 , 41, 21, commonData->bgcolor); // White rect getdisplay().setCursor(364, 45); getdisplay().print(zoom); - + // If true heading available then use HDT oterwise HDM if(showValues == true){ // Frame getdisplay().fillRect(0, 25 , 130, 65, commonData->fgcolor); // Black rect getdisplay().fillRect(2, 27 , 126, 61, commonData->bgcolor); // White rect - // HDT - getdisplay().setCursor(10, 45); - getdisplay().print(name3); - getdisplay().setCursor(70, 45); - getdisplay().print(svalue3); + if(valid3 == true){ + // HDT + getdisplay().setCursor(10, 45); + getdisplay().print(name3); + getdisplay().setCursor(70, 45); + getdisplay().print(svalue3); + } + else{ + // HDM + getdisplay().setCursor(10, 45); + getdisplay().print(name4); + getdisplay().setCursor(70, 45); + getdisplay().print(svalue4); + } // SOG getdisplay().setCursor(10, 65); - getdisplay().print(name4); + getdisplay().print(name5); getdisplay().setCursor(70, 65); - getdisplay().print(svalue4); + getdisplay().print(svalue5); // DBT getdisplay().setCursor(10, 85); - getdisplay().print(name5); + getdisplay().print(name6); getdisplay().setCursor(70, 85); - getdisplay().print(svalue5); + getdisplay().print(svalue6); } // Set botton labels @@ -401,7 +450,7 @@ PageDescription registerPageNavigation( "Navigation", // Page name createPage, // Action 0, // Number of bus values depends on selection in Web configuration - {"LAT","LON","HDT","SOG","DBT"}, // Bus values we need in the page + {"LAT","LON","HDT","HDM","SOG","DBT"}, // Bus values we need in the page true // Show display header on/off ); From 6edf8479580d3c613ac5e9cce1ae43d70f4b8f13 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Sat, 6 Dec 2025 18:14:55 +0100 Subject: [PATCH 070/121] Fix for HDM --- lib/obp60task/PageNavigation.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp index ee6ea7e..59dbc34 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -208,8 +208,8 @@ public: } // HDM value (Magnetic Heading) if(valid4){ - magneticHeading = value4; - value4old = value4; + magneticHeading = (value4 * 360) / (2 * PI); + value4old = magneticHeading; } else{ speedOverGround = value4old; From 69367b91d7311090a244c8bc8c8ea6e3ad5a58d8 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 12 Dec 2025 13:09:06 +0100 Subject: [PATCH 071/121] Add showValues as config parameter --- lib/obp60task/OBP60Extensions.cpp | 26 +++++++ lib/obp60task/OBP60Extensions.h | 2 + lib/obp60task/PageNavigation.cpp | 11 ++- lib/obp60task/config.json | 76 ++++++++++++------ lib/obp60task/config_obp40.json | 124 ++++++++++++++++++------------ 5 files changed, 165 insertions(+), 74 deletions(-) diff --git a/lib/obp60task/OBP60Extensions.cpp b/lib/obp60task/OBP60Extensions.cpp index e45c90b..b8119f0 100644 --- a/lib/obp60task/OBP60Extensions.cpp +++ b/lib/obp60task/OBP60Extensions.cpp @@ -905,4 +905,30 @@ void doImageRequest(GwApi *api, int *pageno, const PageStruct pages[MAX_PAGE_NUM imageBuffer.clear(); } + + +// Calculate the distance between two Geo coordinates +double distanceBetweenCoordinates(double lat1, double lon1, double lat2, double lon2) { + // Grad → Radiant + double lat1Rad = lat1 * DEG_TO_RAD; + double lon1Rad = lon1 * DEG_TO_RAD; + double lat2Rad = lat2 * DEG_TO_RAD; + double lon2Rad = lon2 * DEG_TO_RAD; + + // Differenzen + double dLat = lat2Rad - lat1Rad; + double dLon = lon2Rad - lon1Rad; + + // Haversine-Formel + double a = sin(dLat / 2.0) * sin(dLat / 2.0) + + cos(lat1Rad) * cos(lat2Rad) * + sin(dLon / 2.0) * sin(dLon / 2.0); + + double c = 2.0 * atan2(sqrt(a), sqrt(1.0 - a)); + + // Abstand in Metern + return double(EARTH_RADIUS) * c; +} + + #endif diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index 33b9218..c658797 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -7,6 +7,7 @@ #include "Graphics.h" #include // E-paper lib V2 #include // I2C FRAM +#include #ifdef BOARD_OBP40S3 #include "esp_vfs_fat.h" @@ -31,6 +32,7 @@ #define FRAM_BAROGRAPH_END 0x13FF #define PI 3.1415926535897932384626433832795 +#define EARTH_RADIUS 6371000.0 extern Adafruit_FRAM_I2C fram; extern bool hasFRAM; diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp index 59dbc34..d83cd17 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -72,10 +72,13 @@ public: bool grid = config->getBool(config->grid); String orientation = config->getString(config->orientation); int refreshDistance = config->getInt(config->refreshDistance); + bool showValuesMap = config->getBool(config->showvalues); + bool ownHeading = config->getBool(config->ownheading); if(firstRun == true){ - zoom = zoomLevel; // Over write zoom level with setup value - firstRun = false; // Restet variable + zoom = zoomLevel; // Over write zoom level with setup value + showValues = showValuesMap; // Over write showValues with setup value + firstRun = false; // Restet variable } // Local variables @@ -109,7 +112,9 @@ public: static String unit6old = ""; static double latitude = 0; + static double latitudeold = 0; static double longitude = 0; + static double longitudeold = 0; static double trueHeading = 0; static double magneticHeading = 0; static double speedOverGround = 0; @@ -185,6 +190,7 @@ public: // Latitude if(valid1){ latitude = value1; + latitudeold = value1; value3old = value1; } else{ @@ -193,6 +199,7 @@ public: // Longitude if(valid2){ longitude = value2; + longitudeold = value2; value2old = value2; } else{ diff --git a/lib/obp60task/config.json b/lib/obp60task/config.json index 088001a..a7e4ced 100644 --- a/lib/obp60task/config.json +++ b/lib/obp60task/config.json @@ -934,11 +934,14 @@ "type": "string", "default": "192.168.15.10", "check": "checkIpAddress", - "description": "IP address only for local map service e.g. 192.168.15.10\nor an MDNS name like Raspi.local", + "description": "IP address for local map service e.g. 192.168.15.10\nor an MDNS name like Raspi.local", "category": "OBP60 Navigation", "capabilities": { "obp60":"true" - } + }, + "condition": [ + { "mapsource": ["Local Service"] } + ] }, { "name": "localPort", @@ -947,10 +950,13 @@ "default": "8080", "check":"checkPort", "description": "TCP port for local map server", - "category": "TCP client", + "category": "OBP60 Navigation", "capabilities": { "obp60":"true" - } + }, + "condition": [ + { "mapsource": ["Local Service"] } + ] }, { "name": "maptype", @@ -971,28 +977,31 @@ } }, { - "name": "zoomlevel", - "label": "Default Zool Level", + "name": "refreshDistance", + "label": "Refresh Distance [m]", "type": "number", "default": "15", "check": "checkMinMax", - "min": 7, - "max": 17, - "description": "Zoom level for map [7..17]; 15 = default", + "min": 1, + "max": 50, + "description": "Refresh distance between updates [1..50 m], 15 m = default", "category": "OBP60 Navigation", "capabilities": { "obp60":"true" } }, { - "name": "grid", - "label": "Show Grid", - "type": "boolean", - "default": "false", - "description": "Show the grid for latutude and longitude", + "name": "zoomlevel", + "label": "Default Zoom Level", + "type": "number", + "default": "15", + "check": "checkMinMax", + "min": 7, + "max": 17, + "description": "Start zoom level for map [7..17]; 15 = default", "category": "OBP60 Navigation", "capabilities": { - "obp60": "true" + "obp60":"true" } }, { @@ -1011,17 +1020,36 @@ } }, { - "name": "refreshDistance", - "label": "Refresh Distance [m]", - "type": "number", - "default": "15", - "check": "checkMinMax", - "min": 1, - "max": 50, - "description": "Refresh distance between updates [1..50 m], 15 m = default", + "name": "grid", + "label": "Show Grid", + "type": "boolean", + "default": "false", + "description": "Show the grid for latutude and longitude", "category": "OBP60 Navigation", "capabilities": { - "obp60":"true" + "obp60": "true" + } + }, + { + "name": "showvalues", + "label": "Show Values", + "type": "boolean", + "default": "false", + "description": "Show boat data values in the left upper map corner", + "category": "OBP60 Navigation", + "capabilities": { + "obp60": "true" + } + }, + { + "name": "ownheading", + "label": "Alternativ Heading", + "type": "boolean", + "default": "false", + "description": "Calculating an alternative travel direction for\na better and calmer map orientation", + "category": "OBP60 Navigation", + "capabilities": { + "obp60": "true" } }, { diff --git a/lib/obp60task/config_obp40.json b/lib/obp60task/config_obp40.json index eaca892..91985e3 100644 --- a/lib/obp60task/config_obp40.json +++ b/lib/obp60task/config_obp40.json @@ -934,7 +934,7 @@ "OBP Service", "Local Service" ], - "category": "OBP60 Navigation", + "category": "OBP40 Navigation", "capabilities": { "obp40":"true" } @@ -945,11 +945,14 @@ "type": "string", "default": "192.168.15.10", "check": "checkIpAddress", - "description": "IP address only for local map service e.g. 192.168.15.10\nor an MDNS name like Raspi.local", - "category": "OBP60 Navigation", + "description": "IP address for local map service e.g. 192.168.15.10\nor an MDNS name like Raspi.local", + "category": "OBP40 Navigation", "capabilities": { "obp40":"true" - } + }, + "condition": [ + { "mapsource": ["Local Service"] } + ] }, { "name": "localPort", @@ -958,10 +961,13 @@ "default": "8080", "check":"checkPort", "description": "TCP port for local map server", - "category": "TCP client", + "category": "OBP40 Navigation", "capabilities": { "obp40":"true" - } + }, + "condition": [ + { "mapsource": ["Local Service"] } + ] }, { "name": "maptype", @@ -976,47 +982,7 @@ "Stadimaps Toner", "Free Nautical Chart" ], - "category": "OBP60 Navigation", - "capabilities": { - "obp40":"true" - } - }, - { - "name": "zoomlevel", - "label": "Default Zool Level", - "type": "number", - "default": "15", - "check": "checkMinMax", - "min": 7, - "max": 17, - "description": "Zoom level for map [7..17]; 15 = default", - "category": "OBP60 Navigation", - "capabilities": { - "obp40":"true" - } - }, - { - "name": "grid", - "label": "Show Grid", - "type": "boolean", - "default": "false", - "description": "Show the grid for latutude and longitude", - "category": "OBP60 Navigation", - "capabilities": { - "obp40": "true" - } - }, - { - "name": "orientation", - "label": "Map Orientation", - "type": "list", - "default": "North Dirirection", - "description": "Map orientation for navigation", - "list": [ - "North Direction", - "Travel Direction" - ], - "category": "OBP60 Navigation", + "category": "OBP40 Navigation", "capabilities": { "obp40":"true" } @@ -1030,11 +996,73 @@ "min": 1, "max": 50, "description": "Refresh distance between updates [1..50 m], 15 m = default", - "category": "OBP60 Navigation", + "category": "OBP40 Navigation", "capabilities": { "obp40":"true" } }, + { + "name": "zoomlevel", + "label": "Default Zoom Level", + "type": "number", + "default": "15", + "check": "checkMinMax", + "min": 7, + "max": 17, + "description": "Start zoom level for map [7..17]; 15 = default", + "category": "OBP40 Navigation", + "capabilities": { + "obp40":"true" + } + }, + { + "name": "orientation", + "label": "Map Orientation", + "type": "list", + "default": "North Dirirection", + "description": "Map orientation for navigation", + "list": [ + "North Direction", + "Travel Direction" + ], + "category": "OBP40 Navigation", + "capabilities": { + "obp40":"true" + } + }, + { + "name": "grid", + "label": "Show Grid", + "type": "boolean", + "default": "false", + "description": "Show the grid for latutude and longitude", + "category": "OBP40 Navigation", + "capabilities": { + "obp40": "true" + } + }, + { + "name": "showvalues", + "label": "Show Values", + "type": "boolean", + "default": "false", + "description": "Show boat data values in the left upper map corner", + "category": "OBP40 Navigation", + "capabilities": { + "obp40": "true" + } + }, + { + "name": "ownheading", + "label": "Alternativ Heading", + "type": "boolean", + "default": "false", + "description": "Calculating an alternative travel direction for\na better and calmer map orientation", + "category": "OBP40 Navigation", + "capabilities": { + "obp40": "true" + } + }, { "name": "display", "label": "Display Mode", From 3ce1e31e64a6baf6ef732f402c185fbf44491c9f Mon Sep 17 00:00:00 2001 From: Ulrich Meine Date: Sat, 13 Dec 2025 17:59:16 +0100 Subject: [PATCH 072/121] Initial change to history buffers taking any boat value --- lib/obp60task/OBPDataOperations.cpp | 189 +++++++++++++--------------- lib/obp60task/OBPDataOperations.h | 61 ++++----- lib/obp60task/PageWindPlot.cpp | 22 ++-- lib/obp60task/Pagedata.h | 2 +- lib/obp60task/obp60task.cpp | 14 +-- 5 files changed, 129 insertions(+), 159 deletions(-) diff --git a/lib/obp60task/OBPDataOperations.cpp b/lib/obp60task/OBPDataOperations.cpp index c68ef53..3eadde5 100644 --- a/lib/obp60task/OBPDataOperations.cpp +++ b/lib/obp60task/OBPDataOperations.cpp @@ -1,46 +1,81 @@ #include "OBPDataOperations.h" #include "BoatDataCalibration.h" // Functions lib for data instance calibration #include +#include +#include +#include // --- Class HstryBuf --------------- -// Init history buffers for selected boat data -void HstryBuf::init(BoatValueList* boatValues, GwLog *log) { +HstryBuf::HstryBuf(const String& name, int size, GwLog* log, BoatValueList* boatValues) + : logger(log), boatDataName(name) { + hstry.resize(size); + boatValue = boatValues->findValueOrCreate(name); +} - logger = log; +void HstryBuf::init(const String& format, int updFreq, int mltplr, double minVal, double maxVal) { + hstry.setMetaData(boatDataName, format, updFreq, mltplr, minVal, maxVal); + hstryMin = minVal; + hstryMax = maxVal; + if (!boatValue->valid) { + boatValue->setFormat(format); + boatValue->value = std::numeric_limits::max(); + } +} +void HstryBuf::add(double value) { + if (value >= hstryMin && value <= hstryMax) { + hstry.add(value); + } +} + +void HstryBuf::handle(bool useSimuData) { + GwApi::BoatValue *calBVal; + + if (boatValue->valid) { + calBVal = new GwApi::BoatValue(boatDataName.c_str()); + calBVal->setFormat(boatValue->getFormat()); + calBVal->value = boatValue->value; + calBVal->valid = boatValue->valid; + calibrationData.calibrateInstance(calBVal, logger); + add(calBVal->value); + delete calBVal; + calBVal = nullptr; + } else if (useSimuData) { + double simValue = hstry.getLast(); + if (boatDataName == "TWD" || boatDataName == "AWD") { + simValue += static_cast(random(-349, 349) / 1000.0); + simValue = WindUtils::to2PI(simValue); + } else if (boatDataName == "TWS" || boatDataName == "AWS") { + simValue += static_cast(random(-5000, 5000) / 1000.0); + simValue = constrain(simValue, 0, 40); + } + add(simValue); + } +} + +// --- Class HstryManager --------------- +HstryManager::HstryManager(int size, GwLog* log, BoatValueList* boatValues) { + // Create history buffers for each boat data type + hstryBufs["TWD"] = std::unique_ptr(new HstryBuf("TWD", size, log, boatValues)); + hstryBufs["TWS"] = std::unique_ptr(new HstryBuf("TWS", size, log, boatValues)); + hstryBufs["AWD"] = std::unique_ptr(new HstryBuf("AWD", size, log, boatValues)); + hstryBufs["AWS"] = std::unique_ptr(new HstryBuf("AWS", size, log, boatValues)); + + // Initialize metadata for each buffer int hstryUpdFreq = 1000; // Update frequency for history buffers in ms int mltplr = 1000; // Multiplier which transforms original value into buffer type format double hstryMinVal = 0; // Minimum value for these history buffers - twdHstryMax = 2 * M_PI; // Max value for wind direction (TWD, AWD) in rad [0...2*PI] - twsHstryMax = 65; // Max value for wind speed (TWS, AWS) in m/s [0..65] (limit due to type capacity of buffer - shifted by ) - awdHstryMax = twdHstryMax; - awsHstryMax = twsHstryMax; - twdHstryMin = hstryMinVal; - twsHstryMin = hstryMinVal; - awdHstryMin = hstryMinVal; - awsHstryMin = hstryMinVal; - const double DBL_MAX = std::numeric_limits::max(); + double courseMax = 2 * M_PI; + double speedMax = 65; - // Initialize history buffers with meta data mltplr = 10000; // Store 4 decimals for course data - hstryBufList.twdHstry->setMetaData("TWD", "formatCourse", hstryUpdFreq, mltplr, hstryMinVal, twdHstryMax); - hstryBufList.awdHstry->setMetaData("AWD", "formatCourse", hstryUpdFreq, mltplr, hstryMinVal, twdHstryMax); + hstryBufs["TWD"]->init("formatCourse", hstryUpdFreq, mltplr, hstryMinVal, courseMax); + hstryBufs["AWD"]->init("formatCourse", hstryUpdFreq, mltplr, hstryMinVal, courseMax); + mltplr = 1000; // Store 3 decimals for windspeed data - hstryBufList.twsHstry->setMetaData("TWS", "formatKnots", hstryUpdFreq, mltplr, hstryMinVal, twsHstryMax); - hstryBufList.awsHstry->setMetaData("AWS", "formatKnots", hstryUpdFreq, mltplr, hstryMinVal, twsHstryMax); - - // create boat values for history data types, if they don't exist yet - twdBVal = boatValues->findValueOrCreate(hstryBufList.twdHstry->getName()); - twsBVal = boatValues->findValueOrCreate(hstryBufList.twsHstry->getName()); - twaBVal = boatValues->findValueOrCreate("TWA"); - awdBVal = boatValues->findValueOrCreate(hstryBufList.awdHstry->getName()); - awsBVal = boatValues->findValueOrCreate(hstryBufList.awsHstry->getName()); - - if (!awdBVal->valid) { // AWD usually does not exist - awdBVal->setFormat(hstryBufList.awdHstry->getFormat()); - awdBVal->value = DBL_MAX; - } + hstryBufs["TWS"]->init("formatKnots", hstryUpdFreq, mltplr, hstryMinVal, speedMax); + hstryBufs["AWS"]->init("formatKnots", hstryUpdFreq, mltplr, hstryMinVal, speedMax); // collect boat values for true wind calculation awaBVal = boatValues->findValueOrCreate("AWA"); @@ -49,106 +84,58 @@ void HstryBuf::init(BoatValueList* boatValues, GwLog *log) { varBVal = boatValues->findValueOrCreate("VAR"); cogBVal = boatValues->findValueOrCreate("COG"); sogBVal = boatValues->findValueOrCreate("SOG"); + awdBVal = boatValues->findValueOrCreate("AWD"); } // Handle history buffers for TWD, TWS, AWD, AWS -//void HstryBuf::handleHstryBuf(GwApi* api, BoatValueList* boatValues, bool useSimuData) { -void HstryBuf::handleHstryBuf(bool useSimuData) { +void HstryManager::handleHstryBufs(bool useSimuData) { - static double twd, tws, awd, aws, hdt = 20; //initial value only relevant if we use simulation data + static double hdt = 20; //initial value only relevant if we use simulation data GwApi::BoatValue *calBVal; // temp variable just for data calibration -> we don't want to calibrate the original data here - LOG_DEBUG(GwLog::DEBUG,"obp60task handleHstryBuf: TWD_isValid? %d, twdBVal: %.1f, twaBVal: %.1f, twsBVal: %.1f", twdBVal->valid, twdBVal->value * RAD_TO_DEG, - twaBVal->value * RAD_TO_DEG, twsBVal->value * 3.6 / 1.852); - - if (twdBVal->valid) { -// if (!useSimuData) { - calBVal = new GwApi::BoatValue("TWD"); // temporary solution for calibration of history buffer values - calBVal->setFormat(twdBVal->getFormat()); - calBVal->value = twdBVal->value; - calBVal->valid = twdBVal->valid; - calibrationData.calibrateInstance(calBVal, logger); // Check if boat data value is to be calibrated - twd = calBVal->value; - if (twd >= twdHstryMin && twd <= twdHstryMax) { - hstryBufList.twdHstry->add(twd); - LOG_DEBUG(GwLog::DEBUG,"obp60task handleHstryBuf: calBVal.value %.2f, twd: %.2f, twdHstryMin: %.1f, twdHstryMax: %.2f", calBVal->value, twd, twdHstryMin, twdHstryMax); - } - delete calBVal; - calBVal = nullptr; - } else if (useSimuData) { -// } else { - twd += random(-20, 20); - twd += static_cast(random(-349, 349) / 1000.0); // add up to +/- 20 degree in RAD - twd = WindUtils::to2PI(twd); - hstryBufList.twdHstry->add(twd); - } - - if (twsBVal->valid) { - calBVal = new GwApi::BoatValue("TWS"); // temporary solution for calibration of history buffer values - calBVal->setFormat(twsBVal->getFormat()); - calBVal->value = twsBVal->value; - calBVal->valid = twsBVal->valid; - calibrationData.calibrateInstance(calBVal, logger); // Check if boat data value is to be calibrated - tws = calBVal->value; - if (tws >= twsHstryMin && tws <= twsHstryMax) { - hstryBufList.twsHstry->add(tws); - } - delete calBVal; - calBVal = nullptr; - } else if (useSimuData) { - // tws += random(-5000, 5000); // TWS value in m/s; expands to 3 decimals - tws += static_cast(random(-5000, 5000) / 1000.0); // add up to +/- 5 m/s TWS speed - tws = constrain(tws, 0, 40); // Limit TWS to [0..40] m/s - hstryBufList.twsHstry->add(tws); + // Handle all registered history buffers + for (auto& pair : hstryBufs) { + auto& buf = pair.second; + buf->handle(useSimuData); } + // Special handling for AWD which is calculated if (awaBVal->valid) { if (hdtBVal->valid) { hdt = hdtBVal->value; // Use HDT if available } else { hdt = WindUtils::calcHDT(&hdmBVal->value, &varBVal->value, &cogBVal->value, &sogBVal->value); } - + double awd; awd = awaBVal->value + hdt; awd = WindUtils::to2PI(awd); calBVal = new GwApi::BoatValue("AWD"); // temporary solution for calibration of history buffer values calBVal->value = awd; calBVal->setFormat(awdBVal->getFormat()); calBVal->valid = true; - calibrationData.calibrateInstance(calBVal, logger); // Check if boat data value is to be calibrated + // We don't have a logger here, so we pass nullptr. This should be improved. + calibrationData.calibrateInstance(calBVal, nullptr); // Check if boat data value is to be calibrated awdBVal->value = calBVal->value; awdBVal->valid = true; - awd = calBVal->value; - if (awd >= awdHstryMin && awd <= awdHstryMax) { - hstryBufList.awdHstry->add(awd); + // Find the AWD buffer and add the value. + auto it = hstryBufs.find("AWD"); + if (it != hstryBufs.end()) { + it->second->add(calBVal->value); } + delete calBVal; calBVal = nullptr; } else if (useSimuData) { - awd += static_cast(random(-349, 349) / 1000.0); // add up to +/- 20 degree in RAD - awd = WindUtils::to2PI(awd); - hstryBufList.awdHstry->add(awd); + // Simulation for AWD is handled inside HstryBuf::handle } - - if (awsBVal->valid) { - calBVal = new GwApi::BoatValue("AWS"); // temporary solution for calibration of history buffer values - calBVal->setFormat(awsBVal->getFormat()); - calBVal->value = awsBVal->value; - calBVal->valid = awsBVal->valid; - calibrationData.calibrateInstance(calBVal, logger); // Check if boat data value is to be calibrated - aws = calBVal->value; - if (aws >= awsHstryMin && aws <= awsHstryMax) { - hstryBufList.awsHstry->add(aws); - } - delete calBVal; - calBVal = nullptr; - } else if (useSimuData) { - aws += static_cast(random(-5000, 5000) / 1000.0); // add up to +/- 5 m/s TWS speed - aws = constrain(aws, 0, 40); // Limit TWS to [0..40] m/s - hstryBufList.awsHstry->add(aws); +} + +RingBuffer* HstryManager::getBuffer(const String& name) { + auto it = hstryBufs.find(name); + if (it != hstryBufs.end()) { + return &it->second->hstry; } - LOG_DEBUG(GwLog::DEBUG,"obp60task handleHstryBuf-End: Buffer twdHstry: %.3f, twsHstry: %.3f, awdHstry: %.3f, awsHstry: %.3f", hstryBufList.twdHstry->getLast(), hstryBufList.twsHstry->getLast(), - hstryBufList.awdHstry->getLast(),hstryBufList.awsHstry->getLast()); + return nullptr; } // --- Class HstryBuf --------------- diff --git a/lib/obp60task/OBPDataOperations.h b/lib/obp60task/OBPDataOperations.h index 8422894..de33156 100644 --- a/lib/obp60task/OBPDataOperations.h +++ b/lib/obp60task/OBPDataOperations.h @@ -2,51 +2,38 @@ #pragma once #include "OBPRingBuffer.h" #include "obp60task.h" - -typedef struct { - RingBuffer* twdHstry; - RingBuffer* twsHstry; - RingBuffer* awdHstry; - RingBuffer* awsHstry; -} tBoatHstryData; // Holds pointers to all history buffers for boat data +#include +#include +#include class HstryBuf { private: GwLog *logger; + RingBuffer hstry; // Circular buffer to store history values + String boatDataName; + double hstryMin; + double hstryMax; + GwApi::BoatValue *boatValue; - RingBuffer twdHstry; // Circular buffer to store true wind direction values - RingBuffer twsHstry; // Circular buffer to store true wind speed values (TWS) - RingBuffer awdHstry; // Circular buffer to store apparent wind direction values - RingBuffer awsHstry; // Circular buffer to store apparent xwind speed values (AWS) - double twdHstryMin; // Min value for wind direction (TWD) in history buffer - double twdHstryMax; // Max value for wind direction (TWD) in history buffer - double twsHstryMin; - double twsHstryMax; - double awdHstryMin; - double awdHstryMax; - double awsHstryMin; - double awsHstryMax; - - // boat values for buffers and for true wind calculation - GwApi::BoatValue *twdBVal, *twsBVal, *twaBVal, *awdBVal, *awsBVal; - GwApi::BoatValue *awaBVal, *hdtBVal, *hdmBVal, *varBVal, *cogBVal, *sogBVal; + friend class HstryManager; + void handleHistory(bool useSimuData); public: - tBoatHstryData hstryBufList; + HstryBuf(const String& name, int size, GwLog* log, BoatValueList* boatValues); + void init(const String& format, int updFreq, int mltplr, double minVal, double maxVal); + void add(double value); + void handle(bool useSimuData); +}; - HstryBuf(){ - hstryBufList = {&twdHstry, &twsHstry, &awdHstry, &awsHstry}; // Generate history buffers of zero size - }; - - HstryBuf(int size) { - hstryBufList = {&twdHstry, &twsHstry, &awdHstry, &awsHstry}; - hstryBufList.twdHstry->resize(size); // store xWD values for /60 minutes history - hstryBufList.twsHstry->resize(size); - hstryBufList.awdHstry->resize(size); - hstryBufList.awsHstry->resize(size); - }; - void init(BoatValueList* boatValues, GwLog *log); - void handleHstryBuf(bool useSimuData); +class HstryManager { +private: + std::map> hstryBufs; + // boat values for true wind calculation + GwApi::BoatValue *awaBVal, *hdtBVal, *hdmBVal, *varBVal, *cogBVal, *sogBVal, *awdBVal; +public: + HstryManager(int size, GwLog* log, BoatValueList* boatValues); + void handleHstryBufs(bool useSimuData); + RingBuffer* getBuffer(const String& name); }; class WindUtils { diff --git a/lib/obp60task/PageWindPlot.cpp b/lib/obp60task/PageWindPlot.cpp index 0b4e884..a30c0c8 100644 --- a/lib/obp60task/PageWindPlot.cpp +++ b/lib/obp60task/PageWindPlot.cpp @@ -2,6 +2,7 @@ #include "Pagedata.h" #include "OBP60Extensions.h" +#include "OBPDataOperations.h" #include "OBPcharts.h" // **************************************************************** @@ -163,12 +164,9 @@ public: if (showTruW != oldShowTruW) { if (!twdFlChart) { // Create true wind charts if they don't exist - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Creating true wind charts"); - auto* twdHstry = pageData.boatHstry->hstryBufList.twdHstry; - auto* twsHstry = pageData.boatHstry->hstryBufList.twsHstry; - // LOG_DEBUG(GwLog::DEBUG,"History Buffer addresses PageWindPlot: twdBuf: %p, twsBuf: %p", (void*)pageData.boatHstry->hstryBufList.twdHstry, - // (void*)pageData.boatHstry->hstryBufList.twsHstry); - + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Creating true wind charts"); + auto* twdHstry = pageData.hstryManager->getBuffer("TWD"); + auto* twsHstry = pageData.hstryManager->getBuffer("TWS"); twdFlChart = std::unique_ptr>(new Chart(*twdHstry, 1, 0, dfltRngWd, *commonData, useSimuData)); twsFlChart = std::unique_ptr>(new Chart(*twsHstry, 0, 0, dfltRngWs, *commonData, useSimuData)); twdHfChart = std::unique_ptr>(new Chart(*twdHstry, 1, 1, dfltRngWd, *commonData, useSimuData)); @@ -180,8 +178,8 @@ public: if (!awdFlChart) { // Create apparent wind charts if they don't exist LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Creating apparent wind charts"); - auto* awdHstry = pageData.boatHstry->hstryBufList.awdHstry; - auto* awsHstry = pageData.boatHstry->hstryBufList.awsHstry; + auto* awdHstry = pageData.hstryManager->getBuffer("AWD"); + auto* awsHstry = pageData.hstryManager->getBuffer("AWS"); awdFlChart = std::unique_ptr>(new Chart(*awdHstry, 1, 0, dfltRngWd, *commonData, useSimuData)); awsFlChart = std::unique_ptr>(new Chart(*awsHstry, 0, 0, dfltRngWs, *commonData, useSimuData)); @@ -191,15 +189,15 @@ public: // Switch active charts based on showTruW if (showTruW) { - wdHstry = pageData.boatHstry->hstryBufList.twdHstry; - wsHstry = pageData.boatHstry->hstryBufList.twsHstry; + wdHstry = pageData.hstryManager->getBuffer("TWD"); + wsHstry = pageData.hstryManager->getBuffer("TWS"); wdFlChart = twdFlChart.get(); wsFlChart = twsFlChart.get(); wdHfChart = twdHfChart.get(); wsHfChart = twsHfChart.get(); } else { - wdHstry = pageData.boatHstry->hstryBufList.awdHstry; - wsHstry = pageData.boatHstry->hstryBufList.awsHstry; + wdHstry = pageData.hstryManager->getBuffer("AWD"); + wsHstry = pageData.hstryManager->getBuffer("AWS"); wdFlChart = awdFlChart.get(); wsFlChart = awsFlChart.get(); wdHfChart = awdHfChart.get(); diff --git a/lib/obp60task/Pagedata.h b/lib/obp60task/Pagedata.h index 9c515b4..f40d187 100644 --- a/lib/obp60task/Pagedata.h +++ b/lib/obp60task/Pagedata.h @@ -16,7 +16,7 @@ typedef struct{ uint8_t pageNumber; // page number in sequence of visible pages //the values will always contain the user defined values first ValueList values; - HstryBuf* boatHstry; + HstryManager* hstryManager; } PageData; // Sensor data structure (only for extended sensors, not for NMEA bus sensors) diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index 17e586a..0626a90 100644 --- a/lib/obp60task/obp60task.cpp +++ b/lib/obp60task/obp60task.cpp @@ -433,7 +433,7 @@ void OBP60Task(GwApi *api){ int lastPage=pageNumber; BoatValueList boatValues; //all the boat values for the api query - HstryBuf hstryBufList(1920); // Create ring buffers for history storage of some boat data (1920 seconds = 32 minutes) + HstryManager hstryManager(1920, logger, &boatValues); // Create and manage history buffers WindUtils trueWind(&boatValues); // Create helper object for true wind calculation //commonData.distanceformat=config->getString(xxx); //add all necessary data to common data @@ -477,21 +477,19 @@ void OBP60Task(GwApi *api){ LOG_DEBUG(GwLog::DEBUG,"added fixed value %s to page %d",value->getName().c_str(),i); pages[i].parameters.values.push_back(value); } - // Add boat history data to page parameters - pages[i].parameters.boatHstry = &hstryBufList; + // Add history manager to page parameters + pages[i].parameters.hstryManager = &hstryManager; } // add out of band system page (always available) Page *syspage = allPages.pages[0]->creator(commonData); - // Read all calibration data settings from config - calibrationData.readConfig(config, logger); - // Check user settings for true wind calculation bool calcTrueWnds = api->getConfig()->getBool(api->getConfig()->calcTrueWnds, false); bool useSimuData = api->getConfig()->getBool(api->getConfig()->useSimuData, false); // Initialize history buffer for certain boat data - hstryBufList.init(&boatValues, logger); + // Read all calibration data settings from config + calibrationData.readConfig(config, logger); // Display screenshot handler for HTTP request // http://192.168.15.1/api/user/OBP60Task/screenshot @@ -809,7 +807,7 @@ void OBP60Task(GwApi *api){ trueWind.addTrueWind(api, &boatValues, logger); } // Handle history buffers for certain boat data for windplot page and other usage - hstryBufList.handleHstryBuf(useSimuData); + hstryManager.handleHstryBufs(useSimuData); // Clear display // getdisplay().fillRect(0, 0, getdisplay().width(), getdisplay().height(), commonData.bgcolor); From b54acbae4224e27a3a8c05227070dba59ca6041c Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Sat, 13 Dec 2025 21:08:39 +0100 Subject: [PATCH 073/121] Backup actual firmware --- lib/obp60task/OBP60Extensions.cpp | 4 +--- lib/obp60task/OBP60Hardware.h | 4 ++++ lib/obp60task/PageNavigation.cpp | 30 ++++++++++++++++++++++++++++-- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/lib/obp60task/OBP60Extensions.cpp b/lib/obp60task/OBP60Extensions.cpp index b8119f0..b296602 100644 --- a/lib/obp60task/OBP60Extensions.cpp +++ b/lib/obp60task/OBP60Extensions.cpp @@ -27,9 +27,7 @@ #include "fonts/IBM8x8px.h" // E-Ink Display -#define GxEPD_WIDTH 400 // Display width -#define GxEPD_HEIGHT 300 // Display height - +// Definition for e-paper width an height refer OBP60Hardware.h #ifdef DISPLAY_GDEW042T2 // Set display type and SPI pins for display GxEPD2_BW display(GxEPD2_420(OBP_SPI_CS, OBP_SPI_DC, OBP_SPI_RST, OBP_SPI_BUSY)); // GDEW042T2 400x300, UC8176 (IL0398) diff --git a/lib/obp60task/OBP60Hardware.h b/lib/obp60task/OBP60Hardware.h index ac366c8..1787f83 100644 --- a/lib/obp60task/OBP60Hardware.h +++ b/lib/obp60task/OBP60Hardware.h @@ -42,6 +42,8 @@ #define OBP_SPI_DIN 48 #define SHOW_TIME 6000 // Show time in [ms] for logo and WiFi QR code #define FULL_REFRESH_TIME 600 // Refresh cycle time in [s][600...3600] for full display update (very important healcy function) + #define GxEPD_WIDTH 400 // Display width + #define GxEPD_HEIGHT 300 // Display height // GPS (NEO-6M, NEO-M8N, ATGM336H) #define OBP_GPS_RX 2 @@ -119,6 +121,8 @@ #define OBP_SPI_DIN 11 #define SHOW_TIME 6000 // Show time in [ms] for logo and WiFi QR code #define FULL_REFRESH_TIME 600 // Refresh cycle time in [s][600...3600] for full display update (very important healcy function) + #define GxEPD_WIDTH 400 // Display width + #define GxEPD_HEIGHT 300 // Display height // SPI SD-Card #define SD_SPI_CS GPIO_NUM_10 #define SD_SPI_MOSI GPIO_NUM_40 diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp index d83cd17..be91519 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -19,6 +19,9 @@ bool firstRun = true; // Detect the first page run int zoom = 15; // Default zoom level bool showValues = false; // Show values HDT, SOG, DBT in navigation map +// Init image backup for navigation map +uint8_t* imageBackupData = (uint8_t*) heap_caps_malloc((GxEPD_WIDTH * GxEPD_HEIGHT), MALLOC_CAP_SPIRAM); // Allocate PSRAM for image backup buffer for navigation map(400 x 300 pix) + public: PageNavigation(CommonData &common){ commonData = &common; @@ -119,6 +122,11 @@ public: static double magneticHeading = 0; static double speedOverGround = 0; static double depthBelowTransducer = 0; + int imgWidth = 0; + int imgHeight = 0; + int imgBackupWidth = 400; + int imgBackupHeight = 250; + bool hasImageBackup = false; // Get boat values #1 Latitude GwApi::BoatValue *bvalue1 = pageData.values[0]; // First element in list (only one value by PageOneValue) @@ -360,8 +368,8 @@ public: auto& json = net.json(); // Extract JSON content int numPix = json["number_pixels"] | 0; // Read number of pixels - int imgWidth = json["width"] | 0; // Read width of image - int imgHeight = json["height"] | 0; // Read height og image + imgWidth = json["width"] | 0; // Read width of image + imgHeight = json["height"] | 0; // Read height og image const char* b64src = json["picture_base64"].as(); // Read picture as Base64 content size_t b64len = strlen(b64src); // Calculate length of Base64 content @@ -387,13 +395,31 @@ public: size_t decodedSize = 0; decoder.decodeBase64(b64, imageData, imgSize, decodedSize); + // Copy actual navigation man to ackup map + memcpy(imageBackupData, imageData, imgSize); + // Show image (navigation map) getdisplay().drawBitmap(0, 25, imageData, imgWidth, imgHeight, commonData->fgcolor); + hasImageBackup = true; // Clean PSRAM free(b64); free(imageData); } + // If no network connection then use backup navigation map + else{ + // Show backup image (backup navigation map) + if (hasImageBackup) { + getdisplay().drawBitmap(0, 25, imageBackupData, imgBackupWidth, imgBackupHeight, commonData->fgcolor); + } + + // Show info: Connection lost + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().fillRect(200, 250 , 200, 25, commonData->fgcolor); // Black rect + getdisplay().fillRect(202, 252 , 196, 21, commonData->bgcolor); // White rect + getdisplay().setCursor(205, 270); + getdisplay().print("Connection lost"); + } // ############### Draw Values ################ From 213812ed141acd49776d8c28f96f4bfaf1d087fe Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Sat, 13 Dec 2025 21:28:52 +0100 Subject: [PATCH 074/121] Hold old map by connection lost --- lib/obp60task/PageNavigation.cpp | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp index be91519..2d5b81f 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -19,13 +19,18 @@ bool firstRun = true; // Detect the first page run int zoom = 15; // Default zoom level bool showValues = false; // Show values HDT, SOG, DBT in navigation map -// Init image backup for navigation map -uint8_t* imageBackupData = (uint8_t*) heap_caps_malloc((GxEPD_WIDTH * GxEPD_HEIGHT), MALLOC_CAP_SPIRAM); // Allocate PSRAM for image backup buffer for navigation map(400 x 300 pix) +private: + uint8_t* imageBackupData = nullptr; + int imageBackupWidth = 0; + int imageBackupHeight = 0; + size_t imageBackupSize = 0; + bool hasImageBackup = false; public: PageNavigation(CommonData &common){ commonData = &common; common.logger->logDebug(GwLog::LOG,"Instantiate PageNavigation"); + imageBackupData = (uint8_t*)heap_caps_malloc((GxEPD_WIDTH * GxEPD_HEIGHT), MALLOC_CAP_SPIRAM); } virtual int handleKey(int key){ @@ -124,9 +129,6 @@ public: static double depthBelowTransducer = 0; int imgWidth = 0; int imgHeight = 0; - int imgBackupWidth = 400; - int imgBackupHeight = 250; - bool hasImageBackup = false; // Get boat values #1 Latitude GwApi::BoatValue *bvalue1 = pageData.values[0]; // First element in list (only one value by PageOneValue) @@ -396,11 +398,17 @@ public: decoder.decodeBase64(b64, imageData, imgSize, decodedSize); // Copy actual navigation man to ackup map - memcpy(imageBackupData, imageData, imgSize); + imageBackupWidth = imgWidth; + imageBackupHeight = imgHeight; + imageBackupSize = imgSize; + if (decodedSize > 0) { + memcpy(imageBackupData, imageData, decodedSize); + imageBackupSize = decodedSize; + } + hasImageBackup = true; // Show image (navigation map) getdisplay().drawBitmap(0, 25, imageData, imgWidth, imgHeight, commonData->fgcolor); - hasImageBackup = true; // Clean PSRAM free(b64); @@ -410,7 +418,7 @@ public: else{ // Show backup image (backup navigation map) if (hasImageBackup) { - getdisplay().drawBitmap(0, 25, imageBackupData, imgBackupWidth, imgBackupHeight, commonData->fgcolor); + getdisplay().drawBitmap(0, 25, imageBackupData, imageBackupWidth, imageBackupHeight, commonData->fgcolor); } // Show info: Connection lost From c6276cdcff349bacb6bc699b3de8e394534aa76a Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Sat, 13 Dec 2025 22:11:57 +0100 Subject: [PATCH 075/121] Add delay for connection lost warning --- lib/obp60task/PageNavigation.cpp | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp index 2d5b81f..2e2f738 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -127,6 +127,7 @@ public: static double magneticHeading = 0; static double speedOverGround = 0; static double depthBelowTransducer = 0; + static int lostCounter = 0; // Counter for connection lost to the map server (increment by each page refresh) int imgWidth = 0; int imgHeight = 0; @@ -406,6 +407,7 @@ public: imageBackupSize = decodedSize; } hasImageBackup = true; + lostCounter = 0; // Show image (navigation map) getdisplay().drawBitmap(0, 25, imageData, imgWidth, imgHeight, commonData->fgcolor); @@ -421,12 +423,17 @@ public: getdisplay().drawBitmap(0, 25, imageBackupData, imageBackupWidth, imageBackupHeight, commonData->fgcolor); } - // Show info: Connection lost - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().fillRect(200, 250 , 200, 25, commonData->fgcolor); // Black rect - getdisplay().fillRect(202, 252 , 196, 21, commonData->bgcolor); // White rect - getdisplay().setCursor(205, 270); - getdisplay().print("Connection lost"); + // Show info: Connection lost when 5 page refreshes has a connection lost to the map server + // Short connection losts are uncritical + if(lostCounter >= 5){ + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().fillRect(200, 250 , 200, 25, commonData->fgcolor); // Black rect + getdisplay().fillRect(202, 252 , 196, 21, commonData->bgcolor); // White rect + getdisplay().setCursor(210, 270); + getdisplay().print("Map server lost"); + } + + lostCounter++; // Increment lost counter } From 142f6ca774351225bf1d4df12f66381f32389de2 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Sun, 14 Dec 2025 22:42:30 +0100 Subject: [PATCH 076/121] Add PageDigitalOut --- lib/obp60task/OBP60Extensions.cpp | 14 +++- lib/obp60task/OBP60Extensions.h | 2 +- lib/obp60task/OBP60Hardware.h | 6 +- lib/obp60task/PageDigitalOut.cpp | 131 ++++++++++++++++++++++++++++++ lib/obp60task/PageNavigation.cpp | 6 +- lib/obp60task/config.json | 10 +++ lib/obp60task/config_obp40.json | 10 +++ lib/obp60task/obp60task.cpp | 2 + 8 files changed, 173 insertions(+), 8 deletions(-) create mode 100644 lib/obp60task/PageDigitalOut.cpp diff --git a/lib/obp60task/OBP60Extensions.cpp b/lib/obp60task/OBP60Extensions.cpp index b296602..d386159 100644 --- a/lib/obp60task/OBP60Extensions.cpp +++ b/lib/obp60task/OBP60Extensions.cpp @@ -1,9 +1,9 @@ #if defined BOARD_OBP60S3 || defined BOARD_OBP40S3 #include -#include // Driver for PCF8574 output modul from Horter #include // I2C #include // Driver for DS1388 RTC +#include // PCF8574 modules from Horter #include "SunRise.h" // Lib for sunrise and sunset calculation #include "Pagedata.h" #include "OBP60Hardware.h" @@ -88,10 +88,11 @@ void hardwareInit(GwApi *api) Wire.begin(); // Init PCF8574 digital outputs - Wire.setClock(I2C_SPEED); // Set I2C clock on 10 kHz + Wire.setClock(I2C_SPEED_LOW); // Set I2C clock on 10 kHz if(pcf8574_Out.begin()){ // Initialize PCF8574 pcf8574_Out.write8(255); // Clear all outputs } + Wire.setClock(I2C_SPEED); // Set I2C clock on 100 kHz fram = Adafruit_FRAM_I2C(); if (esp_reset_reason() == ESP_RST_POWERON) { // help initialize FRAM @@ -192,6 +193,15 @@ void powerInit(String powermode) { } } +void setPCF8574PortPin(uint pin, uint8_t value){ + Wire.setClock(I2C_SPEED_LOW); // Set I2C clock on 10 kHz + if(pcf8574_Out.begin()){ // Check available and initialize PCF8574 + pcf8574_Out.write(pin, value); // Toggle pin + } + Wire.setClock(I2C_SPEED); // Set I2C clock on 100 kHz +} + + void setPortPin(uint pin, bool value){ pinMode(pin, OUTPUT); digitalWrite(pin, value); diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index c658797..010bd35 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -89,8 +89,8 @@ uint8_t getLastPage(); void hardwareInit(GwApi *api); void powerInit(String powermode); +void setPCF8574PortPin(uint pin, uint8_t value);// Set PCF8574 port pin void setPortPin(uint pin, bool value); // Set port pin for extension port - void togglePortPin(uint pin); // Toggle extension port pin Color colorMapping(const String &colorString); // Color mapping string to CHSV colors diff --git a/lib/obp60task/OBP60Hardware.h b/lib/obp60task/OBP60Hardware.h index 1787f83..fa25e2d 100644 --- a/lib/obp60task/OBP60Hardware.h +++ b/lib/obp60task/OBP60Hardware.h @@ -5,7 +5,8 @@ // Direction pin for RS485 NMEA0183 #define OBP_DIRECTION_PIN 18 // I2C - #define I2C_SPEED 10000UL // 10kHz clock speed on I2C bus + #define I2C_SPEED 10000UL // 100kHz clock speed on I2C bus + #define I2C_SPEED_LOW 1000UL // 10kHz clock speed on I2C bus for external bus #define OBP_I2C_SDA 47 #define OBP_I2C_SCL 21 // DS1388 RTC @@ -84,7 +85,8 @@ // Direction pin for RS485 NMEA0183 #define OBP_DIRECTION_PIN 8 // I2C - #define I2C_SPEED 100000UL // 100kHz clock speed on I2C bus + #define I2C_SPEED 100000UL // 100kHz clock speed on I2C bus + #define I2C_SPEED_LOW 1000UL // 10kHz clock speed on I2C bus for external bus #define OBP_I2C_SDA 21 #define OBP_I2C_SCL 38 // DS1388 RTC diff --git a/lib/obp60task/PageDigitalOut.cpp b/lib/obp60task/PageDigitalOut.cpp new file mode 100644 index 0000000..dfe0a76 --- /dev/null +++ b/lib/obp60task/PageDigitalOut.cpp @@ -0,0 +1,131 @@ +#if defined BOARD_OBP60S3 || defined BOARD_OBP40S3 + +#include // PCF8574 modules from Horter +#include "Pagedata.h" +#include "OBP60Extensions.h" + +#include "images/OBP_400x300.xbm" // OBP Logo +#ifdef BOARD_OBP60S3 +#include "images/OBP60_400x300.xbm" // MFD with logo +#endif +#ifdef BOARD_OBP40S3 +#include "images/OBP40_400x300.xbm" // MFD with logo +#endif + +class PageDigitalOut : public Page +{ + +// Status values +bool button1 = false; +bool button2 = false; +bool button3 = false; +bool button4 = false; +bool button5 = false; + +public: + PageDigitalOut(CommonData &common){ + commonData = &common; + common.logger->logDebug(GwLog::LOG,"Instantiate PageDigitalOut"); + } + + virtual int handleKey(int key){ + // Code for keylock + if(key == 11){ + commonData->keylock = !commonData->keylock; + return 0; // Commit the key + } + // Code for button 1 + if(key == 1){ + button1 = !button1; + setPCF8574PortPin(0, button1 ? 0 : 1); // Attention! Inverse logic for PCF8574 + return 0; // Commit the key + } + // Code for button 2 + if(key == 2){ + button2 = !button2; + setPCF8574PortPin(1, button2 ? 0 : 1); // Attention! Inverse logic for PCF8574 + return 0; // Commit the key + } + // Code for button 3 + if(key == 3){ + button3 = !button3; + setPCF8574PortPin(2, button3 ? 0 : 1); // Attention! Inverse logic for PCF8574 + return 0; // Commit the key + } + // Code for button 4 + if(key == 4){ + button4 = !button4; + setPCF8574PortPin(3, button4 ? 0 : 1); // Attention! Inverse logic for PCF8574 + return 0; // Commit the key + } + // Code for button 5 + if(key == 5){ + button5 = !button5; + setPCF8574PortPin(4, button5 ? 0 : 1); // Attention! Inverse logic for PCF8574 + return 0; // Commit the key + } + return key; + } + + int displayPage(PageData &pageData){ + GwConfigHandler *config = commonData->config; + GwLog *logger = commonData->logger; + + // Get config data + String lengthformat = config->getString(config->lengthFormat); + bool simulation = config->getBool(config->useSimuData); + bool holdvalues = config->getBool(config->holdvalues); + String flashLED = config->getString(config->flashLED); + String backlightMode = config->getString(config->backlight); + + // Optical warning by limit violation (unused) + if(String(flashLED) == "Limit Violation"){ + setBlinkingLED(false); + setFlashLED(false); + } + + // Logging boat values + LOG_DEBUG(GwLog::LOG,"Drawing at PageDigitalOut"); + + // Draw page + //*********************************************************** + + // Set display in partial refresh mode + getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + getdisplay().setTextColor(commonData->fgcolor); + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().fillRoundRect(200, 250 , 200, 25, 5, commonData->fgcolor); // Black rect + getdisplay().fillRoundRect(202, 252 , 196, 21, 5, commonData->bgcolor); // White rect + getdisplay().setCursor(210, 270); + getdisplay().print("Map server lost"); + + // Set botton labels + commonData->keydata[0].label = "BTN 1"; + commonData->keydata[1].label = "BTN 2"; + commonData->keydata[2].label = "BTN 3"; + commonData->keydata[3].label = "BTN 4"; + commonData->keydata[4].label = "BTN 5"; + + return PAGE_UPDATE; + }; +}; + +static Page* createPage(CommonData &common){ + return new PageDigitalOut(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 registerPageDigitalOut( + "DigitalOut", // Page name + createPage, // Action + 0, // Number of bus values depends on selection in Web configuration + true // Show display header on/off +); + +#endif diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp index 2e2f738..7531685 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -39,7 +39,7 @@ public: commonData->keylock = !commonData->keylock; return 0; // Commit the key } - // Cood for zoom - + // Code for zoom - if(key == 1){ zoom --; // Zoom - if(zoom <7){ @@ -47,7 +47,7 @@ public: } return 0; // Commit the key } - // Cood for zoom - + // Code for zoom - if(key == 2){ zoom ++; // Zoom + if(zoom >17){ @@ -68,7 +68,7 @@ public: // Get config data String lengthformat = config->getString(config->lengthFormat); - // bool simulation = config->getBool(config->useSimuData); + bool simulation = config->getBool(config->useSimuData); bool holdvalues = config->getBool(config->holdvalues); String flashLED = config->getString(config->flashLED); String backlightMode = config->getString(config->backlight); diff --git a/lib/obp60task/config.json b/lib/obp60task/config.json index a7e4ced..3c7d3fe 100644 --- a/lib/obp60task/config.json +++ b/lib/obp60task/config.json @@ -1364,6 +1364,7 @@ "Battery2", "Clock", "Compass", + "DigitalOut", "DST810", "Fluid", "FourValues", @@ -1663,6 +1664,7 @@ "Battery2", "Clock", "Compass", + "DigitalOut", "DST810", "Fluid", "FourValues", @@ -1954,6 +1956,7 @@ "Battery2", "Clock", "Compass", + "DigitalOut", "DST810", "Fluid", "FourValues", @@ -2237,6 +2240,7 @@ "Battery2", "Clock", "Compass", + "DigitalOut", "DST810", "Fluid", "FourValues", @@ -2512,6 +2516,7 @@ "Battery2", "Clock", "Compass", + "DigitalOut", "DST810", "Fluid", "FourValues", @@ -2779,6 +2784,7 @@ "Battery2", "Clock", "Compass", + "DigitalOut", "DST810", "Fluid", "FourValues", @@ -3038,6 +3044,7 @@ "Battery2", "Clock", "Compass", + "DigitalOut", "DST810", "Fluid", "FourValues", @@ -3289,6 +3296,7 @@ "Battery2", "Clock", "Compass", + "DigitalOut", "DST810", "Fluid", "FourValues", @@ -3532,6 +3540,7 @@ "Battery2", "Clock", "Compass", + "DigitalOut", "DST810", "Fluid", "FourValues", @@ -3767,6 +3776,7 @@ "Battery2", "Clock", "Compass", + "DigitalOut", "DST810", "Fluid", "FourValues", diff --git a/lib/obp60task/config_obp40.json b/lib/obp60task/config_obp40.json index 91985e3..c8553c0 100644 --- a/lib/obp60task/config_obp40.json +++ b/lib/obp60task/config_obp40.json @@ -1387,6 +1387,7 @@ "Battery2", "Clock", "Compass", + "DigitalOut", "DST810", "Fluid", "FourValues", @@ -1716,6 +1717,7 @@ "Battery2", "Clock", "Compass", + "DigitalOut", "DST810", "Fluid", "FourValues", @@ -2036,6 +2038,7 @@ "Battery2", "Clock", "Compass", + "DigitalOut", "DST810", "Fluid", "FourValues", @@ -2347,6 +2350,7 @@ "Battery2", "Clock", "Compass", + "DigitalOut", "DST810", "Fluid", "FourValues", @@ -2649,6 +2653,7 @@ "Battery2", "Clock", "Compass", + "DigitalOut", "DST810", "Fluid", "FourValues", @@ -2942,6 +2947,7 @@ "Battery2", "Clock", "Compass", + "DigitalOut", "DST810", "Fluid", "FourValues", @@ -3226,6 +3232,7 @@ "Battery2", "Clock", "Compass", + "DigitalOut", "DST810", "Fluid", "FourValues", @@ -3501,6 +3508,7 @@ "Battery2", "Clock", "Compass", + "DigitalOut", "DST810", "Fluid", "FourValues", @@ -3767,6 +3775,7 @@ "Battery2", "Clock", "Compass", + "DigitalOut", "DST810", "Fluid", "FourValues", @@ -4024,6 +4033,7 @@ "Battery2", "Clock", "Compass", + "DigitalOut", "DST810", "Fluid", "FourValues", diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index 17e586a..45c3752 100644 --- a/lib/obp60task/obp60task.cpp +++ b/lib/obp60task/obp60task.cpp @@ -262,6 +262,8 @@ void registerAllPages(PageList &list){ list.add(®isterPageSkyView); extern PageDescription registerPageNavigation; list.add(®isterPageNavigation); + extern PageDescription registerPageDigitalOut; + list.add(®isterPageDigitalOut); } // Undervoltage detection for shutdown display From d65567452907d3b9e8fe096a285976369997f3f3 Mon Sep 17 00:00:00 2001 From: Ulrich Meine Date: Sun, 14 Dec 2025 20:17:36 +0100 Subject: [PATCH 077/121] Revert wind speed chart for horizontal charts; changed chart format parameters to ; adjusted chart size to OBP standard Revert wind speed chart for horizontal charts; changed chart format parameters to ; adjusted chart size to OBP standard - commit from PageWindPlot-v2 --- lib/obp60task/OBPcharts.cpp | 106 ++++++++++++++++++--------------- lib/obp60task/OBPcharts.h | 13 ++-- lib/obp60task/PageWindPlot.cpp | 21 +++---- 3 files changed, 75 insertions(+), 65 deletions(-) diff --git a/lib/obp60task/OBPcharts.cpp b/lib/obp60task/OBPcharts.cpp index 17caf75..b78bc6e 100644 --- a/lib/obp60task/OBPcharts.cpp +++ b/lib/obp60task/OBPcharts.cpp @@ -5,7 +5,7 @@ // --- Class Chart --------------- template -Chart::Chart(RingBuffer& dataBuf, int8_t chrtDir, int8_t chrtSz, double dfltRng, CommonData& common, bool useSimuData) +Chart::Chart(RingBuffer& dataBuf, char chrtDir, int8_t chrtSz, double dfltRng, CommonData& common, bool useSimuData) : dataBuf(dataBuf) , chrtDir(chrtDir) , chrtSz(chrtSz) @@ -21,41 +21,43 @@ Chart::Chart(RingBuffer& dataBuf, int8_t chrtDir, int8_t chrtSz, double df dWidth = getdisplay().width(); dHeight = getdisplay().height(); - if (chrtDir == 0) { + if (chrtDir == 'H') { // horizontal chart timeline direction timAxis = dWidth; switch (chrtSz) { case 0: valAxis = dHeight - top - bottom; - cStart = { 0, top }; + cStart = { 0, top - 1 }; break; case 1: valAxis = (dHeight - top - bottom) / 2 - hGap; - cStart = { 0, top }; + cStart = { 0, top - 1 }; break; case 2: valAxis = (dHeight - top - bottom) / 2 - hGap; - cStart = { 0, top + (valAxis + hGap) + hGap }; + cStart = { 0, top + (valAxis + hGap) + hGap - 1 }; break; default: LOG_DEBUG(GwLog::ERROR, "displayChart: wrong init parameter"); return; } - } else if (chrtDir == 1) { + } else if (chrtDir == 'V') { // vertical chart timeline direction timAxis = dHeight - top - bottom; switch (chrtSz) { case 0: valAxis = dWidth; - cStart = { 0, top }; + cStart = { 0, top - 1 }; break; case 1: - valAxis = dWidth / 2 - vGap - 1; - cStart = { 0, top }; + // valAxis = dWidth / 2 - vGap - 1; + valAxis = dWidth / 2 - vGap; + cStart = { 0, top - 1 }; break; case 2: - valAxis = dWidth / 2 - vGap - 1; - cStart = { dWidth / 2 + vGap, top }; + // valAxis = dWidth / 2 - vGap - 1; + valAxis = dWidth / 2 - vGap; + cStart = { dWidth / 2 + vGap - 1, top - 1 }; break; default: LOG_DEBUG(GwLog::ERROR, "displayChart: wrong init parameter"); @@ -71,17 +73,21 @@ Chart::Chart(RingBuffer& dataBuf, int8_t chrtDir, int8_t chrtSz, double df dbMAX_VAL = dataBuf.getMaxVal(); bufSize = dataBuf.getCapacity(); - if (dbFormat == "formatCourse" || dbFormat == "FormatWind" || dbFormat == "FormatRot") { + if (dbFormat == "formatCourse" || dbFormat == "formatWind" || dbFormat == "formatRot") { - if (dbFormat == "FormatRot") { - chrtDataFmt = 2; // Chart is showing data of rotational format + if (dbFormat == "formatRot") { + chrtDataFmt = 'R'; // Chart is showing data of rotational format } else { - chrtDataFmt = 1; // Chart is showing data of course / wind format + chrtDataFmt = 'W'; // Chart is showing data of course / wind format } rngStep = M_TWOPI / 360.0 * 10.0; // +/-10 degrees on each end of chrtMid; we are calculating with SI values } else { - chrtDataFmt = 0; // Chart is showing any other data format than + if (dbFormat == "formatDepth") { + chrtDataFmt = 'D'; // Chart ist showing data of format + } else { + chrtDataFmt = 'S'; // Chart is showing any other data format than + } rngStep = 5.0; // +/- 10 for all other values (eg. m/s, m, K, mBar) } @@ -178,16 +184,19 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) chrtPrevVal = dbMAX_VAL; } else { - if (chrtDir == 0) { // horizontal chart + if (chrtDir == 'H') { // horizontal chart x = cStart.x + i; // Position in chart area - if (chrtDataFmt == 0) { + if (chrtDataFmt == 'S') { + // y = cStart.y + static_cast(((chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round + y = cStart.y + valAxis - static_cast(((chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round + } else if (chrtDataFmt == 'D') { y = cStart.y + static_cast(((chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round } else { // degree type value y = cStart.y + static_cast((WindUtils::to2PI(chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round } } else { // vertical chart y = cStart.y + timAxis - i; // Position in chart area - if (chrtDataFmt == 0) { + if (chrtDataFmt == 'S' || chrtDataFmt == 'D') { x = cStart.x + static_cast(((chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round } else { // degree type value x = cStart.x + static_cast((WindUtils::to2PI(chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round @@ -202,7 +211,7 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) prevX = x; prevY = y; - } else if (chrtDataFmt != 0) { + } else if (chrtDataFmt == 'W' || chrtDataFmt == 'R') { // cross borders check for degree values; shift values to [-PI..0..PI]; when crossing borders, range is 2x PI degrees double normCurr = WindUtils::to2PI(chrtVal - chrtMin); double normPrev = WindUtils::to2PI(chrtPrevVal - chrtMin); @@ -212,7 +221,7 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) if (crossedBorders) { // If current value crosses chart borders compared to previous value, split line // LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Chart: crossedBorders: %d, chrtVal: %.2f, chrtPrevVal: %.2f", crossedBorders, chrtVal, chrtPrevVal); bool wrappingFromHighToLow = normCurr < normPrev; // Determine which edge we're crossing - if (chrtDir == 0) { + if (chrtDir == 'H') { int ySplit = wrappingFromHighToLow ? (cStart.y + valAxis) : cStart.y; getdisplay().drawLine(prevX, prevY, x, ySplit, fgColor); if (x != prevX) { // line with some horizontal trend @@ -231,12 +240,18 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) } // Draw line with 2 pixels width + make sure vertical lines are drawn correctly - if (chrtDir == 0 || x == prevX) { // horizontal chart & vertical line -// if (x == prevX) { // vertical line + if (chrtDir == 'H' || x == prevX) { // horizontal chart & vertical line + if (chrtDataFmt == 'D') { + getdisplay().drawLine(x, y, x, cStart.y + valAxis, fgColor); + getdisplay().drawLine(x - 1, y, x - 1, cStart.y + valAxis, fgColor); + } getdisplay().drawLine(prevX, prevY, x, y, fgColor); getdisplay().drawLine(prevX - 1, prevY, x - 1, y, fgColor); - } else if (chrtDir == 1 || x != prevX) { // vertical chart & line with some horizontal trend -> normal state -// } else { // line with some horizontal trend -> normal state + } else if (chrtDir == 'V' || x != prevX) { // vertical chart & line with some horizontal trend -> normal state + if (chrtDataFmt == 'D') { + getdisplay().drawLine(x, y, cStart.x + valAxis, y, fgColor); + getdisplay().drawLine(x, y - 1, cStart.x + valAxis, y - 1, fgColor); + } getdisplay().drawLine(prevX, prevY, x, y, fgColor); getdisplay().drawLine(prevX, prevY - 1, x, y - 1, fgColor); } @@ -249,23 +264,22 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) if (i >= timAxis - 1) { oldChrtIntv = 0; // force reset of buffer start and number of values to show in next display loop - if (chrtDataFmt == 1) { // degree of course or wind + if (chrtDataFmt == 'W') { // degree of course or wind recalcRngCntr = true; LOG_DEBUG(GwLog::DEBUG, "PageWindPlot FreeTop: timAxis: %d, i: %d, bufStart: %d, numBufVals: %d, recalcRngCntr: %d", timAxis, i, bufStart, numBufVals, recalcRngCntr); } break; } } - } else { // No valid data available getdisplay().setFont(&Ubuntu_Bold10pt8b); int pX, pY; - if (chrtDir == 0) { // horizontal chart + if (chrtDir == 'H') { pX = cStart.x + (timAxis / 2); pY = cStart.y + (valAxis / 2) - 10; - } else { // vertical chart + } else { pX = cStart.x + (valAxis / 2); pY = cStart.y + (timAxis / 2) - 10; } @@ -316,7 +330,7 @@ double Chart::getRng(double center, size_t amount) template void Chart::calcChrtBorders(double& rngMid, double& rngMin, double& rngMax, double& rng) { - if (chrtDataFmt == 0) { + if (chrtDataFmt == 'S' || chrtDataFmt == 'D') { // Chart data is of any type but 'degree' double oldRngMin = rngMin; @@ -349,7 +363,7 @@ void Chart::calcChrtBorders(double& rngMid, double& rngMin, double& rngMax, d currMinVal, currMaxVal, rngMin, rngMid, rngMax, rng, rngStep, oldRngMin, oldRngMax, dfltRng, numBufVals); } else { - if (chrtDataFmt == 1) { + if (chrtDataFmt == 'W') { // Chart data is of type 'course' or 'wind' if ((count == 1 && rngMid == 0) || rngMid == dbMAX_VAL) { @@ -378,7 +392,7 @@ void Chart::calcChrtBorders(double& rngMid, double& rngMin, double& rngMax, d rng * RAD_TO_DEG, rngStep * RAD_TO_DEG); } - } else if (chrtDataFmt == 2) { + } else if (chrtDataFmt == 'R') { // Chart data is of type 'rotation'; then we want to have always to be '0' rngMid = 0; } @@ -417,7 +431,7 @@ void Chart::drawChrtTimeAxis(int8_t chrtIntv) getdisplay().setFont(&Ubuntu_Bold8pt8b); getdisplay().setTextColor(fgColor); - if (chrtDir == 0) { // horizontal chart + if (chrtDir == 'H') { // horizontal chart getdisplay().fillRect(0, cStart.y, dWidth, 2, fgColor); timeRng = chrtIntv * 4; // Chart time interval: [1] 4 min., [2] 8 min., [3] 12 min., [4] 16 min., [8] 32 min. @@ -478,7 +492,7 @@ void Chart::drawChrtValAxis() { double slots; int i, intv; - double cVal, cchrtRng, crngMin; + double cVal, cChrtRng, crngMin; char sVal[6]; int sLen; std::unique_ptr tmpBVal; // Temp variable to get formatted and converted data value from OBP60Formatter @@ -486,16 +500,17 @@ void Chart::drawChrtValAxis() tmpBVal->setFormat(dataBuf.getFormat()); tmpBVal->valid = true; - if (chrtDir == 0) { // horizontal chart + if (chrtDir == 'H') { slots = valAxis / 60.0; // number of axis labels tmpBVal->value = chrtRng; - cchrtRng = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) - intv = static_cast(round(cchrtRng / slots)); + cChrtRng = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) + intv = static_cast(round(cChrtRng / slots)); i = intv; if (chrtSz == 0) { // full size chart -> print multiple value lines getdisplay().setFont(&Ubuntu_Bold12pt8b); - for (int j = 60; j < valAxis - 30; j += 60) { +// for (int j = 60; j < valAxis - 30; j += 60) { + for (int j = valAxis - 60; j > 30; j -= 60) { getdisplay().drawLine(cStart.x, cStart.y + j, cStart.x + timAxis, cStart.y + j, fgColor); getdisplay().fillRect(cStart.x, cStart.y + j - 11, 42, 21, bgColor); // Clear small area to remove potential chart lines @@ -542,7 +557,7 @@ void Chart::drawChrtValAxis() } else { getdisplay().setFont(&Ubuntu_Bold10pt8b); } - getdisplay().fillRect(cStart.x, top, valAxis, 2, fgColor); // top chart line + getdisplay().fillRect(cStart.x, cStart.y, valAxis, 2, fgColor); // top chart line tmpBVal->value = chrtMin; cVal = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) @@ -560,14 +575,9 @@ void Chart::drawChrtValAxis() snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); drawTextRalign(cStart.x + valAxis - 2, cStart.y - 2, sVal); // Range high end - for (int j = 0; j <= valAxis + 2; j += ((valAxis + 2) / 2)) { + for (int j = 0; j <= valAxis; j += (valAxis / 2)) { getdisplay().drawLine(cStart.x + j, cStart.y, cStart.x + j, cStart.y + timAxis, fgColor); } - - // if (chrtSz == 0) { - // getdisplay().setFont(&Ubuntu_Bold12pt8b); - // drawTextCenter(cStart.x + (valAxis / 4) + 15, cStart.y - 11, dbName); // buffer data name - // } } } @@ -575,8 +585,8 @@ void Chart::drawChrtValAxis() template void Chart::prntCurrValue(GwApi::BoatValue& currValue) { - const int xPosVal = (chrtDir == 0) ? cStart.x + (timAxis / 2) - 56 : cStart.x + 32; - const int yPosVal = (chrtDir == 0) ? cStart.y + valAxis - 7 : cStart.y + timAxis - 7; + const int xPosVal = (chrtDir == 'H') ? cStart.x + (timAxis / 2) - 56 : cStart.x + 32; + const int yPosVal = (chrtDir == 'H') ? cStart.y + valAxis - 5 : cStart.y + timAxis - 5; FormattedData frmtDbData = formatValue(&currValue, *commonData); double testdbValue = frmtDbData.value; @@ -585,7 +595,7 @@ void Chart::prntCurrValue(GwApi::BoatValue& currValue) // LOG_DEBUG(GwLog::DEBUG, "Chart CurrValue: dbValue: %.2f, sdbValue: %s, fmrtDbValue: %.2f, dbFormat: %s, dbUnit: %s, Valid: %d, Name: %s, Address: %p", currValue.value, sdbValue, // testdbValue, currValue.getFormat(), dbUnit, currValue.valid, currValue.getName(), currValue); - getdisplay().fillRect(xPosVal - 1, yPosVal - 34, 125, 41, bgColor); // Clear area for TWS value + getdisplay().fillRect(xPosVal - 1, yPosVal - 34, 125, 40, bgColor); // Clear area for TWS value getdisplay().drawRect(xPosVal, yPosVal - 33, 123, 39, fgColor); // Draw box for TWS value getdisplay().setFont(&DSEG7Classic_BoldItalic16pt7b); getdisplay().setCursor(xPosVal + 1, yPosVal); diff --git a/lib/obp60task/OBPcharts.h b/lib/obp60task/OBPcharts.h index db298a5..f160dd4 100644 --- a/lib/obp60task/OBPcharts.h +++ b/lib/obp60task/OBPcharts.h @@ -16,19 +16,18 @@ protected: GwLog *logger; RingBuffer &dataBuf; // Buffer to display - int8_t chrtDir; // Chart timeline direction: [0] = horizontal, [1] = vertical + char chrtDir; // Chart timeline direction: 'H' = horizontal, 'V' = vertical int8_t chrtSz; // Chart size: [0] = full size, [1] = half size left/top, [2] half size right/bottom double dfltRng; // Default range of chart, e.g. 30 = [0..30] uint16_t fgColor; // color code for any screen writing uint16_t bgColor; // color code for screen background bool useSimuData; // flag to indicate if simulation data is active - int top = 48; // display top header lines - int bottom = 22; // display bottom lines + // int top = 48; // display top header lines + int top = 44; // chart gap at top of display (25 lines for standard gap + 19 lines for axis labels) + int bottom = 25; // chart gap at bottom of display to keep space for status line int hGap = 11; // gap between 2 horizontal charts; actual gap is 2x int vGap = 20; // gap between 2 vertical charts; actual gap is 2x - int xOffset = 33; // offset for horizontal axis (time/value), because of space for left vertical axis labeling - int yOffset = 10; // offset for vertical axis (time/value), because of space for top horizontal axis labeling int dWidth; // Display width int dHeight; // Display height int timAxis, valAxis; // size of time and value chart axis @@ -41,7 +40,7 @@ protected: bool recalcRngCntr = false; // Flag for re-calculation of mid value of chart for wind data types String dbName, dbFormat; // Name and format of data buffer - int chrtDataFmt; // Data format of chart: [0] size values; [1] degree of course or wind; [2] rotational degrees + char chrtDataFmt; // Data format of chart: 'S' = size values; 'D' = depth value, 'W' = degree of course or wind; 'R' rotational degrees double dbMIN_VAL; // Lowest possible value of buffer of type double dbMAX_VAL; // Highest possible value of buffer of type ; indicates invalid value in buffer size_t bufSize; // History buffer size: 1.920 values for 32 min. history chart @@ -63,7 +62,7 @@ protected: void prntCurrValue(GwApi::BoatValue& currValue); // Add current boat data value to chart public: - Chart(RingBuffer& dataBuf, int8_t chrtDir, int8_t chrtSz, double dfltRng, CommonData& common, bool useSimuData); // Chart object of data chart + Chart(RingBuffer& dataBuf, char chrtDir, int8_t chrtSz, double dfltRng, CommonData& common, bool useSimuData); // Chart object of data chart ~Chart(); void showChrt(int8_t chrtIntv, GwApi::BoatValue currValue); // Perform all actions to draw chart diff --git a/lib/obp60task/PageWindPlot.cpp b/lib/obp60task/PageWindPlot.cpp index a30c0c8..1ce55b9 100644 --- a/lib/obp60task/PageWindPlot.cpp +++ b/lib/obp60task/PageWindPlot.cpp @@ -167,12 +167,13 @@ public: LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Creating true wind charts"); auto* twdHstry = pageData.hstryManager->getBuffer("TWD"); auto* twsHstry = pageData.hstryManager->getBuffer("TWS"); - twdFlChart = std::unique_ptr>(new Chart(*twdHstry, 1, 0, dfltRngWd, *commonData, useSimuData)); - twsFlChart = std::unique_ptr>(new Chart(*twsHstry, 0, 0, dfltRngWs, *commonData, useSimuData)); - twdHfChart = std::unique_ptr>(new Chart(*twdHstry, 1, 1, dfltRngWd, *commonData, useSimuData)); - twsHfChart = std::unique_ptr>(new Chart(*twsHstry, 1, 2, dfltRngWs, *commonData, useSimuData)); - // twdHfChart = std::unique_ptr>(new Chart(*twdHstry, 0, 1, dfltRngWd, *commonData, useSimuData)); - // twsHfChart = std::unique_ptr>(new Chart(*twsHstry, 0, 2, dfltRngWs, *commonData, useSimuData)); + + twdFlChart = std::unique_ptr>(new Chart(*twdHstry, 'V', 0, dfltRngWd, *commonData, useSimuData)); + twsFlChart = std::unique_ptr>(new Chart(*twsHstry, 'H', 0, dfltRngWs, *commonData, useSimuData)); + twdHfChart = std::unique_ptr>(new Chart(*twdHstry, 'V', 1, dfltRngWd, *commonData, useSimuData)); + twsHfChart = std::unique_ptr>(new Chart(*twsHstry, 'V', 2, dfltRngWs, *commonData, useSimuData)); + // twdHfChart = std::unique_ptr>(new Chart(*twdHstry, 'H', 1, dfltRngWd, *commonData, useSimuData)); + // twsHfChart = std::unique_ptr>(new Chart(*twsHstry, 'H', 2, dfltRngWs, *commonData, useSimuData)); // LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: twdHstry: %p, twsHstry: %p", (void*)twdHstry, (void*)twsHstry); } @@ -181,10 +182,10 @@ public: auto* awdHstry = pageData.hstryManager->getBuffer("AWD"); auto* awsHstry = pageData.hstryManager->getBuffer("AWS"); - awdFlChart = std::unique_ptr>(new Chart(*awdHstry, 1, 0, dfltRngWd, *commonData, useSimuData)); - awsFlChart = std::unique_ptr>(new Chart(*awsHstry, 0, 0, dfltRngWs, *commonData, useSimuData)); - awdHfChart = std::unique_ptr>(new Chart(*awdHstry, 1, 1, dfltRngWd, *commonData, useSimuData)); - awsHfChart = std::unique_ptr>(new Chart(*awsHstry, 1, 2, dfltRngWs, *commonData, useSimuData)); + awdFlChart = std::unique_ptr>(new Chart(*awdHstry, 'V', 0, dfltRngWd, *commonData, useSimuData)); + awsFlChart = std::unique_ptr>(new Chart(*awsHstry, 'H', 0, dfltRngWs, *commonData, useSimuData)); + awdHfChart = std::unique_ptr>(new Chart(*awdHstry, 'V', 1, dfltRngWd, *commonData, useSimuData)); + awsHfChart = std::unique_ptr>(new Chart(*awsHstry, 'V', 2, dfltRngWs, *commonData, useSimuData)); } // Switch active charts based on showTruW From 41a8e7d0785bf1f5be7cc4eb5cfa5755e7efd090 Mon Sep 17 00:00:00 2001 From: Ulrich Meine Date: Sat, 20 Dec 2025 22:42:42 +0100 Subject: [PATCH 078/121] General history buffers working; fine tuning required --- lib/obp60task/OBPDataOperations.cpp | 130 ++++++++++++++-------------- lib/obp60task/OBPDataOperations.h | 61 ++++++++++--- lib/obp60task/PageWindPlot.cpp | 59 +++++-------- lib/obp60task/Pagedata.h | 2 +- lib/obp60task/obp60task.cpp | 22 +++-- 5 files changed, 153 insertions(+), 121 deletions(-) diff --git a/lib/obp60task/OBPDataOperations.cpp b/lib/obp60task/OBPDataOperations.cpp index 3eadde5..898ba5a 100644 --- a/lib/obp60task/OBPDataOperations.cpp +++ b/lib/obp60task/OBPDataOperations.cpp @@ -1,38 +1,34 @@ #include "OBPDataOperations.h" #include "BoatDataCalibration.h" // Functions lib for data instance calibration #include -#include -#include -#include // --- Class HstryBuf --------------- - -HstryBuf::HstryBuf(const String& name, int size, GwLog* log, BoatValueList* boatValues) +HstryBuf::HstryBuf(const String& name, int size, BoatValueList* boatValues, GwLog* log) : logger(log), boatDataName(name) { - hstry.resize(size); + hstryBuf.resize(size); boatValue = boatValues->findValueOrCreate(name); } void HstryBuf::init(const String& format, int updFreq, int mltplr, double minVal, double maxVal) { - hstry.setMetaData(boatDataName, format, updFreq, mltplr, minVal, maxVal); + hstryBuf.setMetaData(boatDataName, format, updFreq, mltplr, minVal, maxVal); hstryMin = minVal; hstryMax = maxVal; if (!boatValue->valid) { boatValue->setFormat(format); - boatValue->value = std::numeric_limits::max(); + boatValue->value = std::numeric_limits::max(); // mark current value invalid } } void HstryBuf::add(double value) { if (value >= hstryMin && value <= hstryMax) { - hstry.add(value); + hstryBuf.add(value); } } void HstryBuf::handle(bool useSimuData) { GwApi::BoatValue *calBVal; - if (boatValue->valid) { + if (boatValue->valid) { // add calibrated boat value to history buffer calBVal = new GwApi::BoatValue(boatDataName.c_str()); calBVal->setFormat(boatValue->getFormat()); calBVal->value = boatValue->value; @@ -41,8 +37,8 @@ void HstryBuf::handle(bool useSimuData) { add(calBVal->value); delete calBVal; calBVal = nullptr; - } else if (useSimuData) { - double simValue = hstry.getLast(); + } else if (useSimuData) { // add simulated value to history buffer + double simValue = hstryBuf.getLast(); if (boatDataName == "TWD" || boatDataName == "AWD") { simValue += static_cast(random(-349, 349) / 1000.0); simValue = WindUtils::to2PI(simValue); @@ -53,48 +49,53 @@ void HstryBuf::handle(bool useSimuData) { add(simValue); } } +// --- End Class HstryBuf --------------- -// --- Class HstryManager --------------- -HstryManager::HstryManager(int size, GwLog* log, BoatValueList* boatValues) { - // Create history buffers for each boat data type - hstryBufs["TWD"] = std::unique_ptr(new HstryBuf("TWD", size, log, boatValues)); - hstryBufs["TWS"] = std::unique_ptr(new HstryBuf("TWS", size, log, boatValues)); - hstryBufs["AWD"] = std::unique_ptr(new HstryBuf("AWD", size, log, boatValues)); - hstryBufs["AWS"] = std::unique_ptr(new HstryBuf("AWS", size, log, boatValues)); - - // Initialize metadata for each buffer - int hstryUpdFreq = 1000; // Update frequency for history buffers in ms - int mltplr = 1000; // Multiplier which transforms original value into buffer type format - double hstryMinVal = 0; // Minimum value for these history buffers - double courseMax = 2 * M_PI; - double speedMax = 65; - - mltplr = 10000; // Store 4 decimals for course data - hstryBufs["TWD"]->init("formatCourse", hstryUpdFreq, mltplr, hstryMinVal, courseMax); - hstryBufs["AWD"]->init("formatCourse", hstryUpdFreq, mltplr, hstryMinVal, courseMax); - - mltplr = 1000; // Store 3 decimals for windspeed data - hstryBufs["TWS"]->init("formatKnots", hstryUpdFreq, mltplr, hstryMinVal, speedMax); - hstryBufs["AWS"]->init("formatKnots", hstryUpdFreq, mltplr, hstryMinVal, speedMax); +// --- Class HstryBuffers --------------- +HstryBuffers::HstryBuffers(int size, BoatValueList* boatValues, GwLog* log) + : size(size), boatValueList(boatValues), logger(log) { // collect boat values for true wind calculation - awaBVal = boatValues->findValueOrCreate("AWA"); - hdtBVal = boatValues->findValueOrCreate("HDT"); - hdmBVal = boatValues->findValueOrCreate("HDM"); - varBVal = boatValues->findValueOrCreate("VAR"); - cogBVal = boatValues->findValueOrCreate("COG"); - sogBVal = boatValues->findValueOrCreate("SOG"); - awdBVal = boatValues->findValueOrCreate("AWD"); + // should have been already all created at true wind object initialization + // potentially to be moved to history buffer handling + awaBVal = boatValueList->findValueOrCreate("AWA"); + hdtBVal = boatValueList->findValueOrCreate("HDT"); + hdmBVal = boatValueList->findValueOrCreate("HDM"); + varBVal = boatValueList->findValueOrCreate("VAR"); + cogBVal = boatValueList->findValueOrCreate("COG"); + sogBVal = boatValueList->findValueOrCreate("SOG"); + awdBVal = boatValueList->findValueOrCreate("AWD"); +} + +void HstryBuffers::addBuffer(const String& name) { + // Create history buffer for boat data type + + if (HstryBuffers::getBuffer(name) != nullptr) { // buffer for this data type already exists + return; + } + + hstryBuffers[name] = std::unique_ptr(new HstryBuf(name, size, boatValueList, logger)); + + // Initialize metadata for buffer + // String valueFormat = boatValueList->findValueOrCreate(name)->getFormat().c_str(); // Unfortunately, format is not yet available during system initialization + String valueFormat = bufferParams[name].format; // Data format of boat data type + int hstryUpdFreq = bufferParams[name].hstryUpdFreq; // Update frequency for history buffers in ms + int mltplr = bufferParams[name].mltplr; // default multiplier which transforms original value into buffer type format + double bufferMinVal = bufferParams[name].bufferMinVal; // Min value for this history buffer + double bufferMaxVal = bufferParams[name].bufferMaxVal; // Max value for this history buffer + + hstryBuffers[name]->init(valueFormat, hstryUpdFreq, mltplr, bufferMinVal, bufferMaxVal); + LOG_DEBUG(GwLog::DEBUG,"HstryBuffers-new buffer added: name: %s, format: %s, multiplier: %d, min value: %.2f, max value: %.2f", name, valueFormat, mltplr, bufferMinVal, bufferMaxVal); } // Handle history buffers for TWD, TWS, AWD, AWS -void HstryManager::handleHstryBufs(bool useSimuData) { +void HstryBuffers::handleHstryBufs(bool useSimuData) { static double hdt = 20; //initial value only relevant if we use simulation data GwApi::BoatValue *calBVal; // temp variable just for data calibration -> we don't want to calibrate the original data here // Handle all registered history buffers - for (auto& pair : hstryBufs) { + for (auto& pair : hstryBuffers) { auto& buf = pair.second; buf->handle(useSimuData); } @@ -118,8 +119,8 @@ void HstryManager::handleHstryBufs(bool useSimuData) { awdBVal->value = calBVal->value; awdBVal->valid = true; // Find the AWD buffer and add the value. - auto it = hstryBufs.find("AWD"); - if (it != hstryBufs.end()) { + auto it = hstryBuffers.find("AWD"); + if (it != hstryBuffers.end()) { it->second->add(calBVal->value); } @@ -130,14 +131,14 @@ void HstryManager::handleHstryBufs(bool useSimuData) { } } -RingBuffer* HstryManager::getBuffer(const String& name) { - auto it = hstryBufs.find(name); - if (it != hstryBufs.end()) { - return &it->second->hstry; +RingBuffer* HstryBuffers::getBuffer(const String& name) { + auto it = hstryBuffers.find(name); + if (it != hstryBuffers.end()) { + return &it->second->hstryBuf; } return nullptr; } -// --- Class HstryBuf --------------- +// --- End Class HstryBuffers --------------- // --- Class WindUtils -------------- double WindUtils::to2PI(double a) @@ -277,23 +278,24 @@ bool WindUtils::calcTrueWind(const double* awaVal, const double* awsVal, } // Calculate true wind data and add to obp60task boat data list -bool WindUtils::addTrueWind(GwApi* api, BoatValueList* boatValues, GwLog* log) { +//bool WindUtils::addTrueWind(GwApi* api, GwLog* log) { +bool WindUtils::addTrueWind() { - GwLog* logger = log; +// GwLog* logger = log; - double awaVal, awsVal, cogVal, stwVal, sogVal, hdtVal, hdmVal, varVal; +// double awaVal, awsVal, cogVal, stwVal, sogVal, hdtVal, hdmVal, varVal; double twd, tws, twa; bool isCalculated = false; - awaVal = awaBVal->valid ? awaBVal->value : DBL_MAX; - awsVal = awsBVal->valid ? awsBVal->value : DBL_MAX; - cogVal = cogBVal->valid ? cogBVal->value : DBL_MAX; - stwVal = stwBVal->valid ? stwBVal->value : DBL_MAX; - sogVal = sogBVal->valid ? sogBVal->value : DBL_MAX; - hdtVal = hdtBVal->valid ? hdtBVal->value : DBL_MAX; - hdmVal = hdmBVal->valid ? hdmBVal->value : DBL_MAX; - varVal = varBVal->valid ? varBVal->value : DBL_MAX; - LOG_DEBUG(GwLog::DEBUG,"obp60task addTrueWind: AWA %.1f, AWS %.1f, COG %.1f, STW %.1f, SOG %.2f, HDT %.1f, HDM %.1f, VAR %.1f", awaBVal->value * RAD_TO_DEG, awsBVal->value * 3.6 / 1.852, + double awaVal = awaBVal->valid ? awaBVal->value : DBL_MAX; + double awsVal = awsBVal->valid ? awsBVal->value : DBL_MAX; + double cogVal = cogBVal->valid ? cogBVal->value : DBL_MAX; + double stwVal = stwBVal->valid ? stwBVal->value : DBL_MAX; + double sogVal = sogBVal->valid ? sogBVal->value : DBL_MAX; + double hdtVal = hdtBVal->valid ? hdtBVal->value : DBL_MAX; + double hdmVal = hdmBVal->valid ? hdmBVal->value : DBL_MAX; + double varVal = varBVal->valid ? varBVal->value : DBL_MAX; + LOG_DEBUG(GwLog::DEBUG,"WindUtils:addTrueWind: AWA %.1f, AWS %.1f, COG %.1f, STW %.1f, SOG %.2f, HDT %.1f, HDM %.1f, VAR %.1f", awaBVal->value * RAD_TO_DEG, awsBVal->value * 3.6 / 1.852, cogBVal->value * RAD_TO_DEG, stwBVal->value * 3.6 / 1.852, sogBVal->value * 3.6 / 1.852, hdtBVal->value * RAD_TO_DEG, hdmBVal->value * RAD_TO_DEG, varBVal->value * RAD_TO_DEG); isCalculated = calcTrueWind(&awaVal, &awsVal, &cogVal, &stwVal, &sogVal, &hdtVal, &hdmVal, &varVal, &twd, &tws, &twa); @@ -312,9 +314,9 @@ bool WindUtils::addTrueWind(GwApi* api, BoatValueList* boatValues, GwLog* log) { twaBVal->valid = true; } } - LOG_DEBUG(GwLog::DEBUG,"obp60task addTrueWind: isCalculated %d, TWD %.1f, TWA %.1f, TWS %.1f", isCalculated, twdBVal->value * RAD_TO_DEG, + LOG_DEBUG(GwLog::DEBUG,"WindUtils:addTrueWind: isCalculated %d, TWD %.1f, TWA %.1f, TWS %.1f", isCalculated, twdBVal->value * RAD_TO_DEG, twaBVal->value * RAD_TO_DEG, twsBVal->value * 3.6 / 1.852); return isCalculated; } -// --- Class WindUtils -------------- +// --- End Class WindUtils -------------- diff --git a/lib/obp60task/OBPDataOperations.h b/lib/obp60task/OBPDataOperations.h index de33156..2eaccba 100644 --- a/lib/obp60task/OBPDataOperations.h +++ b/lib/obp60task/OBPDataOperations.h @@ -2,36 +2,66 @@ #pragma once #include "OBPRingBuffer.h" #include "obp60task.h" -#include -#include #include +#include +#include +#include class HstryBuf { private: - GwLog *logger; - RingBuffer hstry; // Circular buffer to store history values + RingBuffer hstryBuf; // Circular buffer to store history values String boatDataName; double hstryMin; double hstryMax; GwApi::BoatValue *boatValue; + GwLog *logger; - friend class HstryManager; - void handleHistory(bool useSimuData); + friend class HstryBuffers; public: - HstryBuf(const String& name, int size, GwLog* log, BoatValueList* boatValues); + HstryBuf(const String& name, int size, BoatValueList* boatValues, GwLog* log); void init(const String& format, int updFreq, int mltplr, double minVal, double maxVal); void add(double value); void handle(bool useSimuData); }; -class HstryManager { +class HstryBuffers { private: - std::map> hstryBufs; - // boat values for true wind calculation - GwApi::BoatValue *awaBVal, *hdtBVal, *hdmBVal, *varBVal, *cogBVal, *sogBVal, *awdBVal; + std::map> hstryBuffers; + int size; // size of all history buffers + BoatValueList* boatValueList; + GwLog* logger; + GwApi::BoatValue *awaBVal, *hdtBVal, *hdmBVal, *varBVal, *cogBVal, *sogBVal, *awdBVal; // boat values for true wind calculation + + struct HistoryParams { + int hstryUpdFreq; + int mltplr; + double bufferMinVal; + double bufferMaxVal; + String format; + }; + + // Define buffer parameters for each boat data type + std::map bufferParams = { + {"AWA", {1000, 10000, -M_PI, M_PI, "formatWind"}}, + {"AWD", {1000, 10000, 0.0, M_TWOPI, "formatCourse"}}, + {"AWS", {1000, 1000, 0.0, 65.0, "formatKnots"}}, + {"DBS", {1000, 100, 0.0, 650, "formatDepth"}}, + {"DBT", {1000, 100, 0.0, 650, "formatDepth"}}, + {"DPT", {1000, 100, 0.0, 650, "formatDepth"}}, + {"HDT", {1000, 10000, 0.0, M_TWOPI, "formatCourse"}}, + {"HDM", {1000, 10000, 0.0, M_TWOPI, "formatCourse"}}, + {"TWA", {1000, 10000, -M_PI, M_PI, "formatWind"}}, + {"TWD", {1000, 10000, 0.0, M_TWOPI, "formatCourse"}}, + {"TWS", {1000, 1000, 0.0, 65.0, "formatKnots"}}, + {"SOG", {1000, 1000, 0.0, 65.0, "formatKnots"}}, + {"STW", {1000, 1000, 0.0, 65.0, "formatKnots"}}, + {"WTemp", {1000, 100, 0.0, 650.0, "kelvinToC"}} + }; + public: - HstryManager(int size, GwLog* log, BoatValueList* boatValues); + HstryBuffers(int size, BoatValueList* boatValues, GwLog* log); + void addBuffer(const String& name); void handleHstryBufs(bool useSimuData); RingBuffer* getBuffer(const String& name); }; @@ -41,9 +71,11 @@ private: GwApi::BoatValue *twdBVal, *twsBVal, *twaBVal; GwApi::BoatValue *awaBVal, *awsBVal, *cogBVal, *stwBVal, *sogBVal, *hdtBVal, *hdmBVal, *varBVal; static constexpr double DBL_MAX = std::numeric_limits::max(); + GwLog* logger; public: - WindUtils(BoatValueList* boatValues){ + WindUtils(BoatValueList* boatValues, GwLog* log) + : logger(log) { twdBVal = boatValues->findValueOrCreate("TWD"); twsBVal = boatValues->findValueOrCreate("TWS"); twaBVal = boatValues->findValueOrCreate("TWA"); @@ -73,5 +105,6 @@ public: bool calcTrueWind(const double* awaVal, const double* awsVal, const double* cogVal, const double* stwVal, const double* sogVal, const double* hdtVal, const double* hdmVal, const double* varVal, double* twdVal, double* twsVal, double* twaVal); - bool addTrueWind(GwApi* api, BoatValueList* boatValues, GwLog *log); +// bool addTrueWind(GwApi* api, BoatValueList* boatValues, GwLog *log); + bool addTrueWind(); }; \ No newline at end of file diff --git a/lib/obp60task/PageWindPlot.cpp b/lib/obp60task/PageWindPlot.cpp index 1ce55b9..10d0d86 100644 --- a/lib/obp60task/PageWindPlot.cpp +++ b/lib/obp60task/PageWindPlot.cpp @@ -125,8 +125,10 @@ public: static RingBuffer* wdHstry; // Wind direction data buffer static RingBuffer* wsHstry; // Wind speed data buffer - static String wdName, wdFormat; // Wind direction name and format - static String wsName, wsFormat; // Wind speed name and format + static RingBuffer* twdHstry; // Wind direction data buffer for TWD + static RingBuffer* twsHstry; // Wind speed data buffer for TWS + static RingBuffer* awdHstry; // Wind direction data buffer for AWD + static RingBuffer* awsHstry; // Wind speed data buffer for AWS // Separate chart objects for true wind and apparent wind static std::unique_ptr> twdFlChart, awdFlChart; // chart object for wind direction chart, full size @@ -139,8 +141,8 @@ public: static Chart* wdHfChart; static Chart* wsHfChart; - static GwApi::BoatValue* wdBVal = new GwApi::BoatValue("TWD"); // temp BoatValue for wind direction unit identification; required by OBP60Formater - static GwApi::BoatValue* wsBVal = new GwApi::BoatValue("TWS"); // temp BoatValue for wind speed unit identification; required by OBP60Formater */ + static GwApi::BoatValue* wdBVal; // BoatValue for wind direction + static GwApi::BoatValue* wsBVal; // BoatValue for wind speed double dfltRngWd = 60.0 * DEG_TO_RAD; // default range for course chart from min to max value in RAD double dfltRngWs = 7.5; // default range for wind speed chart from min to max value in m/s @@ -163,24 +165,20 @@ public: if (showTruW != oldShowTruW) { if (!twdFlChart) { // Create true wind charts if they don't exist - - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Creating true wind charts"); - auto* twdHstry = pageData.hstryManager->getBuffer("TWD"); - auto* twsHstry = pageData.hstryManager->getBuffer("TWS"); + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Creating true wind charts"); + twdHstry = pageData.hstryBuffers->getBuffer("TWD"); + twsHstry = pageData.hstryBuffers->getBuffer("TWS"); twdFlChart = std::unique_ptr>(new Chart(*twdHstry, 'V', 0, dfltRngWd, *commonData, useSimuData)); twsFlChart = std::unique_ptr>(new Chart(*twsHstry, 'H', 0, dfltRngWs, *commonData, useSimuData)); twdHfChart = std::unique_ptr>(new Chart(*twdHstry, 'V', 1, dfltRngWd, *commonData, useSimuData)); twsHfChart = std::unique_ptr>(new Chart(*twsHstry, 'V', 2, dfltRngWs, *commonData, useSimuData)); - // twdHfChart = std::unique_ptr>(new Chart(*twdHstry, 'H', 1, dfltRngWd, *commonData, useSimuData)); - // twsHfChart = std::unique_ptr>(new Chart(*twsHstry, 'H', 2, dfltRngWs, *commonData, useSimuData)); - // LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: twdHstry: %p, twsHstry: %p", (void*)twdHstry, (void*)twsHstry); } if (!awdFlChart) { // Create apparent wind charts if they don't exist LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Creating apparent wind charts"); - auto* awdHstry = pageData.hstryManager->getBuffer("AWD"); - auto* awsHstry = pageData.hstryManager->getBuffer("AWS"); + awdHstry = pageData.hstryBuffers->getBuffer("AWD"); + awsHstry = pageData.hstryBuffers->getBuffer("AWS"); awdFlChart = std::unique_ptr>(new Chart(*awdHstry, 'V', 0, dfltRngWd, *commonData, useSimuData)); awsFlChart = std::unique_ptr>(new Chart(*awsHstry, 'H', 0, dfltRngWs, *commonData, useSimuData)); @@ -190,24 +188,25 @@ public: // Switch active charts based on showTruW if (showTruW) { - wdHstry = pageData.hstryManager->getBuffer("TWD"); - wsHstry = pageData.hstryManager->getBuffer("TWS"); + wdHstry = twdHstry; + wsHstry = twsHstry; wdFlChart = twdFlChart.get(); wsFlChart = twsFlChart.get(); wdHfChart = twdHfChart.get(); wsHfChart = twsHfChart.get(); + wdBVal = bvalue[0]; + wsBVal = bvalue[1]; } else { - wdHstry = pageData.hstryManager->getBuffer("AWD"); - wsHstry = pageData.hstryManager->getBuffer("AWS"); + wdHstry = awdHstry; + wsHstry = awsHstry; wdFlChart = awdFlChart.get(); wsFlChart = awsFlChart.get(); wdHfChart = awdHfChart.get(); wsHfChart = awsHfChart.get(); + wdBVal = bvalue[2]; + wsBVal = bvalue[3]; } - wdHstry->getMetaData(wdName, wdFormat); - wsHstry->getMetaData(wsName, wsFormat); - oldShowTruW = showTruW; } @@ -219,27 +218,17 @@ public: getdisplay().setTextColor(commonData->fgcolor); if (chrtMode == 'D') { - wdBVal->value = wdHstry->getLast(); - wdBVal->valid = wdBVal->value != wdHstry->getMaxVal(); - wdFlChart->showChrt(dataIntv, *bvalue[0]); + wdFlChart->showChrt(dataIntv, *wdBVal); } else if (chrtMode == 'S') { - wsBVal->value = wsHstry->getLast(); - wsBVal->valid = wsBVal->value != wsHstry->getMaxVal(); - wsFlChart->showChrt(dataIntv, *bvalue[1]); + wsFlChart->showChrt(dataIntv, *wsBVal); } else if (chrtMode == 'B') { - wdBVal->value = wdHstry->getLast(); - wdBVal->valid = wdBVal->value != wdHstry->getMaxVal(); - wsBVal->value = wsHstry->getLast(); - wsBVal->valid = wsBVal->value != wsHstry->getMaxVal(); - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot showChrt: wsBVal.name: %s, format: %s, wsBVal.value: %.1f, valid: %d, address: %p", wsBVal->getName(), wsBVal->getFormat(), wsBVal->value, - wsBVal->valid, wsBVal); - wdHfChart->showChrt(dataIntv, *bvalue[0]); - wsHfChart->showChrt(dataIntv, *bvalue[1]); + wdHfChart->showChrt(dataIntv, *wdBVal); + wsHfChart->showChrt(dataIntv, *wsBVal); } - LOG_DEBUG(GwLog::LOG, "PageWindPlot: page time %ldms", millis() - pageTime); + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: page time %ldms", millis() - pageTime); return PAGE_UPDATE; } }; diff --git a/lib/obp60task/Pagedata.h b/lib/obp60task/Pagedata.h index f40d187..a12119f 100644 --- a/lib/obp60task/Pagedata.h +++ b/lib/obp60task/Pagedata.h @@ -16,7 +16,7 @@ typedef struct{ uint8_t pageNumber; // page number in sequence of visible pages //the values will always contain the user defined values first ValueList values; - HstryManager* hstryManager; + HstryBuffers* hstryBuffers; // list of all boat history buffers } PageData; // Sensor data structure (only for extended sensors, not for NMEA bus sensors) diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index 0626a90..bddb425 100644 --- a/lib/obp60task/obp60task.cpp +++ b/lib/obp60task/obp60task.cpp @@ -433,8 +433,8 @@ void OBP60Task(GwApi *api){ int lastPage=pageNumber; BoatValueList boatValues; //all the boat values for the api query - HstryManager hstryManager(1920, logger, &boatValues); // Create and manage history buffers - WindUtils trueWind(&boatValues); // Create helper object for true wind calculation + HstryBuffers hstryBufList(1920, &boatValues, logger); // Create empty list of boat data history buffers + WindUtils trueWind(&boatValues, logger); // Create helper object for true wind calculation //commonData.distanceformat=config->getString(xxx); //add all necessary data to common data @@ -477,9 +477,18 @@ void OBP60Task(GwApi *api){ LOG_DEBUG(GwLog::DEBUG,"added fixed value %s to page %d",value->getName().c_str(),i); pages[i].parameters.values.push_back(value); } - // Add history manager to page parameters - pages[i].parameters.hstryManager = &hstryManager; + + // Read the specified boat data type of relevant pages and create a history buffer for each type + if (pages[i].parameters.pageName == "OneValue" || pages[i].parameters.pageName == "TwoValues" || pages[i].parameters.pageName == "WindPlot") { + for (auto pVal : pages[i].parameters.values) { + hstryBufList.addBuffer(pVal->getName()); + } + } + // Add list of history buffers to page parameters + pages[i].parameters.hstryBuffers = &hstryBufList; + } + // add out of band system page (always available) Page *syspage = allPages.pages[0]->creator(commonData); @@ -487,7 +496,6 @@ void OBP60Task(GwApi *api){ bool calcTrueWnds = api->getConfig()->getBool(api->getConfig()->calcTrueWnds, false); bool useSimuData = api->getConfig()->getBool(api->getConfig()->useSimuData, false); - // Initialize history buffer for certain boat data // Read all calibration data settings from config calibrationData.readConfig(config, logger); @@ -804,10 +812,10 @@ void OBP60Task(GwApi *api){ api->getStatus(commonData.status); if (calcTrueWnds) { - trueWind.addTrueWind(api, &boatValues, logger); + trueWind.addTrueWind(); } // Handle history buffers for certain boat data for windplot page and other usage - hstryManager.handleHstryBufs(useSimuData); + hstryBufList.handleHstryBufs(useSimuData); // Clear display // getdisplay().fillRect(0, 0, getdisplay().width(), getdisplay().height(), commonData.bgcolor); From 362338a7ddbea47544d25dd8933ec1afc4a0b958 Mon Sep 17 00:00:00 2001 From: Ulrich Meine Date: Sun, 21 Dec 2025 13:10:44 +0100 Subject: [PATCH 079/121] Optimized PageWindPlot code; added WindUtils AWD/TWD calculation from AWA/TWA if available --- lib/obp60task/OBPDataOperations.cpp | 208 ++++++++++++++++------------ lib/obp60task/OBPDataOperations.h | 19 +-- lib/obp60task/PageWindPlot.cpp | 141 +++++++++---------- lib/obp60task/obp60task.cpp | 2 +- 4 files changed, 200 insertions(+), 170 deletions(-) diff --git a/lib/obp60task/OBPDataOperations.cpp b/lib/obp60task/OBPDataOperations.cpp index 898ba5a..769ddbf 100644 --- a/lib/obp60task/OBPDataOperations.cpp +++ b/lib/obp60task/OBPDataOperations.cpp @@ -1,15 +1,18 @@ #include "OBPDataOperations.h" -#include "BoatDataCalibration.h" // Functions lib for data instance calibration +#include "BoatDataCalibration.h" // Functions lib for data instance calibration #include // --- Class HstryBuf --------------- HstryBuf::HstryBuf(const String& name, int size, BoatValueList* boatValues, GwLog* log) - : logger(log), boatDataName(name) { + : logger(log) + , boatDataName(name) +{ hstryBuf.resize(size); boatValue = boatValues->findValueOrCreate(name); } -void HstryBuf::init(const String& format, int updFreq, int mltplr, double minVal, double maxVal) { +void HstryBuf::init(const String& format, int updFreq, int mltplr, double minVal, double maxVal) +{ hstryBuf.setMetaData(boatDataName, format, updFreq, mltplr, minVal, maxVal); hstryMin = minVal; hstryMax = maxVal; @@ -19,26 +22,31 @@ void HstryBuf::init(const String& format, int updFreq, int mltplr, double minVal } } -void HstryBuf::add(double value) { +void HstryBuf::add(double value) +{ if (value >= hstryMin && value <= hstryMax) { hstryBuf.add(value); } } -void HstryBuf::handle(bool useSimuData) { - GwApi::BoatValue *calBVal; +void HstryBuf::handle(bool useSimuData) +{ + GwApi::BoatValue* calBVal; - if (boatValue->valid) { // add calibrated boat value to history buffer + if (boatValue->valid) { + // Calibrate boat value before adding it to history buffer calBVal = new GwApi::BoatValue(boatDataName.c_str()); calBVal->setFormat(boatValue->getFormat()); calBVal->value = boatValue->value; calBVal->valid = boatValue->valid; calibrationData.calibrateInstance(calBVal, logger); add(calBVal->value); + delete calBVal; calBVal = nullptr; + } else if (useSimuData) { // add simulated value to history buffer - double simValue = hstryBuf.getLast(); + double simValue = hstryBuf.getLast(); if (boatDataName == "TWD" || boatDataName == "AWD") { simValue += static_cast(random(-349, 349) / 1000.0); simValue = WindUtils::to2PI(simValue); @@ -53,10 +61,13 @@ void HstryBuf::handle(bool useSimuData) { // --- Class HstryBuffers --------------- HstryBuffers::HstryBuffers(int size, BoatValueList* boatValues, GwLog* log) - : size(size), boatValueList(boatValues), logger(log) { + : size(size) + , boatValueList(boatValues) + , logger(log) +{ // collect boat values for true wind calculation - // should have been already all created at true wind object initialization + // should all have been already created at true wind object initialization // potentially to be moved to history buffer handling awaBVal = boatValueList->findValueOrCreate("AWA"); hdtBVal = boatValueList->findValueOrCreate("HDT"); @@ -67,9 +78,9 @@ HstryBuffers::HstryBuffers(int size, BoatValueList* boatValues, GwLog* log) awdBVal = boatValueList->findValueOrCreate("AWD"); } -void HstryBuffers::addBuffer(const String& name) { - // Create history buffer for boat data type - +// Create history buffer for boat data type +void HstryBuffers::addBuffer(const String& name) +{ if (HstryBuffers::getBuffer(name) != nullptr) { // buffer for this data type already exists return; } @@ -77,61 +88,30 @@ void HstryBuffers::addBuffer(const String& name) { hstryBuffers[name] = std::unique_ptr(new HstryBuf(name, size, boatValueList, logger)); // Initialize metadata for buffer - // String valueFormat = boatValueList->findValueOrCreate(name)->getFormat().c_str(); // Unfortunately, format is not yet available during system initialization String valueFormat = bufferParams[name].format; // Data format of boat data type + // String valueFormat = boatValueList->findValueOrCreate(name)->getFormat().c_str(); // Unfortunately, format is not yet available during system initialization int hstryUpdFreq = bufferParams[name].hstryUpdFreq; // Update frequency for history buffers in ms int mltplr = bufferParams[name].mltplr; // default multiplier which transforms original value into buffer type format double bufferMinVal = bufferParams[name].bufferMinVal; // Min value for this history buffer double bufferMaxVal = bufferParams[name].bufferMaxVal; // Max value for this history buffer hstryBuffers[name]->init(valueFormat, hstryUpdFreq, mltplr, bufferMinVal, bufferMaxVal); - LOG_DEBUG(GwLog::DEBUG,"HstryBuffers-new buffer added: name: %s, format: %s, multiplier: %d, min value: %.2f, max value: %.2f", name, valueFormat, mltplr, bufferMinVal, bufferMaxVal); + LOG_DEBUG(GwLog::DEBUG, "HstryBuffers-new buffer added: name: %s, format: %s, multiplier: %d, min value: %.2f, max value: %.2f", name, valueFormat, mltplr, bufferMinVal, bufferMaxVal); } -// Handle history buffers for TWD, TWS, AWD, AWS -void HstryBuffers::handleHstryBufs(bool useSimuData) { - - static double hdt = 20; //initial value only relevant if we use simulation data - GwApi::BoatValue *calBVal; // temp variable just for data calibration -> we don't want to calibrate the original data here +// Handle history buffers +void HstryBuffers::handleHstryBufs(bool useSimuData) +{ // Handle all registered history buffers for (auto& pair : hstryBuffers) { auto& buf = pair.second; buf->handle(useSimuData); } - - // Special handling for AWD which is calculated - if (awaBVal->valid) { - if (hdtBVal->valid) { - hdt = hdtBVal->value; // Use HDT if available - } else { - hdt = WindUtils::calcHDT(&hdmBVal->value, &varBVal->value, &cogBVal->value, &sogBVal->value); - } - double awd; - awd = awaBVal->value + hdt; - awd = WindUtils::to2PI(awd); - calBVal = new GwApi::BoatValue("AWD"); // temporary solution for calibration of history buffer values - calBVal->value = awd; - calBVal->setFormat(awdBVal->getFormat()); - calBVal->valid = true; - // We don't have a logger here, so we pass nullptr. This should be improved. - calibrationData.calibrateInstance(calBVal, nullptr); // Check if boat data value is to be calibrated - awdBVal->value = calBVal->value; - awdBVal->valid = true; - // Find the AWD buffer and add the value. - auto it = hstryBuffers.find("AWD"); - if (it != hstryBuffers.end()) { - it->second->add(calBVal->value); - } - - delete calBVal; - calBVal = nullptr; - } else if (useSimuData) { - // Simulation for AWD is handled inside HstryBuf::handle - } } -RingBuffer* HstryBuffers::getBuffer(const String& name) { +RingBuffer* HstryBuffers::getBuffer(const String& name) +{ auto it = hstryBuffers.find(name); if (it != hstryBuffers.end()) { return &it->second->hstryBuf; @@ -204,14 +184,14 @@ void WindUtils::addPolar(const double* phi1, const double* r1, void WindUtils::calcTwdSA(const double* AWA, const double* AWS, const double* CTW, const double* STW, const double* HDT, - double* TWD, double* TWS, double* TWA) + double* TWD, double* TWS, double* TWA, double* AWD) { - double awd = *AWA + *HDT; - awd = to2PI(awd); + *AWD = *AWA + *HDT; + *AWD = to2PI(*AWD); double stw = -*STW; - addPolar(&awd, AWS, CTW, &stw, TWD, TWS); + addPolar(AWD, AWS, CTW, &stw, TWD, TWS); - // Normalize TWD and TWA to 0-360° + // Normalize TWD and TWA to 0-360°/2PI *TWD = to2PI(*TWD); *TWA = toPI(*TWD - *HDT); } @@ -233,12 +213,12 @@ double WindUtils::calcHDT(const double* hdmVal, const double* varVal, const doub return hdt; } -bool WindUtils::calcTrueWind(const double* awaVal, const double* awsVal, +bool WindUtils::calcWinds(const double* awaVal, const double* awsVal, const double* cogVal, const double* stwVal, const double* sogVal, const double* hdtVal, - const double* hdmVal, const double* varVal, double* twdVal, double* twsVal, double* twaVal) + const double* hdmVal, const double* varVal, double* twdVal, double* twsVal, double* twaVal, double* awdVal) { double stw, hdt, ctw; - double twd, tws, twa; + double twd, tws, twa, awd; double minSogVal = 0.1; // SOG below this value (m/s) is assumed to be data noise from GPS sensor if (*hdtVal != DBL_MAX) { @@ -262,31 +242,68 @@ bool WindUtils::calcTrueWind(const double* awaVal, const double* awsVal, // If STW and SOG are not available, we cannot calculate true wind return false; } -// Serial.println("\ncalcTrueWind: HDT: " + String(hdt) + ", CTW: " + String(ctw) + ", STW: " + String(stw)); + // LOG_DEBUG(GwLog::DEBUG, "WindUtils:calcWinds: HDT: %.1f, CTW %.1f, STW %.1f", hdt, ctw, stw); + if ((*awaVal == DBL_MAX) || (*awsVal == DBL_MAX)) { // Cannot calculate true wind without valid AWA, AWS; other checks are done earlier return false; } else { - calcTwdSA(awaVal, awsVal, &ctw, &stw, &hdt, &twd, &tws, &twa); + calcTwdSA(awaVal, awsVal, &ctw, &stw, &hdt, &twd, &tws, &twa, &awd); *twdVal = twd; *twsVal = tws; *twaVal = twa; + *awdVal = awd; return true; } } -// Calculate true wind data and add to obp60task boat data list -//bool WindUtils::addTrueWind(GwApi* api, GwLog* log) { -bool WindUtils::addTrueWind() { - -// GwLog* logger = log; - -// double awaVal, awsVal, cogVal, stwVal, sogVal, hdtVal, hdmVal, varVal; - double twd, tws, twa; +/* // we don't need this -> AWD is calculated in calcTwdSA +// Calc AWD from existing AWA and HDT/HDM +bool WindUtils::calcATWD(const double* waVal, const double* hdtVal, const double* hdmVal, const double* varVal, const double* cogVal, const double* sogVal, double* wdVal) +{ + double wd, hdt; + GwApi::BoatValue* calBVal; // temp variable just for data calibration bool isCalculated = false; + if (*waVal == DBL_MAX) { + return false; + } + + if (*hdtVal != DBL_MAX) { + hdt = *hdtVal; // Use HDT if available + } else { + hdt = calcHDT(hdmVal, varVal, cogVal, sogVal); + } + + if (hdt != DBL_MAX) { + wd = *waVal + hdt; + wd = to2PI(wd); + isCalculated = true; + } + + // Calibrate AWD/TWD if required + calBVal = new GwApi::BoatValue("AWD"); // temporary solution for calibration of history buffer values + calBVal->value = wd; + calBVal->setFormat(awdBVal->getFormat()); + calBVal->valid = true; + calibrationData.calibrateInstance(calBVal, logger); // Check if boat data value is to be calibrated + *wdVal = calBVal->value; + + delete calBVal; + calBVal = nullptr; + + return isCalculated; +} */ + +// Calculate true wind data and add to obp60task boat data list +bool WindUtils::addWinds() +{ + double twd, tws, twa, awd, hdt; + bool twCalculated = false; + bool awdCalculated = false; + double awaVal = awaBVal->valid ? awaBVal->value : DBL_MAX; double awsVal = awsBVal->valid ? awsBVal->value : DBL_MAX; double cogVal = cogBVal->valid ? cogBVal->value : DBL_MAX; @@ -295,28 +312,49 @@ bool WindUtils::addTrueWind() { double hdtVal = hdtBVal->valid ? hdtBVal->value : DBL_MAX; double hdmVal = hdmBVal->valid ? hdmBVal->value : DBL_MAX; double varVal = varBVal->valid ? varBVal->value : DBL_MAX; - LOG_DEBUG(GwLog::DEBUG,"WindUtils:addTrueWind: AWA %.1f, AWS %.1f, COG %.1f, STW %.1f, SOG %.2f, HDT %.1f, HDM %.1f, VAR %.1f", awaBVal->value * RAD_TO_DEG, awsBVal->value * 3.6 / 1.852, - cogBVal->value * RAD_TO_DEG, stwBVal->value * 3.6 / 1.852, sogBVal->value * 3.6 / 1.852, hdtBVal->value * RAD_TO_DEG, hdmBVal->value * RAD_TO_DEG, varBVal->value * RAD_TO_DEG); + LOG_DEBUG(GwLog::DEBUG, "WindUtils:addWinds: AWA %.1f, AWS %.1f, COG %.1f, STW %.1f, SOG %.2f, HDT %.1f, HDM %.1f, VAR %.1f", awaBVal->value * RAD_TO_DEG, awsBVal->value * 3.6 / 1.852, + cogBVal->value * RAD_TO_DEG, stwBVal->value * 3.6 / 1.852, sogBVal->value * 3.6 / 1.852, hdtBVal->value * RAD_TO_DEG, hdmBVal->value * RAD_TO_DEG, varBVal->value * RAD_TO_DEG); - isCalculated = calcTrueWind(&awaVal, &awsVal, &cogVal, &stwVal, &sogVal, &hdtVal, &hdmVal, &varVal, &twd, &tws, &twa); - - if (isCalculated) { // Replace values only, if successfully calculated and not already available + // Check if TWD can be calculated from TWA and HDT/HDM + if (twaBVal->valid) { if (!twdBVal->valid) { + if (hdtVal != DBL_MAX) { + hdt = hdtVal; // Use HDT if available + } else { + hdt = calcHDT(&hdmVal, &varVal, &cogVal, &sogVal); + } + twd = twaBVal->value + hdt; + twd = to2PI(twd); twdBVal->value = twd; twdBVal->valid = true; } - if (!twsBVal->valid) { - twsBVal->value = tws; - twsBVal->valid = true; - } - if (!twaBVal->valid) { - twaBVal->value = twa; - twaBVal->valid = true; + + } else { + // Calculate true winds and AWD; if true winds exist, use at least AWD calculation + twCalculated = calcWinds(&awaVal, &awsVal, &cogVal, &stwVal, &sogVal, &hdtVal, &hdmVal, &varVal, &twd, &tws, &twa, &awd); + + if (twCalculated) { // Replace values only, if successfully calculated and not already available + if (!twdBVal->valid) { + twdBVal->value = twd; + twdBVal->valid = true; + } + if (!twsBVal->valid) { + twsBVal->value = tws; + twsBVal->valid = true; + } + if (!twaBVal->valid) { + twaBVal->value = twa; + twaBVal->valid = true; + } + if (!awdBVal->valid) { + awdBVal->value = awd; + awdBVal->valid = true; + } } } - LOG_DEBUG(GwLog::DEBUG,"WindUtils:addTrueWind: isCalculated %d, TWD %.1f, TWA %.1f, TWS %.1f", isCalculated, twdBVal->value * RAD_TO_DEG, - twaBVal->value * RAD_TO_DEG, twsBVal->value * 3.6 / 1.852); + LOG_DEBUG(GwLog::DEBUG, "WindUtils:addWinds: twCalculated %d, TWD %.1f, TWA %.1f, TWS %.2f kn, AWD: %.1f", twCalculated, twdBVal->value * RAD_TO_DEG, + twaBVal->value * RAD_TO_DEG, twsBVal->value * 3.6 / 1.852, awdBVal->value * RAD_TO_DEG); - return isCalculated; + return twCalculated; } -// --- End Class WindUtils -------------- +// --- End Class WindUtils -------------- \ No newline at end of file diff --git a/lib/obp60task/OBPDataOperations.h b/lib/obp60task/OBPDataOperations.h index 2eaccba..0a4faaf 100644 --- a/lib/obp60task/OBPDataOperations.h +++ b/lib/obp60task/OBPDataOperations.h @@ -68,19 +68,20 @@ public: class WindUtils { private: - GwApi::BoatValue *twdBVal, *twsBVal, *twaBVal; - GwApi::BoatValue *awaBVal, *awsBVal, *cogBVal, *stwBVal, *sogBVal, *hdtBVal, *hdmBVal, *varBVal; + GwApi::BoatValue *twaBVal, *twsBVal, *twdBVal; + GwApi::BoatValue *awaBVal, *awsBVal, *awdBVal, *cogBVal, *stwBVal, *sogBVal, *hdtBVal, *hdmBVal, *varBVal; static constexpr double DBL_MAX = std::numeric_limits::max(); GwLog* logger; public: WindUtils(BoatValueList* boatValues, GwLog* log) : logger(log) { - twdBVal = boatValues->findValueOrCreate("TWD"); - twsBVal = boatValues->findValueOrCreate("TWS"); twaBVal = boatValues->findValueOrCreate("TWA"); + twsBVal = boatValues->findValueOrCreate("TWS"); + twdBVal = boatValues->findValueOrCreate("TWD"); awaBVal = boatValues->findValueOrCreate("AWA"); awsBVal = boatValues->findValueOrCreate("AWS"); + awdBVal = boatValues->findValueOrCreate("AWD"); cogBVal = boatValues->findValueOrCreate("COG"); stwBVal = boatValues->findValueOrCreate("STW"); sogBVal = boatValues->findValueOrCreate("SOG"); @@ -100,11 +101,11 @@ public: double* phi, double* r); void calcTwdSA(const double* AWA, const double* AWS, const double* CTW, const double* STW, const double* HDT, - double* TWD, double* TWS, double* TWA); + double* TWD, double* TWS, double* TWA, double* AWD); static double calcHDT(const double* hdmVal, const double* varVal, const double* cogVal, const double* sogVal); - bool calcTrueWind(const double* awaVal, const double* awsVal, + bool calcWinds(const double* awaVal, const double* awsVal, const double* cogVal, const double* stwVal, const double* sogVal, const double* hdtVal, - const double* hdmVal, const double* varVal, double* twdVal, double* twsVal, double* twaVal); -// bool addTrueWind(GwApi* api, BoatValueList* boatValues, GwLog *log); - bool addTrueWind(); + const double* hdmVal, const double* varVal, double* twdVal, double* twsVal, double* twaVal, double* awdVal); + bool calcATWD(const double* waVal, const double* hdtVal, const double* hdmVal, const double* varVal, const double* cogVal, const double* sogVal, double* wdVal); + bool addWinds(); }; \ No newline at end of file diff --git a/lib/obp60task/PageWindPlot.cpp b/lib/obp60task/PageWindPlot.cpp index 10d0d86..636d8f9 100644 --- a/lib/obp60task/PageWindPlot.cpp +++ b/lib/obp60task/PageWindPlot.cpp @@ -25,6 +25,33 @@ private: String flashLED; String backlightMode; +#ifdef BOARD_OBP40S3 + String wndSrc; // Wind source true/apparent wind - preselection for OBP40 +#endif + + // Data buffers pointers (owned by HstryBuffers) + RingBuffer* twdHstry = nullptr; + RingBuffer* twsHstry = nullptr; + RingBuffer* awdHstry = nullptr; + RingBuffer* awsHstry = nullptr; + + // Chart objects + std::unique_ptr> twdFlChart, awdFlChart; // Chart object for wind direction, full size + std::unique_ptr> twsFlChart, awsFlChart; // Chart object for wind speed, full size + std::unique_ptr> twdHfChart, awdHfChart; // Chart object for wind direction, half size + std::unique_ptr> twsHfChart, awsHfChart; // Chart object for wind speed, half size + + // Active charts and values + Chart* wdFlChart = nullptr; + Chart* wsFlChart = nullptr; + Chart* wdHfChart = nullptr; + Chart* wsHfChart = nullptr; + GwApi::BoatValue* wdBVal = nullptr; + GwApi::BoatValue* wsBVal = nullptr; + + const double dfltRngWd = 60.0 * DEG_TO_RAD; // default range for course chart from min to max value in RAD + const double dfltRngWs = 7.5; // default range for wind speed chart from min to max value in m/s + public: PageWindPlot(CommonData& common) { @@ -32,11 +59,16 @@ public: logger = commonData->logger; LOG_DEBUG(GwLog::LOG, "Instantiate PageWindPlot"); + width = getdisplay().width(); // Screen width + height = getdisplay().height(); // Screen height + // Get config data useSimuData = common.config->getBool(common.config->useSimuData); // holdValues = common.config->getBool(common.config->holdvalues); flashLED = common.config->getString(common.config->flashLED); backlightMode = common.config->getString(common.config->backlight); + + oldShowTruW = !showTruW; // makes wind source being initialized at initial page call } virtual void setupKeys() @@ -102,111 +134,70 @@ public: virtual void displayNew(PageData& pageData) { +#ifdef BOARD_OBP60S3 + // Clear optical warning + if (flashLED == "Limit Violation") { + setBlinkingLED(false); + setFlashLED(false); + } +#endif #ifdef BOARD_OBP40S3 - String wndSrc; // Wind source true/apparent wind - preselection for OBP40 - wndSrc = commonData->config->getString("page" + String(pageData.pageNumber) + "wndsrc"); if (wndSrc == "True wind") { showTruW = true; } else { showTruW = false; // Wind source is apparent wind } - LOG_DEBUG(GwLog::LOG, "New PageWindPlot; wind source=%s", wndSrc); + oldShowTruW = !showTruW; // Force chart update in displayPage #endif - oldShowTruW = !showTruW; // makes wind source being initialized at initial page call - width = getdisplay().width(); // Screen width - height = getdisplay().height(); // Screen height + if (!twdFlChart) { // Create true wind charts if they don't exist + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Creating true wind charts"); + twdHstry = pageData.hstryBuffers->getBuffer("TWD"); + twsHstry = pageData.hstryBuffers->getBuffer("TWS"); + + twdFlChart.reset(new Chart(*twdHstry, 'V', 0, dfltRngWd, *commonData, useSimuData)); + twsFlChart.reset(new Chart(*twsHstry, 'H', 0, dfltRngWs, *commonData, useSimuData)); + twdHfChart.reset(new Chart(*twdHstry, 'V', 1, dfltRngWd, *commonData, useSimuData)); + twsHfChart.reset(new Chart(*twsHstry, 'V', 2, dfltRngWs, *commonData, useSimuData)); + } + + if (!awdFlChart) { // Create apparent wind charts if they don't exist + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Creating apparent wind charts"); + awdHstry = pageData.hstryBuffers->getBuffer("AWD"); + awsHstry = pageData.hstryBuffers->getBuffer("AWS"); + + awdFlChart.reset(new Chart(*awdHstry, 'V', 0, dfltRngWd, *commonData, useSimuData)); + awsFlChart.reset(new Chart(*awsHstry, 'H', 0, dfltRngWs, *commonData, useSimuData)); + awdHfChart.reset(new Chart(*awdHstry, 'V', 1, dfltRngWd, *commonData, useSimuData)); + awsHfChart.reset(new Chart(*awsHstry, 'V', 2, dfltRngWs, *commonData, useSimuData)); + } } int displayPage(PageData& pageData) { - GwConfigHandler* config = commonData->config; - - static RingBuffer* wdHstry; // Wind direction data buffer - static RingBuffer* wsHstry; // Wind speed data buffer - static RingBuffer* twdHstry; // Wind direction data buffer for TWD - static RingBuffer* twsHstry; // Wind speed data buffer for TWS - static RingBuffer* awdHstry; // Wind direction data buffer for AWD - static RingBuffer* awsHstry; // Wind speed data buffer for AWS - - // Separate chart objects for true wind and apparent wind - static std::unique_ptr> twdFlChart, awdFlChart; // chart object for wind direction chart, full size - static std::unique_ptr> twsFlChart, awsFlChart; // chart object for wind speed chart, full size - static std::unique_ptr> twdHfChart, awdHfChart; // chart object for wind direction chart, half size - static std::unique_ptr> twsHfChart, awsHfChart; // chart object for wind speed chart, half size - // Pointers to the currently active charts - static Chart* wdFlChart; - static Chart* wsFlChart; - static Chart* wdHfChart; - static Chart* wsHfChart; - - static GwApi::BoatValue* wdBVal; // BoatValue for wind direction - static GwApi::BoatValue* wsBVal; // BoatValue for wind speed - double dfltRngWd = 60.0 * DEG_TO_RAD; // default range for course chart from min to max value in RAD - double dfltRngWs = 7.5; // default range for wind speed chart from min to max value in m/s - - const int numBoatData = 4; - GwApi::BoatValue* bvalue[numBoatData]; // current boat data values - LOG_DEBUG(GwLog::LOG, "Display PageWindPlot"); ulong pageTime = millis(); - // read boat data values - for (int i = 0; i < numBoatData; i++) { - bvalue[i] = pageData.values[i]; - } - - // Optical warning by limit violation (unused) - if (String(flashLED) == "Limit Violation") { - setBlinkingLED(false); - setFlashLED(false); - } - if (showTruW != oldShowTruW) { - if (!twdFlChart) { // Create true wind charts if they don't exist - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Creating true wind charts"); - twdHstry = pageData.hstryBuffers->getBuffer("TWD"); - twsHstry = pageData.hstryBuffers->getBuffer("TWS"); - twdFlChart = std::unique_ptr>(new Chart(*twdHstry, 'V', 0, dfltRngWd, *commonData, useSimuData)); - twsFlChart = std::unique_ptr>(new Chart(*twsHstry, 'H', 0, dfltRngWs, *commonData, useSimuData)); - twdHfChart = std::unique_ptr>(new Chart(*twdHstry, 'V', 1, dfltRngWd, *commonData, useSimuData)); - twsHfChart = std::unique_ptr>(new Chart(*twsHstry, 'V', 2, dfltRngWs, *commonData, useSimuData)); - } - - if (!awdFlChart) { // Create apparent wind charts if they don't exist - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Creating apparent wind charts"); - awdHstry = pageData.hstryBuffers->getBuffer("AWD"); - awsHstry = pageData.hstryBuffers->getBuffer("AWS"); - - awdFlChart = std::unique_ptr>(new Chart(*awdHstry, 'V', 0, dfltRngWd, *commonData, useSimuData)); - awsFlChart = std::unique_ptr>(new Chart(*awsHstry, 'H', 0, dfltRngWs, *commonData, useSimuData)); - awdHfChart = std::unique_ptr>(new Chart(*awdHstry, 'V', 1, dfltRngWd, *commonData, useSimuData)); - awsHfChart = std::unique_ptr>(new Chart(*awsHstry, 'V', 2, dfltRngWs, *commonData, useSimuData)); - } - // Switch active charts based on showTruW if (showTruW) { - wdHstry = twdHstry; - wsHstry = twsHstry; wdFlChart = twdFlChart.get(); wsFlChart = twsFlChart.get(); wdHfChart = twdHfChart.get(); wsHfChart = twsHfChart.get(); - wdBVal = bvalue[0]; - wsBVal = bvalue[1]; + wdBVal = pageData.values[0]; + wsBVal = pageData.values[1]; } else { - wdHstry = awdHstry; - wsHstry = awsHstry; wdFlChart = awdFlChart.get(); wsFlChart = awsFlChart.get(); wdHfChart = awdHfChart.get(); wsHfChart = awsHfChart.get(); - wdBVal = bvalue[2]; - wsBVal = bvalue[3]; + wdBVal = pageData.values[2]; + wsBVal = pageData.values[3]; } - + oldShowTruW = showTruW; } diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index bddb425..4689a10 100644 --- a/lib/obp60task/obp60task.cpp +++ b/lib/obp60task/obp60task.cpp @@ -812,7 +812,7 @@ void OBP60Task(GwApi *api){ api->getStatus(commonData.status); if (calcTrueWnds) { - trueWind.addTrueWind(); + trueWind.addWinds(); } // Handle history buffers for certain boat data for windplot page and other usage hstryBufList.handleHstryBufs(useSimuData); From 2deaf07ea4747efe23e141e5a53254cb243bf3c9 Mon Sep 17 00:00:00 2001 From: TobiasE-github Date: Tue, 23 Dec 2025 15:46:48 +0100 Subject: [PATCH 080/121] force a blank digit in front of two-digit numbers --- lib/obp60task/OBP60Formatter.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/obp60task/OBP60Formatter.cpp b/lib/obp60task/OBP60Formatter.cpp index f51c5fa..44ce771 100644 --- a/lib/obp60task/OBP60Formatter.cpp +++ b/lib/obp60task/OBP60Formatter.cpp @@ -79,9 +79,13 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ const char* fmt_dec_10; const char* fmt_dec_100; if (precision == "1") { - fmt_dec_1 = "%3.1f"; - fmt_dec_10 = "%3.0f"; - fmt_dec_100 = "%3.0f"; + // + //All values are displayed using a DSEG7* font. In this font, ' ' is a very short space, and '.' takes up no space at all. + //For a space that is as long as a number, '!' is used. For details see https://www.keshikan.net/fonts-e.html + // + fmt_dec_1 = "!%1.1f"; //insert a blank digit and then display a two-digit number + fmt_dec_10 = "!%2.0f"; //insert a blank digit and then display a two-digit number + fmt_dec_100 = "%3.0f"; //dispay a three digit number } else { fmt_dec_1 = "%3.2f"; fmt_dec_10 = "%3.1f"; From 69754b85fd619fc36ceddea6ba73867974bb3e6b Mon Sep 17 00:00:00 2001 From: Ulrich Meine Date: Tue, 23 Dec 2025 18:16:53 +0100 Subject: [PATCH 081/121] optimized chart initialization for PageWindPlot; added chart options to PageOneValue; printing of current boat value on horizontal half charts is selectable; fixed value axis direction for depth and other boat data; changed time axis labels to full numbers; changed "INTV" button label to "ZOOM" --- lib/obp60task/OBP60Extensions.cpp | 2 +- lib/obp60task/OBPDataOperations.cpp | 38 ---- lib/obp60task/OBPDataOperations.h | 18 +- lib/obp60task/OBPcharts.cpp | 206 ++++++++++--------- lib/obp60task/OBPcharts.h | 26 ++- lib/obp60task/PageOneValue.cpp | 304 ++++++++++++++++++++-------- lib/obp60task/PageWindPlot.cpp | 13 +- 7 files changed, 364 insertions(+), 243 deletions(-) diff --git a/lib/obp60task/OBP60Extensions.cpp b/lib/obp60task/OBP60Extensions.cpp index e45c90b..de153db 100644 --- a/lib/obp60task/OBP60Extensions.cpp +++ b/lib/obp60task/OBP60Extensions.cpp @@ -434,7 +434,7 @@ void drawTextRalign(int16_t x, int16_t y, String text) { int16_t x1, y1; uint16_t w, h; getdisplay().getTextBounds(text, 0, 150, &x1, &y1, &w, &h); - getdisplay().setCursor(x - w, y); + getdisplay().setCursor(x - w - 1, y); // '-1' required since some strings wrap around w/o it getdisplay().print(text); } diff --git a/lib/obp60task/OBPDataOperations.cpp b/lib/obp60task/OBPDataOperations.cpp index 769ddbf..3d81fe0 100644 --- a/lib/obp60task/OBPDataOperations.cpp +++ b/lib/obp60task/OBPDataOperations.cpp @@ -259,44 +259,6 @@ bool WindUtils::calcWinds(const double* awaVal, const double* awsVal, } } -/* // we don't need this -> AWD is calculated in calcTwdSA -// Calc AWD from existing AWA and HDT/HDM -bool WindUtils::calcATWD(const double* waVal, const double* hdtVal, const double* hdmVal, const double* varVal, const double* cogVal, const double* sogVal, double* wdVal) -{ - double wd, hdt; - GwApi::BoatValue* calBVal; // temp variable just for data calibration - bool isCalculated = false; - - if (*waVal == DBL_MAX) { - return false; - } - - if (*hdtVal != DBL_MAX) { - hdt = *hdtVal; // Use HDT if available - } else { - hdt = calcHDT(hdmVal, varVal, cogVal, sogVal); - } - - if (hdt != DBL_MAX) { - wd = *waVal + hdt; - wd = to2PI(wd); - isCalculated = true; - } - - // Calibrate AWD/TWD if required - calBVal = new GwApi::BoatValue("AWD"); // temporary solution for calibration of history buffer values - calBVal->value = wd; - calBVal->setFormat(awdBVal->getFormat()); - calBVal->valid = true; - calibrationData.calibrateInstance(calBVal, logger); // Check if boat data value is to be calibrated - *wdVal = calBVal->value; - - delete calBVal; - calBVal = nullptr; - - return isCalculated; -} */ - // Calculate true wind data and add to obp60task boat data list bool WindUtils::addWinds() { diff --git a/lib/obp60task/OBPDataOperations.h b/lib/obp60task/OBPDataOperations.h index 0a4faaf..53cae41 100644 --- a/lib/obp60task/OBPDataOperations.h +++ b/lib/obp60task/OBPDataOperations.h @@ -3,9 +3,6 @@ #include "OBPRingBuffer.h" #include "obp60task.h" #include -#include -#include -#include class HstryBuf { private: @@ -46,16 +43,18 @@ private: {"AWA", {1000, 10000, -M_PI, M_PI, "formatWind"}}, {"AWD", {1000, 10000, 0.0, M_TWOPI, "formatCourse"}}, {"AWS", {1000, 1000, 0.0, 65.0, "formatKnots"}}, - {"DBS", {1000, 100, 0.0, 650, "formatDepth"}}, - {"DBT", {1000, 100, 0.0, 650, "formatDepth"}}, - {"DPT", {1000, 100, 0.0, 650, "formatDepth"}}, - {"HDT", {1000, 10000, 0.0, M_TWOPI, "formatCourse"}}, + {"COG", {1000, 10000, 0.0, M_TWOPI, "formatCourse"}}, + {"DBS", {1000, 100, 0.0, 650.0, "formatDepth"}}, + {"DBT", {1000, 100, 0.0, 650.0, "formatDepth"}}, + {"DPT", {1000, 100, 0.0, 650.0, "formatDepth"}}, {"HDM", {1000, 10000, 0.0, M_TWOPI, "formatCourse"}}, + {"HDT", {1000, 10000, 0.0, M_TWOPI, "formatCourse"}}, + {"ROT", {1000, 10000, -M_PI / 180.0 * 99.0, M_PI / 180.0 * 99.0, "formatRot"}}, // min/max is -/+ 99 degrees for rotational angle + {"SOG", {1000, 1000, 0.0, 65.0, "formatKnots"}}, + {"STW", {1000, 1000, 0.0, 65.0, "formatKnots"}}, {"TWA", {1000, 10000, -M_PI, M_PI, "formatWind"}}, {"TWD", {1000, 10000, 0.0, M_TWOPI, "formatCourse"}}, {"TWS", {1000, 1000, 0.0, 65.0, "formatKnots"}}, - {"SOG", {1000, 1000, 0.0, 65.0, "formatKnots"}}, - {"STW", {1000, 1000, 0.0, 65.0, "formatKnots"}}, {"WTemp", {1000, 100, 0.0, 650.0, "kelvinToC"}} }; @@ -106,6 +105,5 @@ public: bool calcWinds(const double* awaVal, const double* awsVal, const double* cogVal, const double* stwVal, const double* sogVal, const double* hdtVal, const double* hdmVal, const double* varVal, double* twdVal, double* twsVal, double* twaVal, double* awdVal); - bool calcATWD(const double* waVal, const double* hdtVal, const double* hdmVal, const double* varVal, const double* cogVal, const double* sogVal, double* wdVal); bool addWinds(); }; \ No newline at end of file diff --git a/lib/obp60task/OBPcharts.cpp b/lib/obp60task/OBPcharts.cpp index b78bc6e..97bd71a 100644 --- a/lib/obp60task/OBPcharts.cpp +++ b/lib/obp60task/OBPcharts.cpp @@ -4,6 +4,13 @@ #include "OBPRingBuffer.h" // --- Class Chart --------------- + +// Chart - object holding the actual chart, incl. data buffer and format definition +// Parameters: chart timeline direction: 'H' = horizontal, 'V' = vertical; +// chart size: [0] = full size, [1] = half size left/top, [2] half size right/bottom; +// default range of chart, e.g. 30 = [0..30]; +// common program data; required for logger and color data +// flag to indicate if simulation data is active template Chart::Chart(RingBuffer& dataBuf, char chrtDir, int8_t chrtSz, double dfltRng, CommonData& common, bool useSimuData) : dataBuf(dataBuf) @@ -23,7 +30,7 @@ Chart::Chart(RingBuffer& dataBuf, char chrtDir, int8_t chrtSz, double dflt if (chrtDir == 'H') { // horizontal chart timeline direction - timAxis = dWidth; + timAxis = dWidth - 1; switch (chrtSz) { case 0: valAxis = dHeight - top - bottom; @@ -41,12 +48,13 @@ Chart::Chart(RingBuffer& dataBuf, char chrtDir, int8_t chrtSz, double dflt LOG_DEBUG(GwLog::ERROR, "displayChart: wrong init parameter"); return; } + } else if (chrtDir == 'V') { // vertical chart timeline direction timAxis = dHeight - top - bottom; switch (chrtSz) { case 0: - valAxis = dWidth; + valAxis = dWidth - 1; cStart = { 0, top - 1 }; break; case 1: @@ -106,21 +114,39 @@ Chart::~Chart() } // Perform all actions to draw chart -// Parameters are chart time interval, and the current boat data value to be printed +// Parameters: chart time interval, current boat data value to be printed, current boat data shall be shown yes/no template -void Chart::showChrt(int8_t chrtIntv, GwApi::BoatValue currValue) +void Chart::showChrt(int8_t chrtIntv, GwApi::BoatValue currValue, bool showCurrValue) { drawChrt(chrtIntv, currValue); drawChrtTimeAxis(chrtIntv); drawChrtValAxis(); if (bufDataValid) { - // uses BoatValue temp variable to format latest buffer value - // doesn't work unfortunately when 'simulation data' is active, because OBP60Formatter generates own simulation value in that case - currValue.value = dataBuf.getLast(); - currValue.valid = currValue.value != dbMAX_VAL; - Chart::prntCurrValue(currValue); - LOG_DEBUG(GwLog::DEBUG, "Chart drawChrt: currValue-value: %.1f, Valid: %d, Name: %s, Address: %p", currValue.value, currValue.valid, currValue.getName(), (void*)&currValue); + if (showCurrValue) { + // uses BoatValue temp variable to format latest buffer value + // doesn't work unfortunately when 'simulation data' is active, because OBP60Formatter generates own simulation value in that case + currValue.value = dataBuf.getLast(); + currValue.valid = currValue.value != dbMAX_VAL; + Chart::prntCurrValue(currValue); + LOG_DEBUG(GwLog::DEBUG, "OBPcharts showChrt: currValue-value: %.1f, Valid: %d, Name: %s, Address: %p", currValue.value, currValue.valid, currValue.getName(), (void*)&currValue); + } + + } else { // No valid data available -> print message + getdisplay().setFont(&Ubuntu_Bold10pt8b); + + int pX, pY; + if (chrtDir == 'H') { + pX = cStart.x + (timAxis / 2); + pY = cStart.y + (valAxis / 2) - 10; + } else { + pX = cStart.x + (valAxis / 2); + pY = cStart.y + (timAxis / 2) - 10; + } + + getdisplay().fillRect(pX - 37, pY - 10, 78, 24, bgColor); // Clear area for message + drawTextCenter(pX, pY, "No data"); + LOG_DEBUG(GwLog::LOG, "Page chart: No valid data available"); } } @@ -131,7 +157,6 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) double chrtVal; // Current data value double chrtScl; // Scale for data values in pixels per value static double chrtPrevVal; // Last data value in chart area - // bool bufDataValid = false; // Flag to indicate if buffer data is valid static int numNoData; // Counter for multiple invalid data values in a row int x, y; // x and y coordinates for drawing @@ -144,7 +169,6 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) if (chrtIntv != oldChrtIntv || count == 1) { // new data interval selected by user; this is only x * 230 values instead of 240 seconds (4 minutes) per interval step - // intvBufSize = timAxis * chrtIntv; // obsolete numBufVals = min(count, (timAxis - 60) * chrtIntv); // keep free or release 60 values on chart for plotting of new values bufStart = max(0, count - numBufVals); lastAddedIdx = currIdx; @@ -186,7 +210,8 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) if (chrtDir == 'H') { // horizontal chart x = cStart.x + i; // Position in chart area - if (chrtDataFmt == 'S') { + + if (chrtDataFmt == 'S') { // speed data format -> print low values at bottom // y = cStart.y + static_cast(((chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round y = cStart.y + valAxis - static_cast(((chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round } else if (chrtDataFmt == 'D') { @@ -194,8 +219,10 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) } else { // degree type value y = cStart.y + static_cast((WindUtils::to2PI(chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round } + } else { // vertical chart y = cStart.y + timAxis - i; // Position in chart area + if (chrtDataFmt == 'S' || chrtDataFmt == 'D') { x = cStart.x + static_cast(((chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round } else { // degree type value @@ -266,27 +293,11 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) if (chrtDataFmt == 'W') { // degree of course or wind recalcRngCntr = true; - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot FreeTop: timAxis: %d, i: %d, bufStart: %d, numBufVals: %d, recalcRngCntr: %d", timAxis, i, bufStart, numBufVals, recalcRngCntr); + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: chart end: timAxis: %d, i: %d, bufStart: %d, numBufVals: %d, recalcRngCntr: %d", timAxis, i, bufStart, numBufVals, recalcRngCntr); } break; } } - } else { - // No valid data available - getdisplay().setFont(&Ubuntu_Bold10pt8b); - - int pX, pY; - if (chrtDir == 'H') { - pX = cStart.x + (timAxis / 2); - pY = cStart.y + (valAxis / 2) - 10; - } else { - pX = cStart.x + (valAxis / 2); - pY = cStart.y + (timAxis / 2) - 10; - } - - getdisplay().fillRect(pX - 33, pY - 10, 66, 24, bgColor); // Clear area for message - drawTextCenter(pX, pY, "No data"); - LOG_DEBUG(GwLog::LOG, "PageWindPlot: No valid data available"); } } @@ -330,38 +341,8 @@ double Chart::getRng(double center, size_t amount) template void Chart::calcChrtBorders(double& rngMid, double& rngMin, double& rngMax, double& rng) { - if (chrtDataFmt == 'S' || chrtDataFmt == 'D') { - // Chart data is of any type but 'degree' - - double oldRngMin = rngMin; - double oldRngMax = rngMax; - - // Chart starts at lowest range value, but at least '0' or includes even negative values - double currMinVal = dataBuf.getMin(numBufVals); - LOG_DEBUG(GwLog::DEBUG, "calcChrtRange0a: currMinVal: %.1f, currMaxVal: %.1f, rngMin: %.1f, rngMid: %.1f, rngMax: %.1f, rng: %.1f, rngStep: %.1f, oldRngMin: %.1f, oldRngMax: %.1f, dfltRng: %.1f, numBufVals: %d", - currMinVal, dataBuf.getMax(numBufVals), rngMin, rngMid, rngMax, rng, rngStep, oldRngMin, oldRngMax, dfltRng, numBufVals); - - if (currMinVal != dbMAX_VAL) { // current min value is valid - if (currMinVal > 0 && dbMIN_VAL == 0) { // Chart range starts at least at '0' or includes negative values - rngMin = 0; - } else if (currMinVal < oldRngMin || (oldRngMin < 0 && (currMinVal > (oldRngMin + rngStep)))) { // decrease rngMin if required or increase if lowest value is higher than old rngMin - rngMin = std::floor(currMinVal / rngStep) * rngStep; - } - } // otherwise keep rngMin unchanged - - double currMaxVal = dataBuf.getMax(numBufVals); - if (currMaxVal != dbMAX_VAL) { // current max value is valid - if ((currMaxVal > oldRngMax) || (currMaxVal < (oldRngMax - rngStep))) { // increase rngMax if required or decrease if lowest value is lower than old rngMax - rngMax = std::ceil(currMaxVal / rngStep) * rngStep; - rngMax = std::max(rngMax, rngMin + dfltRng); // keep at least default chart range - } - } // otherwise keep rngMax unchanged - - rngMid = (rngMin + rngMax) / 2.0; - rng = rngMax - rngMin; - LOG_DEBUG(GwLog::DEBUG, "calcChrtRange1a: currMinVal: %.1f, currMaxVal: %.1f, rngMin: %.1f, rngMid: %.1f, rngMax: %.1f, rng: %.1f, rngStep: %.1f, oldRngMin: %.1f, oldRngMax: %.1f, dfltRng: %.1f, numBufVals: %d", - currMinVal, currMaxVal, rngMin, rngMid, rngMax, rng, rngStep, oldRngMin, oldRngMax, dfltRng, numBufVals); - } else { + if (chrtDataFmt == 'W' || chrtDataFmt == 'R') { + // Chart data is of type 'course', 'wind' or 'rot' if (chrtDataFmt == 'W') { // Chart data is of type 'course' or 'wind' @@ -416,8 +397,38 @@ void Chart::calcChrtBorders(double& rngMid, double& rngMin, double& rngMax, d // LOG_DEBUG(GwLog::DEBUG, "calcChrtRange2: diffRng: %.1f°, halfRng: %.1f°", diffRng * RAD_TO_DEG, halfRng * RAD_TO_DEG); rng = halfRng * 2.0; - LOG_DEBUG(GwLog::DEBUG, "calcChrtRange2b: rngMid: %.1f°, rngMin: %.1f°, rngMax: %.1f°, diffRng: %.1f°, rng: %.1f°, rngStep: %.1f°", rngMid * RAD_TO_DEG, rngMin * RAD_TO_DEG, rngMax * RAD_TO_DEG, - diffRng * RAD_TO_DEG, rng * RAD_TO_DEG, rngStep * RAD_TO_DEG); + // LOG_DEBUG(GwLog::DEBUG, "calcChrtRange2b: rngMid: %.1f°, rngMin: %.1f°, rngMax: %.1f°, diffRng: %.1f°, rng: %.1f°, rngStep: %.1f°", rngMid * RAD_TO_DEG, rngMin * RAD_TO_DEG, rngMax * RAD_TO_DEG, + // diffRng * RAD_TO_DEG, rng * RAD_TO_DEG, rngStep * RAD_TO_DEG); + + } else { + double oldRngMin = rngMin; + double oldRngMax = rngMax; + + // Chart starts at lowest range value, but at least '0' or includes even negative values + double currMinVal = dataBuf.getMin(numBufVals); + // LOG_DEBUG(GwLog::DEBUG, "calcChrtRange0a: currMinVal: %.1f, currMaxVal: %.1f, rngMin: %.1f, rngMid: %.1f, rngMax: %.1f, rng: %.1f, rngStep: %.1f, oldRngMin: %.1f, oldRngMax: %.1f, dfltRng: %.1f, numBufVals: %d", + // currMinVal, dataBuf.getMax(numBufVals), rngMin, rngMid, rngMax, rng, rngStep, oldRngMin, oldRngMax, dfltRng, numBufVals); + + if (currMinVal != dbMAX_VAL) { // current min value is valid + if (currMinVal > 0 && dbMIN_VAL == 0) { // Chart range starts at least at '0' or includes negative values + rngMin = 0; + } else if (currMinVal < oldRngMin || (oldRngMin < 0 && (currMinVal > (oldRngMin + rngStep)))) { // decrease rngMin if required or increase if lowest value is higher than old rngMin + rngMin = std::floor(currMinVal / rngStep) * rngStep; + } + } // otherwise keep rngMin unchanged + + double currMaxVal = dataBuf.getMax(numBufVals); + if (currMaxVal != dbMAX_VAL) { // current max value is valid + if ((currMaxVal > oldRngMax) || (currMaxVal < (oldRngMax - rngStep))) { // increase rngMax if required or decrease if lowest value is lower than old rngMax + rngMax = std::ceil(currMaxVal / rngStep) * rngStep; + rngMax = std::max(rngMax, rngMin + dfltRng); // keep at least default chart range + } + } // otherwise keep rngMax unchanged + + rngMid = (rngMin + rngMax) / 2.0; + rng = rngMax - rngMin; + // LOG_DEBUG(GwLog::DEBUG, "calcChrtRange1a: currMinVal: %.1f, currMaxVal: %.1f, rngMin: %.1f, rngMid: %.1f, rngMax: %.1f, rng: %.1f, rngStep: %.1f, oldRngMin: %.1f, oldRngMax: %.1f, dfltRng: %.1f, numBufVals: %d", + // currMinVal, currMaxVal, rngMin, rngMid, rngMax, rng, rngStep, oldRngMin, oldRngMax, dfltRng, numBufVals); } } @@ -425,52 +436,40 @@ void Chart::calcChrtBorders(double& rngMid, double& rngMin, double& rngMax, d template void Chart::drawChrtTimeAxis(int8_t chrtIntv) { - int timeRng; float slots, intv, i; char sTime[6]; + int timeRng = chrtIntv * 4; // chart time interval: [1] 4 min., [2] 8 min., [3] 12 min., [4] 16 min., [8] 32 min. + getdisplay().setFont(&Ubuntu_Bold8pt8b); getdisplay().setTextColor(fgColor); if (chrtDir == 'H') { // horizontal chart getdisplay().fillRect(0, cStart.y, dWidth, 2, fgColor); - timeRng = chrtIntv * 4; // Chart time interval: [1] 4 min., [2] 8 min., [3] 12 min., [4] 16 min., [8] 32 min. - slots = timAxis / 80.0; // number of axis labels - intv = timeRng / slots; // minutes per chart axis interval + slots = 5; // number of axis labels + intv = timAxis / (slots - 1); // minutes per chart axis interval (interval is 1 less than slots) i = timeRng; // Chart axis label start at -32, -16, -12, ... minutes - for (int j = 0; j < timAxis - 30; j += 80) { // fill time axis with values but keep area free on right hand side for value label - // LOG_DEBUG(GwLog::DEBUG, "ChartTimeAxis: timAxis: %d, {x,y}: {%d,%d}, i: %.1f, j: %d, chrtIntv: %d, intv: %.1f, slots: %.1f", timAxis, cStart.x, cStart.y, i, j, chrtIntv, intv, slots); - - // Format time label based on interval - if (chrtIntv < 3) { - snprintf(sTime, sizeof(sTime), "-%.1f", i); - } else { - snprintf(sTime, sizeof(sTime), "-%.0f", std::round(i)); - } + for (float j = 0; j < timAxis - 1; j += intv) { // fill time axis with values but keep area free on right hand side for value label // draw text with appropriate offset - // int tOffset = (j == 0) ? 13 : (chrtIntv < 3 ? -4 : -4); int tOffset = j == 0 ? 13 : -4; + snprintf(sTime, sizeof(sTime), "-%.0f", i); drawTextCenter(cStart.x + j + tOffset, cStart.y - 8, sTime); getdisplay().drawLine(cStart.x + j, cStart.y, cStart.x + j, cStart.y + 5, fgColor); // draw short vertical time mark - i -= intv; + i -= chrtIntv; } } else { // vertical chart - timeRng = chrtIntv * 4; // chart time interval: [1] 4 min., [2] 8 min., [3] 12 min., [4] 16 min., [8] 32 min. - slots = timAxis / 75.0; // number of axis labels - intv = timeRng / slots; // minutes per chart axis interval - i = -intv; // chart axis label start at -32, -16, -12, ... minutes + slots = 5; // number of axis labels + intv = timAxis / (slots - 1); // minutes per chart axis interval (interval is 1 less than slots) + i = timeRng; // Chart axis label start at -32, -16, -12, ... minutes - for (int j = 75; j < (timAxis - 75); j += 75) { // don't print time label at upper and lower end of time axis - if (chrtIntv < 3) { // print 1 decimal if time range is single digit (4 or 8 minutes) - snprintf(sTime, sizeof(sTime), "%.1f", i); - } else { - snprintf(sTime, sizeof(sTime), "%.0f", std::floor(i)); - } + for (float j = intv; j < timAxis - 1; j += intv) { // don't print time label at upper and lower end of time axis + i -= chrtIntv; // we start not at top chart position + snprintf(sTime, sizeof(sTime), "-%.0f", i); getdisplay().drawLine(cStart.x, cStart.y + j, cStart.x + valAxis, cStart.y + j, fgColor); // Grid line if (chrtSz == 0) { // full size chart @@ -480,8 +479,6 @@ void Chart::drawChrtTimeAxis(int8_t chrtIntv) } else if (chrtSz == 2) { // half size chart; right side drawTextCenter(dWidth / 2, cStart.y + j, sTime); // time value; print mid screen } - - i -= intv; } } } @@ -509,8 +506,21 @@ void Chart::drawChrtValAxis() if (chrtSz == 0) { // full size chart -> print multiple value lines getdisplay().setFont(&Ubuntu_Bold12pt8b); -// for (int j = 60; j < valAxis - 30; j += 60) { - for (int j = valAxis - 60; j > 30; j -= 60) { + + int loopStrt, loopEnd, loopStp; + if (chrtDataFmt == 'S') { + loopStrt = valAxis - 60; + loopEnd = 30; + loopStp = -60; + } else { + loopStrt = 60; + loopEnd = valAxis - 30; + loopStp = 60; + } + LOG_DEBUG(GwLog::DEBUG, "Chart drawValAxis: chrtDataFmt: %c, loopStrt: %d, loopEnd: %d, loopIntv: %d", chrtDataFmt, loopStrt, loopEnd, loopStp); + + for (int j = loopStrt; (loopStp > 0) ? (j < loopEnd) : (j > loopEnd); j += loopStp) { + LOG_DEBUG(GwLog::DEBUG, "Chart drawValAxis2: j: %d, i: %d", j, i); getdisplay().drawLine(cStart.x, cStart.y + j, cStart.x + timAxis, cStart.y + j, fgColor); getdisplay().fillRect(cStart.x, cStart.y + j - 11, 42, 21, bgColor); // Clear small area to remove potential chart lines @@ -523,7 +533,7 @@ void Chart::drawChrtValAxis() } else { // half size chart -> print just edge values + middle chart line getdisplay().setFont(&Ubuntu_Bold10pt8b); - tmpBVal->value = chrtMin; + tmpBVal->value = (chrtDataFmt == 'D') ? chrtMin : chrtMax; cVal = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) sLen = snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); getdisplay().fillRect(cStart.x, cStart.y + 2, 42, 16, bgColor); // Clear small area to remove potential chart lines @@ -538,7 +548,7 @@ void Chart::drawChrtValAxis() getdisplay().printf("%s", sVal); // Range mid value getdisplay().drawLine(cStart.x + 43, cStart.y + (valAxis / 2), cStart.x + timAxis, cStart.y + (valAxis / 2), fgColor); - tmpBVal->value = chrtMax; + tmpBVal->value = (chrtDataFmt == 'D') ? chrtMax : chrtMin; cVal = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) sLen = snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); getdisplay().fillRect(cStart.x, cStart.y + valAxis - 16, 42, 16, bgColor); // Clear small area to remove potential chart lines @@ -586,7 +596,7 @@ template void Chart::prntCurrValue(GwApi::BoatValue& currValue) { const int xPosVal = (chrtDir == 'H') ? cStart.x + (timAxis / 2) - 56 : cStart.x + 32; - const int yPosVal = (chrtDir == 'H') ? cStart.y + valAxis - 5 : cStart.y + timAxis - 5; + const int yPosVal = (chrtDir == 'H') ? cStart.y + valAxis - 7 : cStart.y + timAxis - 7; FormattedData frmtDbData = formatValue(&currValue, *commonData); double testdbValue = frmtDbData.value; @@ -595,8 +605,8 @@ void Chart::prntCurrValue(GwApi::BoatValue& currValue) // LOG_DEBUG(GwLog::DEBUG, "Chart CurrValue: dbValue: %.2f, sdbValue: %s, fmrtDbValue: %.2f, dbFormat: %s, dbUnit: %s, Valid: %d, Name: %s, Address: %p", currValue.value, sdbValue, // testdbValue, currValue.getFormat(), dbUnit, currValue.valid, currValue.getName(), currValue); - getdisplay().fillRect(xPosVal - 1, yPosVal - 34, 125, 40, bgColor); // Clear area for TWS value - getdisplay().drawRect(xPosVal, yPosVal - 33, 123, 39, fgColor); // Draw box for TWS value + getdisplay().fillRect(xPosVal - 1, yPosVal - 35, 125, 41, bgColor); // Clear area for TWS value + getdisplay().drawRect(xPosVal, yPosVal - 34, 123, 40, fgColor); // Draw box for TWS value getdisplay().setFont(&DSEG7Classic_BoldItalic16pt7b); getdisplay().setCursor(xPosVal + 1, yPosVal); if (useSimuData) { diff --git a/lib/obp60task/OBPcharts.h b/lib/obp60task/OBPcharts.h index f160dd4..2ffa141 100644 --- a/lib/obp60task/OBPcharts.h +++ b/lib/obp60task/OBPcharts.h @@ -6,16 +6,16 @@ struct Pos { int x; int y; }; + template class RingBuffer; class GwLog; -template -class Chart { +template class Chart { protected: - CommonData *commonData; - GwLog *logger; + CommonData* commonData; + GwLog* logger; - RingBuffer &dataBuf; // Buffer to display + RingBuffer& dataBuf; // Buffer to display char chrtDir; // Chart timeline direction: 'H' = horizontal, 'V' = vertical int8_t chrtSz; // Chart size: [0] = full size, [1] = half size left/top, [2] half size right/bottom double dfltRng; // Default range of chart, e.g. 30 = [0..30] @@ -23,7 +23,6 @@ protected: uint16_t bgColor; // color code for screen background bool useSimuData; // flag to indicate if simulation data is active - // int top = 48; // display top header lines int top = 44; // chart gap at top of display (25 lines for standard gap + 19 lines for axis labels) int bottom = 25; // chart gap at bottom of display to keep space for status line int hGap = 11; // gap between 2 horizontal charts; actual gap is 2x @@ -59,11 +58,22 @@ protected: void calcChrtBorders(double& rngMid, double& rngMin, double& rngMax, double& rng); // Calculate chart points for value axis and return range between and void drawChrtTimeAxis(int8_t chrtIntv); // Draw time axis of chart, value and lines void drawChrtValAxis(); // Draw value axis of chart, value and lines - void prntCurrValue(GwApi::BoatValue& currValue); // Add current boat data value to chart + void prntCurrValue(GwApi::BoatValue& currValue); // Add current boat data value to chart public: + // Define default chart range for each boat data type + static std::map dfltChartRng; + Chart(RingBuffer& dataBuf, char chrtDir, int8_t chrtSz, double dfltRng, CommonData& common, bool useSimuData); // Chart object of data chart ~Chart(); - void showChrt(int8_t chrtIntv, GwApi::BoatValue currValue); // Perform all actions to draw chart + void showChrt(int8_t chrtIntv, GwApi::BoatValue currValue, bool showCurrValue); // Perform all actions to draw chart +}; +template +std::map Chart::dfltChartRng = { + { "formatWind", 60.0 * DEG_TO_RAD }, // default course range 60 degrees + { "formatCourse", 60.0 * DEG_TO_RAD }, // default course range 60 degrees + { "formatKnots", 5.1 }, // default speed range in m/s + { "formatDepth", 15 }, // default depth range in m + { "kelvinToC", 30 } // default temp range in °C/K }; \ No newline at end of file diff --git a/lib/obp60task/PageOneValue.cpp b/lib/obp60task/PageOneValue.cpp index 6f33597..fe2ac93 100644 --- a/lib/obp60task/PageOneValue.cpp +++ b/lib/obp60task/PageOneValue.cpp @@ -3,112 +3,254 @@ #include "Pagedata.h" #include "OBP60Extensions.h" #include "BoatDataCalibration.h" +#include "OBPcharts.h" -class PageOneValue : public Page -{ - public: - PageOneValue(CommonData &common){ - commonData = &common; - common.logger->logDebug(GwLog::LOG,"Instantiate PageOneValue"); +class PageOneValue : public Page { +private: + GwLog* logger; + + int width; // Screen width + int height; // Screen height + + bool keylock = false; // Keylock + char pageMode = 'V'; // Page mode: 'V' for value, 'C' for chart, 'B' for both + int dataIntv = 1; // Update interval for wind history chart: + // (1)|(2)|(3)|(4)|(8) x 240 seconds for 4, 8, 12, 16, 32 min. history chart + + String lengthformat; + bool useSimuData; + bool holdValues; + String flashLED; + String backlightMode; + + // Old values for hold function + String sValue1Old = ""; + String unit1Old = ""; + + // Data buffer pointer (owned by HstryBuffers) + RingBuffer* dataHstryBuf = nullptr; + std::unique_ptr> dataFlChart, dataHfChart; // Chart object, full and half size + // Active chart and value + Chart* dataChart = nullptr; + + void showData(GwApi::BoatValue* bValue1, char size) + { + int nameXoff, nameYoff, unitXoff, unitYoff, value1Xoff, value1Yoff; + const GFXfont *nameFnt, *unitFnt, *valueFnt1, *valueFnt2, *valueFnt3; + + if (size == 'F') { // full size data display + nameXoff = 0; + nameYoff = 0; + nameFnt = &Ubuntu_Bold32pt8b; + unitXoff = 0; + unitYoff = 0; + unitFnt = &Ubuntu_Bold20pt8b; + value1Xoff = 0; + value1Yoff = 0; + valueFnt1 = &Ubuntu_Bold20pt8b; + valueFnt2 = &Ubuntu_Bold32pt8b; + valueFnt3 = &DSEG7Classic_BoldItalic60pt7b; + } else { // half size data and chart display + nameXoff = 105; + nameYoff = -40; + nameFnt = &Ubuntu_Bold20pt8b; + unitXoff = -33; + unitYoff = -40; + unitFnt = &Ubuntu_Bold12pt8b; + valueFnt1 = &Ubuntu_Bold12pt8b; + value1Xoff = 105; + value1Yoff = -105; + valueFnt2 = &Ubuntu_Bold20pt8b; + valueFnt3 = &DSEG7Classic_BoldItalic30pt7b; + } + + String name1 = xdrDelete(bValue1->getName()); // Value name + name1 = name1.substring(0, 6); // String length limit for value name + calibrationData.calibrateInstance(bValue1, logger); // Check if boat data value is to be calibrated + double value1 = bValue1->value; // Value as double in SI unit + bool valid1 = bValue1->valid; // Valid information + String sValue1 = formatValue(bValue1, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places + String unit1 = formatValue(bValue1, *commonData).unit; // Unit of value + + // Show name + getdisplay().setTextColor(commonData->fgcolor); + getdisplay().setFont(nameFnt); + getdisplay().setCursor(20 + nameXoff, 100 + nameYoff); + getdisplay().print(name1); // name + + // Show unit + getdisplay().setFont(unitFnt); + getdisplay().setCursor(270 + unitXoff, 100 + unitYoff); + if (holdValues == false) { + //getdisplay().print(unit1); // Unit + drawTextRalign(298 + unitXoff, 100 + unitYoff, unit1); // Unit + + } else { + // getdisplay().print(unit1Old); + drawTextRalign(298 + unitXoff, 100 + unitYoff, unit1Old); + } + + // Switch font if format for any values + if (bValue1->getFormat() == "formatLatitude" || bValue1->getFormat() == "formatLongitude") { + getdisplay().setFont(valueFnt1); + getdisplay().setCursor(20 + value1Xoff, 180 + value1Yoff); + } else if (bValue1->getFormat() == "formatTime" || bValue1->getFormat() == "formatDate") { + getdisplay().setFont(valueFnt2); + getdisplay().setCursor(20 + value1Xoff, 200 + value1Yoff); + } else { + getdisplay().setFont(valueFnt3); + getdisplay().setCursor(20 + value1Xoff, 240 + value1Yoff); + } + + // Show bus data + if (holdValues == false) { + getdisplay().print(sValue1); // Real value as formated string + } else { + getdisplay().print(sValue1Old); // Old value as formated string + } + if (valid1 == true) { + sValue1Old = sValue1; // Save the old value + unit1Old = unit1; // Save the old unit + } } - virtual int handleKey(int key){ - // Code for keylock - if(key == 11){ +public: + PageOneValue(CommonData& common) + { + commonData = &common; + logger = commonData->logger; + LOG_DEBUG(GwLog::LOG, "Instantiate PageOneValue"); + + width = getdisplay().width(); // Screen width + height = getdisplay().height(); // Screen height + + // Get config data + lengthformat = common.config->getString(common.config->lengthFormat); + useSimuData = common.config->getBool(common.config->useSimuData); + holdValues = common.config->getBool(common.config->holdvalues); + flashLED = common.config->getString(common.config->flashLED); + backlightMode = common.config->getString(common.config->backlight); + } + + virtual void setupKeys() + { + Page::setupKeys(); + commonData->keydata[0].label = "MODE"; +#if defined BOARD_OBP60S3 + commonData->keydata[4].label = "ZOOM"; +#elif defined BOARD_OBP40S3 + commonData->keydata[1].label = "ZOOM"; +#endif + } + + // Key functions + virtual int handleKey(int key) + { + // Set page mode value | full chart | value/half chart + if (key == 1) { + if (pageMode == 'V') { + pageMode = 'C'; + } else if (pageMode == 'C') { + pageMode = 'B'; + } else { + pageMode = 'V'; + } + return 0; // Commit the key + } + + // Set interval for history chart update time (interval) +#if defined BOARD_OBP60S3 + if (key == 5) { +#elif defined BOARD_OBP40S3 + if (key == 2) { +#endif + if (dataIntv == 1) { + dataIntv = 2; + } else if (dataIntv == 2) { + dataIntv = 3; + } else if (dataIntv == 3) { + dataIntv = 4; + } else if (dataIntv == 4) { + dataIntv = 8; + } else { + dataIntv = 1; + } + return 0; // Commit the key + } + + // Keylock function + if (key == 11) { // Code for keylock commonData->keylock = !commonData->keylock; - return 0; // Commit the key + return 0; // Commit the key } return key; } - int displayPage(PageData &pageData){ - GwConfigHandler *config = commonData->config; - GwLog *logger = commonData->logger; + virtual void displayNew(PageData& pageData) + { +#ifdef BOARD_OBP60S3 + // Clear optical warning + if (flashLED == "Limit Violation") { + setBlinkingLED(false); + setFlashLED(false); + } +#endif + if (!dataFlChart) { // Create chart objects if they don't exist + GwApi::BoatValue* bValue1 = pageData.values[0]; // Page boat data element + String bValName1 = bValue1->getName(); // Value name + String bValFormat = bValue1->getFormat(); // Value format - // Old values for hold function - static String svalue1old = ""; - static String unit1old = ""; + dataHstryBuf = pageData.hstryBuffers->getBuffer(bValName1); - // Get config data - String lengthformat = config->getString(config->lengthFormat); - // bool simulation = config->getBool(config->useSimuData); - bool holdvalues = config->getBool(config->holdvalues); - String flashLED = config->getString(config->flashLED); - String backlightMode = config->getString(config->backlight); - - // Get boat values - GwApi::BoatValue *bvalue1 = pageData.values[0]; // First element in list (only one value by PageOneValue) - String name1 = xdrDelete(bvalue1->getName()); // Value name - name1 = name1.substring(0, 6); // String length limit for value name - calibrationData.calibrateInstance(bvalue1, logger); // Check if boat data value is to be calibrated - double value1 = bvalue1->value; // Value as double in SI unit - bool valid1 = bvalue1->valid; // Valid information - String svalue1 = formatValue(bvalue1, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places - String unit1 = formatValue(bvalue1, *commonData).unit; // Unit of value + dataFlChart.reset(new Chart(*dataHstryBuf, 'H', 0, Chart::dfltChartRng[bValFormat], *commonData, useSimuData)); + dataHfChart.reset(new Chart(*dataHstryBuf, 'H', 2, Chart::dfltChartRng[bValFormat], *commonData, useSimuData)); + LOG_DEBUG(GwLog::DEBUG, "PageOneValue: Created chart objects for %s", bValName1); + } + } + + int displayPage(PageData& pageData) + { + + LOG_DEBUG(GwLog::LOG, "Display PageOneValue"); + GwConfigHandler* config = commonData->config; + GwLog* logger = commonData->logger; + + // Get boat value for page + GwApi::BoatValue* bValue1 = pageData.values[0]; // Page boat data element // Optical warning by limit violation (unused) - if(String(flashLED) == "Limit Violation"){ + if (String(flashLED) == "Limit Violation") { setBlinkingLED(false); - setFlashLED(false); + setFlashLED(false); } // Logging boat values - if (bvalue1 == NULL) return PAGE_OK; // WTF why this statement? - LOG_DEBUG(GwLog::LOG,"Drawing at PageOneValue, %s: %f", name1.c_str(), value1); + if (bValue1 == NULL) + return PAGE_OK; // WTF why this statement? + LOG_DEBUG(GwLog::LOG, "Drawing at PageOneValue, %s: %f", bValue1->getName().c_str(), bValue1->value); // Draw page //*********************************************************** - /// Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + getdisplay().setPartialWindow(0, 0, width, height); // Set partial update - // Show name - getdisplay().setTextColor(commonData->fgcolor); - getdisplay().setFont(&Ubuntu_Bold32pt8b); - getdisplay().setCursor(20, 100); - getdisplay().print(name1); // Page name + if (pageMode == 'V') { // show only data value + showData(bValue1, 'F'); - // Show unit - getdisplay().setFont(&Ubuntu_Bold20pt8b); - getdisplay().setCursor(270, 100); - if(holdvalues == false){ - getdisplay().print(unit1); // Unit - } - else{ - getdisplay().print(unit1old); - } + } else if (pageMode == 'C') { // show only data chart + dataFlChart->showChrt(dataIntv, *bValue1, true); - // Switch font if format for any values - if(bvalue1->getFormat() == "formatLatitude" || bvalue1->getFormat() == "formatLongitude"){ - getdisplay().setFont(&Ubuntu_Bold20pt8b); - getdisplay().setCursor(20, 180); - } - else if(bvalue1->getFormat() == "formatTime" || bvalue1->getFormat() == "formatDate"){ - getdisplay().setFont(&Ubuntu_Bold32pt8b); - getdisplay().setCursor(20, 200); - } - else{ - getdisplay().setFont(&DSEG7Classic_BoldItalic60pt7b); - getdisplay().setCursor(20, 240); - } - - // Show bus data - if(holdvalues == false){ - getdisplay().print(svalue1); // Real value as formated string - } - else{ - getdisplay().print(svalue1old); // Old value as formated string - } - if(valid1 == true){ - svalue1old = svalue1; // Save the old value - unit1old = unit1; // Save the old unit + } else if (pageMode == 'B') { // show data value and chart + showData(bValue1, 'H'); + dataHfChart->showChrt(dataIntv, *bValue1, false); } return PAGE_UPDATE; }; }; -static Page* createPage(CommonData &common){ +static Page* createPage(CommonData& common) +{ return new PageOneValue(common); } @@ -120,10 +262,10 @@ static Page* createPage(CommonData &common){ * this will be number of BoatValue pointers in pageData.values */ PageDescription registerPageOneValue( - "OneValue", // Page name - createPage, // Action - 1, // Number of bus values depends on selection in Web configuration - true // Show display header on/off + "OneValue", // Page name + createPage, // Action + 1, // Number of bus values depends on selection in Web configuration + true // Show display header on/off ); #endif diff --git a/lib/obp60task/PageWindPlot.cpp b/lib/obp60task/PageWindPlot.cpp index 636d8f9..5042d7c 100644 --- a/lib/obp60task/PageWindPlot.cpp +++ b/lib/obp60task/PageWindPlot.cpp @@ -2,7 +2,6 @@ #include "Pagedata.h" #include "OBP60Extensions.h" -#include "OBPDataOperations.h" #include "OBPcharts.h" // **************************************************************** @@ -77,9 +76,9 @@ public: commonData->keydata[0].label = "MODE"; #if defined BOARD_OBP60S3 commonData->keydata[1].label = "SRC"; - commonData->keydata[4].label = "INTV"; + commonData->keydata[4].label = "ZOOM"; #elif defined BOARD_OBP40S3 - commonData->keydata[1].label = "INTV"; + commonData->keydata[1].label = "ZOOM"; #endif } @@ -209,14 +208,14 @@ public: getdisplay().setTextColor(commonData->fgcolor); if (chrtMode == 'D') { - wdFlChart->showChrt(dataIntv, *wdBVal); + wdFlChart->showChrt(dataIntv, *wdBVal, true); } else if (chrtMode == 'S') { - wsFlChart->showChrt(dataIntv, *wsBVal); + wsFlChart->showChrt(dataIntv, *wsBVal, true); } else if (chrtMode == 'B') { - wdHfChart->showChrt(dataIntv, *wdBVal); - wsHfChart->showChrt(dataIntv, *wsBVal); + wdHfChart->showChrt(dataIntv, *wdBVal, true); + wsHfChart->showChrt(dataIntv, *wsBVal, true); } LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: page time %ldms", millis() - pageTime); From 784cc15b8fec55e0ca02a3f6abd78b11b275baed Mon Sep 17 00:00:00 2001 From: Ulrich Meine Date: Wed, 24 Dec 2025 01:40:37 +0100 Subject: [PATCH 082/121] adjusted simulation calc in OBPFormatter; WindPlot + PageOneValue: aligned simulation data handling to standard; added "holdValues"; improved data check for chart buffer data; changed handling of tmpBVal -> always unique_ptr --- lib/obp60task/OBP60Formatter.cpp | 10 ++-- lib/obp60task/OBPDataOperations.cpp | 42 ++++++-------- lib/obp60task/OBPDataOperations.h | 5 +- lib/obp60task/OBPcharts.cpp | 44 +++++++++----- lib/obp60task/OBPcharts.h | 2 +- lib/obp60task/PageOneValue.cpp | 61 +++++++++++++------ lib/obp60task/PageWindPlot.cpp | 90 +++++++++++++++++++++++------ lib/obp60task/Pagedata.h | 3 +- lib/obp60task/obp60task.cpp | 2 +- 9 files changed, 176 insertions(+), 83 deletions(-) diff --git a/lib/obp60task/OBP60Formatter.cpp b/lib/obp60task/OBP60Formatter.cpp index f51c5fa..79bd16a 100644 --- a/lib/obp60task/OBP60Formatter.cpp +++ b/lib/obp60task/OBP60Formatter.cpp @@ -192,10 +192,10 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ rawvalue = value->value; } else { - course = 2.53 + float(random(0, 10) / 100.0); + course = M_PI_2 + float(random(-17, 17) / 100.0); // create random course/wind values with 90° +/- 10° rawvalue = course; } - course = course * 57.2958; // Unit conversion form rad to deg + course = course * RAD_TO_DEG; // Unit conversion form rad to deg // Format 3 numbers with prefix zero snprintf(buffer,bsize,"%03.0f",course); @@ -210,7 +210,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ rawvalue = value->value; } else{ - rawvalue = 4.0 + float(random(0, 40)); + rawvalue = 4.0 + float(random(-30, 40) / 10.0); // create random speed values from [1..8] m/s speed = rawvalue; } if (String(speedFormat) == "km/h"){ @@ -244,7 +244,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ rawvalue = value->value; } else { - rawvalue = 4.0 + float(random(0, 40)); + rawvalue = 4.0 + float(random(0, 40) / 10.0); // create random wind speed values from [4..8] m/s speed = rawvalue; } if (String(windspeedFormat) == "km/h"){ @@ -429,7 +429,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ rawvalue = value->value; } else { - rawvalue = 18.0 + float(random(0, 100)) / 10.0; + rawvalue = 18.0 + float(random(0, 100)) / 10.0; // create random depth values from [18..28] metres depth = rawvalue; } if(String(lengthFormat) == "ft"){ diff --git a/lib/obp60task/OBPDataOperations.cpp b/lib/obp60task/OBPDataOperations.cpp index 3d81fe0..b966bf5 100644 --- a/lib/obp60task/OBPDataOperations.cpp +++ b/lib/obp60task/OBPDataOperations.cpp @@ -1,6 +1,5 @@ #include "OBPDataOperations.h" #include "BoatDataCalibration.h" // Functions lib for data instance calibration -#include // --- Class HstryBuf --------------- HstryBuf::HstryBuf(const String& name, int size, BoatValueList* boatValues, GwLog* log) @@ -26,34 +25,29 @@ void HstryBuf::add(double value) { if (value >= hstryMin && value <= hstryMax) { hstryBuf.add(value); + LOG_DEBUG(GwLog::DEBUG, "HstryBuf::add: name: %s, value: %.3f", hstryBuf.getName(), value); } } -void HstryBuf::handle(bool useSimuData) +void HstryBuf::handle(bool useSimuData, CommonData& common) { - GwApi::BoatValue* calBVal; + //GwApi::BoatValue* tmpBVal; + std::unique_ptr tmpBVal; // Temp variable to get formatted and converted data value from OBP60Formatter + + // create temporary boat value for calibration purposes and retrieval of simulation value + //tmpBVal = new GwApi::BoatValue(boatDataName.c_str()); + tmpBVal = std::unique_ptr(new GwApi::BoatValue(boatDataName)); + tmpBVal->setFormat(boatValue->getFormat()); + tmpBVal->value = boatValue->value; + tmpBVal->valid = boatValue->valid; if (boatValue->valid) { // Calibrate boat value before adding it to history buffer - calBVal = new GwApi::BoatValue(boatDataName.c_str()); - calBVal->setFormat(boatValue->getFormat()); - calBVal->value = boatValue->value; - calBVal->valid = boatValue->valid; - calibrationData.calibrateInstance(calBVal, logger); - add(calBVal->value); - - delete calBVal; - calBVal = nullptr; - + calibrationData.calibrateInstance(tmpBVal.get(), logger); + add(tmpBVal->value); + } else if (useSimuData) { // add simulated value to history buffer - double simValue = hstryBuf.getLast(); - if (boatDataName == "TWD" || boatDataName == "AWD") { - simValue += static_cast(random(-349, 349) / 1000.0); - simValue = WindUtils::to2PI(simValue); - } else if (boatDataName == "TWS" || boatDataName == "AWS") { - simValue += static_cast(random(-5000, 5000) / 1000.0); - simValue = constrain(simValue, 0, 40); - } + double simValue = formatValue(tmpBVal.get(), common).value; // simulated value is generated at add(simValue); } } @@ -96,17 +90,17 @@ void HstryBuffers::addBuffer(const String& name) double bufferMaxVal = bufferParams[name].bufferMaxVal; // Max value for this history buffer hstryBuffers[name]->init(valueFormat, hstryUpdFreq, mltplr, bufferMinVal, bufferMaxVal); - LOG_DEBUG(GwLog::DEBUG, "HstryBuffers-new buffer added: name: %s, format: %s, multiplier: %d, min value: %.2f, max value: %.2f", name, valueFormat, mltplr, bufferMinVal, bufferMaxVal); + LOG_DEBUG(GwLog::DEBUG, "HstryBuffers: new buffer added: name: %s, format: %s, multiplier: %d, min value: %.2f, max value: %.2f", name, valueFormat, mltplr, bufferMinVal, bufferMaxVal); } // Handle history buffers -void HstryBuffers::handleHstryBufs(bool useSimuData) +void HstryBuffers::handleHstryBufs(bool useSimuData, CommonData& common) { // Handle all registered history buffers for (auto& pair : hstryBuffers) { auto& buf = pair.second; - buf->handle(useSimuData); + buf->handle(useSimuData, common); } } diff --git a/lib/obp60task/OBPDataOperations.h b/lib/obp60task/OBPDataOperations.h index 53cae41..f8c9c6e 100644 --- a/lib/obp60task/OBPDataOperations.h +++ b/lib/obp60task/OBPDataOperations.h @@ -2,6 +2,7 @@ #pragma once #include "OBPRingBuffer.h" #include "obp60task.h" +#include "Pagedata.h" #include class HstryBuf { @@ -19,7 +20,7 @@ public: HstryBuf(const String& name, int size, BoatValueList* boatValues, GwLog* log); void init(const String& format, int updFreq, int mltplr, double minVal, double maxVal); void add(double value); - void handle(bool useSimuData); + void handle(bool useSimuData, CommonData& common); }; class HstryBuffers { @@ -61,7 +62,7 @@ private: public: HstryBuffers(int size, BoatValueList* boatValues, GwLog* log); void addBuffer(const String& name); - void handleHstryBufs(bool useSimuData); + void handleHstryBufs(bool useSimuData, CommonData& common); RingBuffer* getBuffer(const String& name); }; diff --git a/lib/obp60task/OBPcharts.cpp b/lib/obp60task/OBPcharts.cpp index 97bd71a..0b00cb7 100644 --- a/lib/obp60task/OBPcharts.cpp +++ b/lib/obp60task/OBPcharts.cpp @@ -1,6 +1,7 @@ // Function lib for display of boat data in various chart formats #include "OBPcharts.h" #include "OBP60Extensions.h" +#include "OBPDataOperations.h" #include "OBPRingBuffer.h" // --- Class Chart --------------- @@ -183,6 +184,8 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) } calcChrtBorders(chrtMid, chrtMin, chrtMax, chrtRng); + LOG_DEBUG(GwLog::DEBUG, "calcChrtBorders: min: %.3f, mid: %.3f, max: %.3f, rng: %.3f", chrtMin, chrtMid, chrtMax, chrtRng); + chrtScl = double(valAxis) / chrtRng; // Chart scale: pixels per value step // Do we have valid buffer data? @@ -501,6 +504,10 @@ void Chart::drawChrtValAxis() slots = valAxis / 60.0; // number of axis labels tmpBVal->value = chrtRng; cChrtRng = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) + if (useSimuData) { + // cannot use in this case, because that would change the range value to some random data + cChrtRng = tmpBVal->value; // take SI value in this case -> need to be improved + } intv = static_cast(round(cChrtRng / slots)); i = intv; @@ -517,10 +524,8 @@ void Chart::drawChrtValAxis() loopEnd = valAxis - 30; loopStp = 60; } - LOG_DEBUG(GwLog::DEBUG, "Chart drawValAxis: chrtDataFmt: %c, loopStrt: %d, loopEnd: %d, loopIntv: %d", chrtDataFmt, loopStrt, loopEnd, loopStp); for (int j = loopStrt; (loopStp > 0) ? (j < loopEnd) : (j > loopEnd); j += loopStp) { - LOG_DEBUG(GwLog::DEBUG, "Chart drawValAxis2: j: %d, i: %d", j, i); getdisplay().drawLine(cStart.x, cStart.y + j, cStart.x + timAxis, cStart.y + j, fgColor); getdisplay().fillRect(cStart.x, cStart.y + j - 11, 42, 21, bgColor); // Clear small area to remove potential chart lines @@ -535,6 +540,9 @@ void Chart::drawChrtValAxis() tmpBVal->value = (chrtDataFmt == 'D') ? chrtMin : chrtMax; cVal = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) + if (useSimuData) { // dirty fix for problem that OBP60Formatter can only be used without data simulation -> returns random values in simulation mode + cVal = tmpBVal->value; // no value conversion here + } sLen = snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); getdisplay().fillRect(cStart.x, cStart.y + 2, 42, 16, bgColor); // Clear small area to remove potential chart lines getdisplay().setCursor(cStart.x + ((3 - sLen) * 10), cStart.y + 16); @@ -542,6 +550,9 @@ void Chart::drawChrtValAxis() tmpBVal->value = chrtMid; cVal = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) + if (useSimuData) { // dirty fix for problem that OBP60Formatter can only be used without data simulation -> returns random values in simulation mode + cVal = tmpBVal->value; // no value conversion here + } sLen = snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); getdisplay().fillRect(cStart.x, cStart.y + (valAxis / 2) - 9, 42, 16, bgColor); // Clear small area to remove potential chart lines getdisplay().setCursor(cStart.x + ((3 - sLen) * 10), cStart.y + (valAxis / 2) + 5); @@ -550,6 +561,9 @@ void Chart::drawChrtValAxis() tmpBVal->value = (chrtDataFmt == 'D') ? chrtMax : chrtMin; cVal = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) + if (useSimuData) { // dirty fix for problem that OBP60Formatter can only be used without data simulation -> returns random values in simulation mode + cVal = tmpBVal->value; // no value conversion here + } sLen = snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); getdisplay().fillRect(cStart.x, cStart.y + valAxis - 16, 42, 16, bgColor); // Clear small area to remove potential chart lines getdisplay().setCursor(cStart.x + ((3 - sLen) * 10), cStart.y + valAxis - 1); @@ -571,17 +585,26 @@ void Chart::drawChrtValAxis() tmpBVal->value = chrtMin; cVal = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) + if (useSimuData) { // dirty fix for problem that OBP60Formatter can only be used without data simulation -> returns random values in simulation mode + cVal = tmpBVal->value; // no value conversion here + } snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); getdisplay().setCursor(cStart.x, cStart.y - 2); getdisplay().printf("%s", sVal); // Range low end tmpBVal->value = chrtMid; cVal = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) + if (useSimuData) { // dirty fix for problem that OBP60Formatter can only be used without data simulation -> returns random values in simulation mode + cVal = tmpBVal->value; // no value conversion here + } snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); drawTextCenter(cStart.x + (valAxis / 2), cStart.y - 10, sVal); // Range mid end tmpBVal->value = chrtMax; cVal = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) + if (useSimuData) { // dirty fix for problem that OBP60Formatter can only be used without data simulation -> returns random values in simulation mode + cVal = tmpBVal->value; // no value conversion here + } snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); drawTextRalign(cStart.x + valAxis - 2, cStart.y - 2, sVal); // Range high end @@ -599,21 +622,16 @@ void Chart::prntCurrValue(GwApi::BoatValue& currValue) const int yPosVal = (chrtDir == 'H') ? cStart.y + valAxis - 7 : cStart.y + timAxis - 7; FormattedData frmtDbData = formatValue(&currValue, *commonData); - double testdbValue = frmtDbData.value; - String sdbValue = frmtDbData.svalue; // value (string) + String sdbValue = frmtDbData.svalue; // value as formatted string String dbUnit = frmtDbData.unit; // Unit of value - // LOG_DEBUG(GwLog::DEBUG, "Chart CurrValue: dbValue: %.2f, sdbValue: %s, fmrtDbValue: %.2f, dbFormat: %s, dbUnit: %s, Valid: %d, Name: %s, Address: %p", currValue.value, sdbValue, - // testdbValue, currValue.getFormat(), dbUnit, currValue.valid, currValue.getName(), currValue); + // LOG_DEBUG(GwLog::DEBUG, "Chart CurrValue: dbValue: %.2f, sdbValue: %s, dbFormat: %s, dbUnit: %s, Valid: %d, Name: %s, Address: %p", currValue.value, sdbValue, + // currValue.getFormat(), dbUnit, currValue.valid, currValue.getName(), currValue); - getdisplay().fillRect(xPosVal - 1, yPosVal - 35, 125, 41, bgColor); // Clear area for TWS value - getdisplay().drawRect(xPosVal, yPosVal - 34, 123, 40, fgColor); // Draw box for TWS value + getdisplay().fillRect(xPosVal - 1, yPosVal - 35, 128, 41, bgColor); // Clear area for TWS value + getdisplay().drawRect(xPosVal, yPosVal - 34, 126, 40, fgColor); // Draw box for TWS value getdisplay().setFont(&DSEG7Classic_BoldItalic16pt7b); getdisplay().setCursor(xPosVal + 1, yPosVal); - if (useSimuData) { - getdisplay().printf("%2.1f", currValue.value); // Value - } else { - getdisplay().print(sdbValue); // Value - } + getdisplay().print(sdbValue); // alue getdisplay().setFont(&Ubuntu_Bold10pt8b); getdisplay().setCursor(xPosVal + 76, yPosVal - 17); diff --git a/lib/obp60task/OBPcharts.h b/lib/obp60task/OBPcharts.h index 2ffa141..7740f91 100644 --- a/lib/obp60task/OBPcharts.h +++ b/lib/obp60task/OBPcharts.h @@ -26,7 +26,7 @@ protected: int top = 44; // chart gap at top of display (25 lines for standard gap + 19 lines for axis labels) int bottom = 25; // chart gap at bottom of display to keep space for status line int hGap = 11; // gap between 2 horizontal charts; actual gap is 2x - int vGap = 20; // gap between 2 vertical charts; actual gap is 2x + int vGap = 17; // gap between 2 vertical charts; actual gap is 2x int dWidth; // Display width int dHeight; // Display height int timAxis, valAxis; // size of time and value chart axis diff --git a/lib/obp60task/PageOneValue.cpp b/lib/obp60task/PageOneValue.cpp index fe2ac93..8f1cc19 100644 --- a/lib/obp60task/PageOneValue.cpp +++ b/lib/obp60task/PageOneValue.cpp @@ -2,6 +2,7 @@ #include "Pagedata.h" #include "OBP60Extensions.h" +#include "OBPDataOperations.h" #include "BoatDataCalibration.h" #include "OBPcharts.h" @@ -30,8 +31,6 @@ private: // Data buffer pointer (owned by HstryBuffers) RingBuffer* dataHstryBuf = nullptr; std::unique_ptr> dataFlChart, dataHfChart; // Chart object, full and half size - // Active chart and value - Chart* dataChart = nullptr; void showData(GwApi::BoatValue* bValue1, char size) { @@ -70,7 +69,7 @@ private: double value1 = bValue1->value; // Value as double in SI unit bool valid1 = bValue1->valid; // Valid information String sValue1 = formatValue(bValue1, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places - String unit1 = formatValue(bValue1, *commonData).unit; // Unit of value + String unit1 = formatValue(bValue1, *commonData).unit; // Unit of value // Show name getdisplay().setTextColor(commonData->fgcolor); @@ -81,16 +80,13 @@ private: // Show unit getdisplay().setFont(unitFnt); getdisplay().setCursor(270 + unitXoff, 100 + unitYoff); - if (holdValues == false) { - //getdisplay().print(unit1); // Unit - drawTextRalign(298 + unitXoff, 100 + unitYoff, unit1); // Unit - - } else { - // getdisplay().print(unit1Old); + if (holdValues) { drawTextRalign(298 + unitXoff, 100 + unitYoff, unit1Old); + } else { + drawTextRalign(298 + unitXoff, 100 + unitYoff, unit1); // Unit } - // Switch font if format for any values + // Switch font depending on value format and adjust position if (bValue1->getFormat() == "formatLatitude" || bValue1->getFormat() == "formatLongitude") { getdisplay().setFont(valueFnt1); getdisplay().setCursor(20 + value1Xoff, 180 + value1Yoff); @@ -103,11 +99,12 @@ private: } // Show bus data - if (holdValues == false) { + if (!holdValues || useSimuData) { getdisplay().print(sValue1); // Real value as formated string } else { getdisplay().print(sValue1Old); // Old value as formated string } + if (valid1 == true) { sValue1Old = sValue1; // Save the old value unit1Old = unit1; // Save the old unit @@ -195,17 +192,22 @@ public: setFlashLED(false); } #endif - if (!dataFlChart) { // Create chart objects if they don't exist + // buffer initialization cannot be performed here, because is not executed at system start for default page + /* if (!dataFlChart) { // Create chart objects if they don't exist GwApi::BoatValue* bValue1 = pageData.values[0]; // Page boat data element String bValName1 = bValue1->getName(); // Value name String bValFormat = bValue1->getFormat(); // Value format dataHstryBuf = pageData.hstryBuffers->getBuffer(bValName1); - dataFlChart.reset(new Chart(*dataHstryBuf, 'H', 0, Chart::dfltChartRng[bValFormat], *commonData, useSimuData)); - dataHfChart.reset(new Chart(*dataHstryBuf, 'H', 2, Chart::dfltChartRng[bValFormat], *commonData, useSimuData)); - LOG_DEBUG(GwLog::DEBUG, "PageOneValue: Created chart objects for %s", bValName1); - } + if (dataHstryBuf) { + dataFlChart.reset(new Chart(*dataHstryBuf, 'H', 0, Chart::dfltChartRng[bValFormat], *commonData, useSimuData)); + dataHfChart.reset(new Chart(*dataHstryBuf, 'H', 2, Chart::dfltChartRng[bValFormat], *commonData, useSimuData)); + LOG_DEBUG(GwLog::DEBUG, "PageOneValue: Created chart objects for %s", bValName1); + } else { + LOG_DEBUG(GwLog::DEBUG, "PageOneValue: No chart objects available for %s", bValName1); + } + } */ } int displayPage(PageData& pageData) @@ -224,10 +226,27 @@ public: setFlashLED(false); } + if (!dataFlChart) { // Create chart objects if they don't exist + GwApi::BoatValue* bValue1 = pageData.values[0]; // Page boat data element + String bValName1 = bValue1->getName(); // Value name + String bValFormat = bValue1->getFormat(); // Value format + + dataHstryBuf = pageData.hstryBuffers->getBuffer(bValName1); + + if (dataHstryBuf) { + dataFlChart.reset(new Chart(*dataHstryBuf, 'H', 0, Chart::dfltChartRng[bValFormat], *commonData, useSimuData)); + dataHfChart.reset(new Chart(*dataHstryBuf, 'H', 2, Chart::dfltChartRng[bValFormat], *commonData, useSimuData)); + LOG_DEBUG(GwLog::DEBUG, "PageOneValue: Created chart objects for %s", bValName1); + } else { + LOG_DEBUG(GwLog::DEBUG, "PageOneValue: No chart objects available for %s", bValName1); + } + } + // Logging boat values if (bValue1 == NULL) return PAGE_OK; // WTF why this statement? - LOG_DEBUG(GwLog::LOG, "Drawing at PageOneValue, %s: %f", bValue1->getName().c_str(), bValue1->value); + + LOG_DEBUG(GwLog::DEBUG, "Drawing at PageOneValue, %s, %.3f", bValue1->getName().c_str(), bValue1->value); // Draw page //*********************************************************** @@ -238,11 +257,15 @@ public: showData(bValue1, 'F'); } else if (pageMode == 'C') { // show only data chart - dataFlChart->showChrt(dataIntv, *bValue1, true); + if (dataFlChart) { + dataFlChart->showChrt(dataIntv, *bValue1, true); + } } else if (pageMode == 'B') { // show data value and chart showData(bValue1, 'H'); - dataHfChart->showChrt(dataIntv, *bValue1, false); + if (dataHfChart) { + dataHfChart->showChrt(dataIntv, *bValue1, false); + } } return PAGE_UPDATE; diff --git a/lib/obp60task/PageWindPlot.cpp b/lib/obp60task/PageWindPlot.cpp index 5042d7c..088be08 100644 --- a/lib/obp60task/PageWindPlot.cpp +++ b/lib/obp60task/PageWindPlot.cpp @@ -2,6 +2,7 @@ #include "Pagedata.h" #include "OBP60Extensions.h" +#include "OBPDataOperations.h" #include "OBPcharts.h" // **************************************************************** @@ -21,6 +22,7 @@ private: int dataIntv = 1; // Update interval for wind history chart: // (1)|(2)|(3)|(4)|(8) x 240 seconds for 4, 8, 12, 16, 32 min. history chart bool useSimuData; + //bool holdValues; String flashLED; String backlightMode; @@ -63,7 +65,7 @@ public: // Get config data useSimuData = common.config->getBool(common.config->useSimuData); - // holdValues = common.config->getBool(common.config->holdvalues); + //holdValues = common.config->getBool(common.config->holdvalues); flashLED = common.config->getString(common.config->flashLED); backlightMode = common.config->getString(common.config->backlight); @@ -149,28 +151,40 @@ public: } oldShowTruW = !showTruW; // Force chart update in displayPage #endif + // buffer initialization cannot be performed here, because is not executed at system start for default page - if (!twdFlChart) { // Create true wind charts if they don't exist - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Creating true wind charts"); + /* if (!twdFlChart) { // Create true wind charts if they don't exist twdHstry = pageData.hstryBuffers->getBuffer("TWD"); twsHstry = pageData.hstryBuffers->getBuffer("TWS"); - twdFlChart.reset(new Chart(*twdHstry, 'V', 0, dfltRngWd, *commonData, useSimuData)); - twsFlChart.reset(new Chart(*twsHstry, 'H', 0, dfltRngWs, *commonData, useSimuData)); - twdHfChart.reset(new Chart(*twdHstry, 'V', 1, dfltRngWd, *commonData, useSimuData)); - twsHfChart.reset(new Chart(*twsHstry, 'V', 2, dfltRngWs, *commonData, useSimuData)); + if (twdHstry) { + twdFlChart.reset(new Chart(*twdHstry, 'V', 0, dfltRngWd, *commonData, useSimuData)); + twdHfChart.reset(new Chart(*twdHstry, 'V', 1, dfltRngWd, *commonData, useSimuData)); + } + if (twsHstry) { + twsFlChart.reset(new Chart(*twsHstry, 'H', 0, dfltRngWs, *commonData, useSimuData)); + twsHfChart.reset(new Chart(*twsHstry, 'V', 2, dfltRngWs, *commonData, useSimuData)); + } } if (!awdFlChart) { // Create apparent wind charts if they don't exist - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Creating apparent wind charts"); awdHstry = pageData.hstryBuffers->getBuffer("AWD"); awsHstry = pageData.hstryBuffers->getBuffer("AWS"); - awdFlChart.reset(new Chart(*awdHstry, 'V', 0, dfltRngWd, *commonData, useSimuData)); - awsFlChart.reset(new Chart(*awsHstry, 'H', 0, dfltRngWs, *commonData, useSimuData)); - awdHfChart.reset(new Chart(*awdHstry, 'V', 1, dfltRngWd, *commonData, useSimuData)); - awsHfChart.reset(new Chart(*awsHstry, 'V', 2, dfltRngWs, *commonData, useSimuData)); - } + if (awdHstry) { + awdFlChart.reset(new Chart(*awdHstry, 'V', 0, dfltRngWd, *commonData, useSimuData)); + awdHfChart.reset(new Chart(*awdHstry, 'V', 1, dfltRngWd, *commonData, useSimuData)); + } + if (awsHstry) { + awsFlChart.reset(new Chart(*awsHstry, 'H', 0, dfltRngWs, *commonData, useSimuData)); + awsHfChart.reset(new Chart(*awsHstry, 'V', 2, dfltRngWs, *commonData, useSimuData)); + } + if (twdHstry && twsHstry && awdHstry && awsHstry) { + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Created wind charts"); + } else { + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Some/all chart objects for wind data missing"); + } + } */ } int displayPage(PageData& pageData) @@ -178,6 +192,39 @@ public: LOG_DEBUG(GwLog::LOG, "Display PageWindPlot"); ulong pageTime = millis(); + if (!twdFlChart) { // Create true wind charts if they don't exist + twdHstry = pageData.hstryBuffers->getBuffer("TWD"); + twsHstry = pageData.hstryBuffers->getBuffer("TWS"); + + if (twdHstry) { + twdFlChart.reset(new Chart(*twdHstry, 'V', 0, dfltRngWd, *commonData, useSimuData)); + twdHfChart.reset(new Chart(*twdHstry, 'V', 1, dfltRngWd, *commonData, useSimuData)); + } + if (twsHstry) { + twsFlChart.reset(new Chart(*twsHstry, 'H', 0, dfltRngWs, *commonData, useSimuData)); + twsHfChart.reset(new Chart(*twsHstry, 'V', 2, dfltRngWs, *commonData, useSimuData)); + } + } + + if (!awdFlChart) { // Create apparent wind charts if they don't exist + awdHstry = pageData.hstryBuffers->getBuffer("AWD"); + awsHstry = pageData.hstryBuffers->getBuffer("AWS"); + + if (awdHstry) { + awdFlChart.reset(new Chart(*awdHstry, 'V', 0, dfltRngWd, *commonData, useSimuData)); + awdHfChart.reset(new Chart(*awdHstry, 'V', 1, dfltRngWd, *commonData, useSimuData)); + } + if (awsHstry) { + awsFlChart.reset(new Chart(*awsHstry, 'H', 0, dfltRngWs, *commonData, useSimuData)); + awsHfChart.reset(new Chart(*awsHstry, 'V', 2, dfltRngWs, *commonData, useSimuData)); + } + if (twdHstry && twsHstry && awdHstry && awsHstry) { + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Created wind charts"); + } else { + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Some/all chart objects for wind data missing"); + } + } + if (showTruW != oldShowTruW) { // Switch active charts based on showTruW @@ -199,6 +246,7 @@ public: oldShowTruW = showTruW; } + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: draw with data %s: %.2f, %s: %.2f", wdBVal->getName().c_str(), wdBVal->value, wsBVal->getName().c_str(), wsBVal->value); // Draw page //*********************************************************** @@ -208,14 +256,22 @@ public: getdisplay().setTextColor(commonData->fgcolor); if (chrtMode == 'D') { - wdFlChart->showChrt(dataIntv, *wdBVal, true); + if (wdFlChart) { + wdFlChart->showChrt(dataIntv, *wdBVal, true); + } } else if (chrtMode == 'S') { - wsFlChart->showChrt(dataIntv, *wsBVal, true); + if (wsFlChart) { + wsFlChart->showChrt(dataIntv, *wsBVal, true); + } } else if (chrtMode == 'B') { - wdHfChart->showChrt(dataIntv, *wdBVal, true); - wsHfChart->showChrt(dataIntv, *wsBVal, true); + if (wdHfChart) { + wdHfChart->showChrt(dataIntv, *wdBVal, true); + } + if (wsHfChart) { + wsHfChart->showChrt(dataIntv, *wsBVal, true); + } } LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: page time %ldms", millis() - pageTime); diff --git a/lib/obp60task/Pagedata.h b/lib/obp60task/Pagedata.h index a12119f..103082f 100644 --- a/lib/obp60task/Pagedata.h +++ b/lib/obp60task/Pagedata.h @@ -4,12 +4,13 @@ #include #include #include "LedSpiTask.h" -#include "OBPDataOperations.h" #define MAX_PAGE_NUMBER 10 // Max number of pages for show data typedef std::vector ValueList; +class HstryBuffers; + typedef struct{ GwApi *api; String pageName; diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index 4689a10..15d859e 100644 --- a/lib/obp60task/obp60task.cpp +++ b/lib/obp60task/obp60task.cpp @@ -815,7 +815,7 @@ void OBP60Task(GwApi *api){ trueWind.addWinds(); } // Handle history buffers for certain boat data for windplot page and other usage - hstryBufList.handleHstryBufs(useSimuData); + hstryBufList.handleHstryBufs(useSimuData, commonData); // Clear display // getdisplay().fillRect(0, 0, getdisplay().width(), getdisplay().height(), commonData.bgcolor); From 2e836bc7501ce7ee720c0e0d63e3494f7d837ff5 Mon Sep 17 00:00:00 2001 From: Ulrich Meine Date: Thu, 1 Jan 2026 22:52:33 +0100 Subject: [PATCH 083/121] added helper method for boat value conversion to OBP60Formatter; fixed range calculation for temperature and other value formats; fixed printing for names > len(3); show "mode" only for supported data types --- lib/obp60task/OBP60Formatter.cpp | 17 ++ lib/obp60task/OBPDataOperations.cpp | 20 +-- lib/obp60task/OBPDataOperations.h | 10 +- lib/obp60task/OBPcharts.cpp | 266 ++++++++++++++++------------ lib/obp60task/OBPcharts.h | 21 ++- lib/obp60task/PageOneValue.cpp | 72 ++++---- lib/obp60task/PageWindPlot.cpp | 15 +- lib/obp60task/Pagedata.h | 3 + 8 files changed, 246 insertions(+), 178 deletions(-) diff --git a/lib/obp60task/OBP60Formatter.cpp b/lib/obp60task/OBP60Formatter.cpp index 79bd16a..830a846 100644 --- a/lib/obp60task/OBP60Formatter.cpp +++ b/lib/obp60task/OBP60Formatter.cpp @@ -877,4 +877,21 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ return result; } +// Helper method for conversion of boat data values from SI to user defined format +double convertValue(const double &value, const String &format, CommonData &commondata) +{ + std::unique_ptr tmpBValue; // Temp variable to get converted data value from + double result; // data value converted to user defined target data format + + // prepare dummy BoatValue structure for use in + tmpBValue = std::unique_ptr(new GwApi::BoatValue("dummy")); // we don't need boat value name for pure value conversion + tmpBValue->setFormat(format); + tmpBValue->valid = true; + tmpBValue->value = value; + + result = formatValue(tmpBValue.get(), commondata).cvalue; // get value (converted) + + return result; +} + #endif diff --git a/lib/obp60task/OBPDataOperations.cpp b/lib/obp60task/OBPDataOperations.cpp index b966bf5..aae0040 100644 --- a/lib/obp60task/OBPDataOperations.cpp +++ b/lib/obp60task/OBPDataOperations.cpp @@ -31,11 +31,11 @@ void HstryBuf::add(double value) void HstryBuf::handle(bool useSimuData, CommonData& common) { - //GwApi::BoatValue* tmpBVal; + // GwApi::BoatValue* tmpBVal; std::unique_ptr tmpBVal; // Temp variable to get formatted and converted data value from OBP60Formatter // create temporary boat value for calibration purposes and retrieval of simulation value - //tmpBVal = new GwApi::BoatValue(boatDataName.c_str()); + // tmpBVal = new GwApi::BoatValue(boatDataName.c_str()); tmpBVal = std::unique_ptr(new GwApi::BoatValue(boatDataName)); tmpBVal->setFormat(boatValue->getFormat()); tmpBVal->value = boatValue->value; @@ -45,7 +45,7 @@ void HstryBuf::handle(bool useSimuData, CommonData& common) // Calibrate boat value before adding it to history buffer calibrationData.calibrateInstance(tmpBVal.get(), logger); add(tmpBVal->value); - + } else if (useSimuData) { // add simulated value to history buffer double simValue = formatValue(tmpBVal.get(), common).value; // simulated value is generated at add(simValue); @@ -78,6 +78,9 @@ void HstryBuffers::addBuffer(const String& name) if (HstryBuffers::getBuffer(name) != nullptr) { // buffer for this data type already exists return; } + if (bufferParams.find(name) == bufferParams.end()) { // requested boat data type is not supported in list of + return; + } hstryBuffers[name] = std::unique_ptr(new HstryBuf(name, size, boatValueList, logger)); @@ -93,13 +96,11 @@ void HstryBuffers::addBuffer(const String& name) LOG_DEBUG(GwLog::DEBUG, "HstryBuffers: new buffer added: name: %s, format: %s, multiplier: %d, min value: %.2f, max value: %.2f", name, valueFormat, mltplr, bufferMinVal, bufferMaxVal); } -// Handle history buffers +// Handle all registered history buffers void HstryBuffers::handleHstryBufs(bool useSimuData, CommonData& common) { - - // Handle all registered history buffers - for (auto& pair : hstryBuffers) { - auto& buf = pair.second; + for (auto& bufMap : hstryBuffers) { + auto& buf = bufMap.second; buf->handle(useSimuData, common); } } @@ -238,7 +239,6 @@ bool WindUtils::calcWinds(const double* awaVal, const double* awsVal, } // LOG_DEBUG(GwLog::DEBUG, "WindUtils:calcWinds: HDT: %.1f, CTW %.1f, STW %.1f", hdt, ctw, stw); - if ((*awaVal == DBL_MAX) || (*awsVal == DBL_MAX)) { // Cannot calculate true wind without valid AWA, AWS; other checks are done earlier return false; @@ -284,7 +284,7 @@ bool WindUtils::addWinds() twdBVal->value = twd; twdBVal->valid = true; } - + } else { // Calculate true winds and AWD; if true winds exist, use at least AWD calculation twCalculated = calcWinds(&awaVal, &awsVal, &cogVal, &stwVal, &sogVal, &hdtVal, &hdmVal, &varVal, &twd, &tws, &twa, &awd); diff --git a/lib/obp60task/OBPDataOperations.h b/lib/obp60task/OBPDataOperations.h index f8c9c6e..7cb4320 100644 --- a/lib/obp60task/OBPDataOperations.h +++ b/lib/obp60task/OBPDataOperations.h @@ -39,9 +39,9 @@ private: String format; }; - // Define buffer parameters for each boat data type + // Define buffer parameters for supported boat data type std::map bufferParams = { - {"AWA", {1000, 10000, -M_PI, M_PI, "formatWind"}}, + {"AWA", {1000, 10000, 0.0, M_TWOPI, "formatWind"}}, {"AWD", {1000, 10000, 0.0, M_TWOPI, "formatCourse"}}, {"AWS", {1000, 1000, 0.0, 65.0, "formatKnots"}}, {"COG", {1000, 10000, 0.0, M_TWOPI, "formatCourse"}}, @@ -50,13 +50,13 @@ private: {"DPT", {1000, 100, 0.0, 650.0, "formatDepth"}}, {"HDM", {1000, 10000, 0.0, M_TWOPI, "formatCourse"}}, {"HDT", {1000, 10000, 0.0, M_TWOPI, "formatCourse"}}, - {"ROT", {1000, 10000, -M_PI / 180.0 * 99.0, M_PI / 180.0 * 99.0, "formatRot"}}, // min/max is -/+ 99 degrees for rotational angle + {"ROT", {1000, 10000, -M_PI / 180.0 * 99.0, M_PI / 180.0 * 99.0, "formatRot"}}, // min/max is -/+ 99 degrees for "rate of turn" {"SOG", {1000, 1000, 0.0, 65.0, "formatKnots"}}, {"STW", {1000, 1000, 0.0, 65.0, "formatKnots"}}, - {"TWA", {1000, 10000, -M_PI, M_PI, "formatWind"}}, + {"TWA", {1000, 10000, 0.0, M_TWOPI, "formatWind"}}, {"TWD", {1000, 10000, 0.0, M_TWOPI, "formatCourse"}}, {"TWS", {1000, 1000, 0.0, 65.0, "formatKnots"}}, - {"WTemp", {1000, 100, 0.0, 650.0, "kelvinToC"}} + {"WTemp", {1000, 100, 233.0, 650.0, "kelvinToC"}} // [-50..376] °C }; public: diff --git a/lib/obp60task/OBPcharts.cpp b/lib/obp60task/OBPcharts.cpp index 0b00cb7..3143273 100644 --- a/lib/obp60task/OBPcharts.cpp +++ b/lib/obp60task/OBPcharts.cpp @@ -25,6 +25,17 @@ Chart::Chart(RingBuffer& dataBuf, char chrtDir, int8_t chrtSz, double dflt fgColor = commonData->fgcolor; bgColor = commonData->bgcolor; + // we need "0" value for any user defined temperature format + tempFormat = commonData->config->getString(commonData->config->tempFormat); // [K|°C|°F] + if (tempFormat == "K") { + zeroValue = 0.0; + } else if (tempFormat == "C") { + zeroValue = 273.15; + } else if (tempFormat == "F") { + zeroValue = 255.37; + } + // LOG_DEBUG(GwLog::DEBUG, "Chart-init: fgColor: %d, bgColor: %d, tempFormat: %s, zeroValue: %.1f, &commonData: %p", fgColor, bgColor, tempFormat, zeroValue, commonData); + // LOG_DEBUG(GwLog::DEBUG, "Chart Init: Chart::dataBuf: %p, passed dataBuf: %p", (void*)&this->dataBuf, (void*)&dataBuf); dWidth = getdisplay().width(); dHeight = getdisplay().height(); @@ -59,12 +70,10 @@ Chart::Chart(RingBuffer& dataBuf, char chrtDir, int8_t chrtSz, double dflt cStart = { 0, top - 1 }; break; case 1: - // valAxis = dWidth / 2 - vGap - 1; valAxis = dWidth / 2 - vGap; cStart = { 0, top - 1 }; break; case 2: - // valAxis = dWidth / 2 - vGap - 1; valAxis = dWidth / 2 - vGap; cStart = { dWidth / 2 + vGap - 1, top - 1 }; break; @@ -94,19 +103,22 @@ Chart::Chart(RingBuffer& dataBuf, char chrtDir, int8_t chrtSz, double dflt } else { if (dbFormat == "formatDepth") { chrtDataFmt = 'D'; // Chart ist showing data of format + } else if (dbFormat == "kelvinToC") { + chrtDataFmt = 'T'; // Chart ist showing data of format } else { - chrtDataFmt = 'S'; // Chart is showing any other data format than + chrtDataFmt = 'S'; // Chart is showing any other data format } - rngStep = 5.0; // +/- 10 for all other values (eg. m/s, m, K, mBar) + rngStep = 10.0; // +/- 10 for all other values (eg. m/s, m, K, mBar) } - chrtMin = 0; - chrtMax = 0; + chrtMin = dbMIN_VAL; + chrtMax = dbMAX_VAL; chrtMid = dbMAX_VAL; chrtRng = dfltRng; - recalcRngCntr = true; // initialize on first screen call + recalcRngCntr = true; // initialize and chart borders on first screen call - LOG_DEBUG(GwLog::DEBUG, "Chart Init: dWidth: %d, dHeight: %d, timAxis: %d, valAxis: %d, cStart {x,y}: %d, %d, dbname: %s, rngStep: %.4f", dWidth, dHeight, timAxis, valAxis, cStart.x, cStart.y, dbName, rngStep); + LOG_DEBUG(GwLog::DEBUG, "Chart Init: dWidth: %d, dHeight: %d, timAxis: %d, valAxis: %d, cStart {x,y}: %d, %d, dbname: %s, rngStep: %.4f, chrtDataFmt: %c", + dWidth, dHeight, timAxis, valAxis, cStart.x, cStart.y, dbName, rngStep, chrtDataFmt); }; template @@ -117,7 +129,7 @@ Chart::~Chart() // Perform all actions to draw chart // Parameters: chart time interval, current boat data value to be printed, current boat data shall be shown yes/no template -void Chart::showChrt(int8_t chrtIntv, GwApi::BoatValue currValue, bool showCurrValue) +void Chart::showChrt(GwApi::BoatValue currValue, int8_t chrtIntv, bool showCurrValue) { drawChrt(chrtIntv, currValue); drawChrtTimeAxis(chrtIntv); @@ -126,11 +138,10 @@ void Chart::showChrt(int8_t chrtIntv, GwApi::BoatValue currValue, bool showCu if (bufDataValid) { if (showCurrValue) { // uses BoatValue temp variable to format latest buffer value - // doesn't work unfortunately when 'simulation data' is active, because OBP60Formatter generates own simulation value in that case + // doesn't work unfortunately when 'simulation data' is active, because OBP60Formatter generates own simulation values in that case currValue.value = dataBuf.getLast(); currValue.valid = currValue.value != dbMAX_VAL; Chart::prntCurrValue(currValue); - LOG_DEBUG(GwLog::DEBUG, "OBPcharts showChrt: currValue-value: %.1f, Valid: %d, Name: %s, Address: %p", currValue.value, currValue.valid, currValue.getName(), (void*)&currValue); } } else { // No valid data available -> print message @@ -157,11 +168,8 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) { double chrtVal; // Current data value double chrtScl; // Scale for data values in pixels per value - static double chrtPrevVal; // Last data value in chart area - static int numNoData; // Counter for multiple invalid data values in a row int x, y; // x and y coordinates for drawing - static int prevX, prevY; // Last x and y coordinates for drawing // Identify buffer size and buffer start position for chart count = dataBuf.getCurrentSize(); @@ -183,10 +191,12 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) } } - calcChrtBorders(chrtMid, chrtMin, chrtMax, chrtRng); - LOG_DEBUG(GwLog::DEBUG, "calcChrtBorders: min: %.3f, mid: %.3f, max: %.3f, rng: %.3f", chrtMin, chrtMid, chrtMax, chrtRng); + // LOG_DEBUG(GwLog::DEBUG, "PageOneValue:drawChart: min: %.1f, mid: %.1f, max: %.1f, rng: %.1f", chrtMin, chrtMid, chrtMax, chrtRng); + calcChrtBorders(chrtMin, chrtMid, chrtMax, chrtRng); + LOG_DEBUG(GwLog::DEBUG, "PageOneValue:drawChart2: min: %.1f, mid: %.1f, max: %.1f, rng: %.1f", chrtMin, chrtMid, chrtMax, chrtRng); - chrtScl = double(valAxis) / chrtRng; // Chart scale: pixels per value step + // Chart scale: pixels per value step + chrtScl = double(valAxis) / chrtRng; // Do we have valid buffer data? if (dataBuf.getMax() == dbMAX_VAL) { // only values in buffer -> no valid wind data available @@ -214,8 +224,7 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) if (chrtDir == 'H') { // horizontal chart x = cStart.x + i; // Position in chart area - if (chrtDataFmt == 'S') { // speed data format -> print low values at bottom - // y = cStart.y + static_cast(((chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round + if (chrtDataFmt == 'S' or chrtDataFmt == 'T') { // speed data format -> print low values at bottom y = cStart.y + valAxis - static_cast(((chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round } else if (chrtDataFmt == 'D') { y = cStart.y + static_cast(((chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round @@ -226,7 +235,7 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) } else { // vertical chart y = cStart.y + timAxis - i; // Position in chart area - if (chrtDataFmt == 'S' || chrtDataFmt == 'D') { + if (chrtDataFmt == 'S' || chrtDataFmt == 'D' || chrtDataFmt == 'T') { x = cStart.x + static_cast(((chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round } else { // degree type value x = cStart.x + static_cast((WindUtils::to2PI(chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round @@ -234,7 +243,7 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) } // if (i >= (numBufVals / chrtIntv) - 5) // log chart data of 1 line (adjust for test purposes) - // LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Chart: i: %d, chrtVal: %.4f, {x,y} {%d,%d}", i, chrtVal, x, y); + // LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Chart: i: %d, chrtVal: %.2f, chrtMin: %.2f, {x,y} {%d,%d}", i, chrtVal, chrtMin, x, y); if ((i == 0) || (chrtPrevVal == dbMAX_VAL)) { // just a dot for 1st chart point or after some invalid values @@ -304,45 +313,9 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) } } -// Get maximum difference of last of dataBuf ringbuffer values to center chart -template -double Chart::getRng(double center, size_t amount) -{ - size_t count = dataBuf.getCurrentSize(); - - if (dataBuf.isEmpty() || amount <= 0) { - return dbMAX_VAL; - } - if (amount > count) - amount = count; - - double value = 0; - double range = 0; - double maxRng = dbMIN_VAL; - - // Start from the newest value (last) and go backwards x times - for (size_t i = 0; i < amount; i++) { - value = dataBuf.get(count - 1 - i); - - if (value == dbMAX_VAL) { - continue; // ignore invalid values - } - - range = abs(fmod((value - center + (M_TWOPI + M_PI)), M_TWOPI) - M_PI); - if (range > maxRng) - maxRng = range; - } - - if (maxRng > M_PI) { - maxRng = M_PI; - } - - return (maxRng != dbMIN_VAL ? maxRng : dbMAX_VAL); // Return range from to -} - // check and adjust chart range and set range borders and range middle template -void Chart::calcChrtBorders(double& rngMid, double& rngMin, double& rngMax, double& rng) +void Chart::calcChrtBorders(double& rngMin, double& rngMid, double& rngMax, double& rng) { if (chrtDataFmt == 'W' || chrtDataFmt == 'R') { // Chart data is of type 'course', 'wind' or 'rot' @@ -372,7 +345,7 @@ void Chart::calcChrtBorders(double& rngMid, double& rngMin, double& rngMax, d } } recalcRngCntr = false; // Reset flag for determination - LOG_DEBUG(GwLog::DEBUG, "calcChrtRange1b: rngMid: %.1f°, rngMin: %.1f°, rngMax: %.1f°, rng: %.1f°, rngStep: %.1f°", rngMid * RAD_TO_DEG, rngMin * RAD_TO_DEG, rngMax * RAD_TO_DEG, + LOG_DEBUG(GwLog::DEBUG, "calcChrtRange: rngMid: %.1f°, rngMin: %.1f°, rngMax: %.1f°, rng: %.1f°, rngStep: %.1f°", rngMid * RAD_TO_DEG, rngMin * RAD_TO_DEG, rngMax * RAD_TO_DEG, rng * RAD_TO_DEG, rngStep * RAD_TO_DEG); } @@ -383,10 +356,9 @@ void Chart::calcChrtBorders(double& rngMid, double& rngMin, double& rngMax, d // check and adjust range between left, center, and right chart limit double halfRng = rng / 2.0; // we calculate with range between and edges - double diffRng = getRng(rngMid, numBufVals); - // LOG_DEBUG(GwLog::DEBUG, "calcChrtRange2: diffRng: %.1f°, halfRng: %.1f°", diffRng * RAD_TO_DEG, halfRng * RAD_TO_DEG); + double diffRng = getAngleRng(rngMid, numBufVals); diffRng = (diffRng == dbMAX_VAL ? 0 : std::ceil(diffRng / rngStep) * rngStep); - // LOG_DEBUG(GwLog::DEBUG, "calcChrtRange2: diffRng: %.1f°, halfRng: %.1f°", diffRng * RAD_TO_DEG, halfRng * RAD_TO_DEG); + // LOG_DEBUG(GwLog::DEBUG, "calcChrtBorders: diffRng: %.1f°, halfRng: %.1f°", diffRng * RAD_TO_DEG, halfRng * RAD_TO_DEG); if (diffRng > halfRng) { halfRng = diffRng; // round to next value @@ -397,41 +369,47 @@ void Chart::calcChrtBorders(double& rngMid, double& rngMin, double& rngMax, d rngMin = WindUtils::to2PI(rngMid - halfRng); rngMax = (halfRng < M_PI ? rngMid + halfRng : rngMid + halfRng - (M_TWOPI / 360)); // if chart range is 360°, then make 1° smaller than rngMax = WindUtils::to2PI(rngMax); - // LOG_DEBUG(GwLog::DEBUG, "calcChrtRange2: diffRng: %.1f°, halfRng: %.1f°", diffRng * RAD_TO_DEG, halfRng * RAD_TO_DEG); rng = halfRng * 2.0; - // LOG_DEBUG(GwLog::DEBUG, "calcChrtRange2b: rngMid: %.1f°, rngMin: %.1f°, rngMax: %.1f°, diffRng: %.1f°, rng: %.1f°, rngStep: %.1f°", rngMid * RAD_TO_DEG, rngMin * RAD_TO_DEG, rngMax * RAD_TO_DEG, - // diffRng * RAD_TO_DEG, rng * RAD_TO_DEG, rngStep * RAD_TO_DEG); + LOG_DEBUG(GwLog::DEBUG, "calcChrtBorders: rngMid: %.1f°, rngMin: %.1f°, rngMax: %.1f°, diffRng: %.1f°, rng: %.1f°, rngStep: %.1f°", rngMid * RAD_TO_DEG, rngMin * RAD_TO_DEG, rngMax * RAD_TO_DEG, + diffRng * RAD_TO_DEG, rng * RAD_TO_DEG, rngStep * RAD_TO_DEG); + + } else { // chart data is of any other type - } else { double oldRngMin = rngMin; double oldRngMax = rngMax; - // Chart starts at lowest range value, but at least '0' or includes even negative values + // calculate low end range value double currMinVal = dataBuf.getMin(numBufVals); - // LOG_DEBUG(GwLog::DEBUG, "calcChrtRange0a: currMinVal: %.1f, currMaxVal: %.1f, rngMin: %.1f, rngMid: %.1f, rngMax: %.1f, rng: %.1f, rngStep: %.1f, oldRngMin: %.1f, oldRngMax: %.1f, dfltRng: %.1f, numBufVals: %d", - // currMinVal, dataBuf.getMax(numBufVals), rngMin, rngMid, rngMax, rng, rngStep, oldRngMin, oldRngMax, dfltRng, numBufVals); + LOG_DEBUG(GwLog::DEBUG, "calcChrtBorders: currMinVal: %.1f, currMaxVal: %.1f, rngMin: %.1f, rngMid: %.1f, rngMax: %.1f, rng: %.1f, rngStep: %.1f, zeroValue: %.1f, oldRngMin: %.1f, oldRngMax: %.1f, dfltRng: %.1f, dbMIN_VAL: %.1f", + currMinVal, dataBuf.getMax(numBufVals), rngMin, rngMid, rngMax, rng, rngStep, zeroValue, oldRngMin, oldRngMax, dfltRng, dbMIN_VAL); if (currMinVal != dbMAX_VAL) { // current min value is valid - if (currMinVal > 0 && dbMIN_VAL == 0) { // Chart range starts at least at '0' or includes negative values - rngMin = 0; - } else if (currMinVal < oldRngMin || (oldRngMin < 0 && (currMinVal > (oldRngMin + rngStep)))) { // decrease rngMin if required or increase if lowest value is higher than old rngMin - rngMin = std::floor(currMinVal / rngStep) * rngStep; + + if (currMinVal < oldRngMin || (currMinVal > (oldRngMin + rngStep))) { // recalculate rngMin if required or increase if lowest value is higher than old rngMin + // rngMin = std::floor(currMinVal / rngStep) * rngStep; // align low range to lowest buffer value and nearest range interval + rngMin = currMinVal; + LOG_DEBUG(GwLog::DEBUG, "calcChrtBorders2: currMinVal: %.1f, rngMin: %.1f, oldRngMin: %.1f, zeroValue: %.1f", currMinVal, rngMin, oldRngMin, zeroValue); + } + if (rngMin > zeroValue && dbMIN_VAL <= zeroValue) { // Chart range starts at least at '0' if minimum data value allows it + rngMin = zeroValue; + LOG_DEBUG(GwLog::DEBUG, "calcChrtBorders3: currMinVal: %.1f, rngMin: %.1f, oldRngMin: %.1f, zeroValue: %.1f", currMinVal, rngMin, oldRngMin, zeroValue); } } // otherwise keep rngMin unchanged double currMaxVal = dataBuf.getMax(numBufVals); if (currMaxVal != dbMAX_VAL) { // current max value is valid if ((currMaxVal > oldRngMax) || (currMaxVal < (oldRngMax - rngStep))) { // increase rngMax if required or decrease if lowest value is lower than old rngMax - rngMax = std::ceil(currMaxVal / rngStep) * rngStep; + // rngMax = std::ceil(currMaxVal / rngStep) * rngStep; + rngMax = currMaxVal; rngMax = std::max(rngMax, rngMin + dfltRng); // keep at least default chart range } } // otherwise keep rngMax unchanged rngMid = (rngMin + rngMax) / 2.0; rng = rngMax - rngMin; - // LOG_DEBUG(GwLog::DEBUG, "calcChrtRange1a: currMinVal: %.1f, currMaxVal: %.1f, rngMin: %.1f, rngMid: %.1f, rngMax: %.1f, rng: %.1f, rngStep: %.1f, oldRngMin: %.1f, oldRngMax: %.1f, dfltRng: %.1f, numBufVals: %d", - // currMinVal, currMaxVal, rngMin, rngMid, rngMax, rng, rngStep, oldRngMin, oldRngMax, dfltRng, numBufVals); + // LOG_DEBUG(GwLog::DEBUG, "calcChrtRange-end: currMinVal: %.1f, currMaxVal: %.1f, rngMin: %.1f, rngMid: %.1f, rngMax: %.1f, rng: %.1f, rngStep: %.1f, oldRngMin: %.1f, oldRngMax: %.1f, dfltRng: %.1f, numBufVals: %d", + // currMinVal, currMaxVal, rngMin, rngMid, rngMax, rng, rngStep, oldRngMin, oldRngMax, dfltRng, numBufVals); } } @@ -492,30 +470,39 @@ void Chart::drawChrtValAxis() { double slots; int i, intv; - double cVal, cChrtRng, crngMin; + double cVal, cChrtRng; char sVal[6]; int sLen; - std::unique_ptr tmpBVal; // Temp variable to get formatted and converted data value from OBP60Formatter - tmpBVal = std::unique_ptr(new GwApi::BoatValue(dataBuf.getName())); - tmpBVal->setFormat(dataBuf.getFormat()); - tmpBVal->valid = true; if (chrtDir == 'H') { - slots = valAxis / 60.0; // number of axis labels - tmpBVal->value = chrtRng; - cChrtRng = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) - if (useSimuData) { - // cannot use in this case, because that would change the range value to some random data - cChrtRng = tmpBVal->value; // take SI value in this case -> need to be improved - } - intv = static_cast(round(cChrtRng / slots)); - i = intv; - if (chrtSz == 0) { // full size chart -> print multiple value lines + // adjust value range to user defined data format + if (chrtDataFmt == 'T') { + if (tempFormat == "F") { + cChrtRng = chrtRng * (9 / 5); + } else { + // data steps for Kelvin and Celsius are identical and '1' + cChrtRng = chrtRng; + } + } else { + // any other data format can be converted with standard rules + cChrtRng = convertValue(chrtRng, dataBuf.getFormat(), *commonData); + } + if (useSimuData) { + // cannot use in this case, because that would change the range value to some random data + cChrtRng = chrtRng; // take SI value in this case -> need to be improved + } + + slots = valAxis / 60.0; // number of axis labels + intv = static_cast(round(cChrtRng / slots)); + i = static_cast(convertValue(chrtMin, dataBuf.getFormat(), *commonData) + intv + 0.5); // convert and round lower chart value end + LOG_DEBUG(GwLog::DEBUG, "Chart::drawChrtValAxis: chrtRng: %.2f, cChrtRng: %.2f, slots: %.2f, intv: %d, chrtMin: %.2f, chrtMid: %.2f, chrtMax: %.2f", chrtRng, cChrtRng, slots, intv, chrtMin, chrtMid, chrtMax); + + if (chrtSz == 0 && chrtDataFmt != 'W') { // full size chart -> print multiple value lines getdisplay().setFont(&Ubuntu_Bold12pt8b); int loopStrt, loopEnd, loopStp; - if (chrtDataFmt == 'S') { + if (chrtDataFmt == 'S' || chrtDataFmt == 'T') { loopStrt = valAxis - 60; loopEnd = 30; loopStp = -60; @@ -535,23 +522,27 @@ void Chart::drawChrtValAxis() i += intv; } - } else { // half size chart -> print just edge values + middle chart line + + } else { // half size chart or degree values -> print just edge values + middle chart line + LOG_DEBUG(GwLog::DEBUG, "Chart::drawChrtValAxis: chrtDataFmt: %c, chrtMin: %.2f, chrtMid: %.2f, chrtMax: %.2f", chrtDataFmt, chrtMin, chrtMid, chrtMax); getdisplay().setFont(&Ubuntu_Bold10pt8b); - tmpBVal->value = (chrtDataFmt == 'D') ? chrtMin : chrtMax; - cVal = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) + // cVal = (chrtDataFmt == 'D') ? chrtMin : chrtMax; + cVal = (chrtDataFmt == 'S' || chrtDataFmt == 'T') ? chrtMax : chrtMin; + cVal = convertValue(cVal, dataBuf.getFormat(), *commonData); // value (converted) if (useSimuData) { // dirty fix for problem that OBP60Formatter can only be used without data simulation -> returns random values in simulation mode - cVal = tmpBVal->value; // no value conversion here + // cVal = (chrtDataFmt == 'D') ? chrtMin : chrtMax; // no value conversion + cVal = (chrtDataFmt == 'S' || chrtDataFmt == 'T') ? chrtMax : chrtMin; // no value conversion } sLen = snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); getdisplay().fillRect(cStart.x, cStart.y + 2, 42, 16, bgColor); // Clear small area to remove potential chart lines getdisplay().setCursor(cStart.x + ((3 - sLen) * 10), cStart.y + 16); getdisplay().printf("%s", sVal); // Range low end - tmpBVal->value = chrtMid; - cVal = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) + cVal = chrtMid; + cVal = convertValue(cVal, dataBuf.getFormat(), *commonData); // value (converted) if (useSimuData) { // dirty fix for problem that OBP60Formatter can only be used without data simulation -> returns random values in simulation mode - cVal = tmpBVal->value; // no value conversion here + cVal = chrtMid; // no value conversion } sLen = snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); getdisplay().fillRect(cStart.x, cStart.y + (valAxis / 2) - 9, 42, 16, bgColor); // Clear small area to remove potential chart lines @@ -559,10 +550,12 @@ void Chart::drawChrtValAxis() getdisplay().printf("%s", sVal); // Range mid value getdisplay().drawLine(cStart.x + 43, cStart.y + (valAxis / 2), cStart.x + timAxis, cStart.y + (valAxis / 2), fgColor); - tmpBVal->value = (chrtDataFmt == 'D') ? chrtMax : chrtMin; - cVal = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) + // cVal = (chrtDataFmt == 'D') ? chrtMax : chrtMin; + cVal = (chrtDataFmt == 'S' || chrtDataFmt == 'T') ? chrtMin : chrtMax; + cVal = convertValue(cVal, dataBuf.getFormat(), *commonData); // value (converted) if (useSimuData) { // dirty fix for problem that OBP60Formatter can only be used without data simulation -> returns random values in simulation mode - cVal = tmpBVal->value; // no value conversion here + // cVal = (chrtDataFmt == 'D') ? chrtMax : chrtMin; // no value conversion + cVal = (chrtDataFmt == 'S' || chrtDataFmt == 'T') ? chrtMax : chrtMin; // no value conversion } sLen = snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); getdisplay().fillRect(cStart.x, cStart.y + valAxis - 16, 42, 16, bgColor); // Clear small area to remove potential chart lines @@ -572,42 +565,43 @@ void Chart::drawChrtValAxis() } getdisplay().setFont(&Ubuntu_Bold12pt8b); - drawTextRalign(cStart.x + timAxis, cStart.y - 3, dbName); // buffer data name + drawTextRalign(cStart.x + timAxis, cStart.y - 3, dbName.substring(0, 4)); // buffer data name (max. size 4 characters) } else { // vertical chart if (chrtSz == 0) { // full size chart -> use larger font getdisplay().setFont(&Ubuntu_Bold12pt8b); - drawTextCenter(cStart.x + (valAxis / 4) + 25, cStart.y - 10, dbName); // buffer data name + drawTextRalign(cStart.x + (valAxis * 0.42), cStart.y - 2, dbName.substring(0, 6)); // buffer data name (max. size 5 characters) } else { getdisplay().setFont(&Ubuntu_Bold10pt8b); } getdisplay().fillRect(cStart.x, cStart.y, valAxis, 2, fgColor); // top chart line - tmpBVal->value = chrtMin; - cVal = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) + cVal = chrtMin; + cVal = convertValue(cVal, dataBuf.getFormat(), *commonData); if (useSimuData) { // dirty fix for problem that OBP60Formatter can only be used without data simulation -> returns random values in simulation mode - cVal = tmpBVal->value; // no value conversion here + cVal = chrtMin; // no value conversion } snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); getdisplay().setCursor(cStart.x, cStart.y - 2); getdisplay().printf("%s", sVal); // Range low end - tmpBVal->value = chrtMid; - cVal = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) + cVal = chrtMid; + cVal = convertValue(cVal, dataBuf.getFormat(), *commonData); if (useSimuData) { // dirty fix for problem that OBP60Formatter can only be used without data simulation -> returns random values in simulation mode - cVal = tmpBVal->value; // no value conversion here + cVal = chrtMid; // no value conversion } snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); - drawTextCenter(cStart.x + (valAxis / 2), cStart.y - 10, sVal); // Range mid end + drawTextCenter(cStart.x + (valAxis / 2), cStart.y - 9, sVal); // Range mid end - tmpBVal->value = chrtMax; - cVal = formatValue(tmpBVal.get(), *commonData).cvalue; // value (converted) + cVal = chrtMax; + cVal = convertValue(cVal, dataBuf.getFormat(), *commonData); if (useSimuData) { // dirty fix for problem that OBP60Formatter can only be used without data simulation -> returns random values in simulation mode - cVal = tmpBVal->value; // no value conversion here + cVal = chrtMax; // no value conversion } snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); drawTextRalign(cStart.x + valAxis - 2, cStart.y - 2, sVal); // Range high end + // draw vertical grid lines for each axis label for (int j = 0; j <= valAxis; j += (valAxis / 2)) { getdisplay().drawLine(cStart.x + j, cStart.y, cStart.x + j, cStart.y + timAxis, fgColor); } @@ -623,25 +617,61 @@ void Chart::prntCurrValue(GwApi::BoatValue& currValue) FormattedData frmtDbData = formatValue(&currValue, *commonData); String sdbValue = frmtDbData.svalue; // value as formatted string - String dbUnit = frmtDbData.unit; // Unit of value + String dbUnit = frmtDbData.unit; // Unit of value; limit length to 3 characters // LOG_DEBUG(GwLog::DEBUG, "Chart CurrValue: dbValue: %.2f, sdbValue: %s, dbFormat: %s, dbUnit: %s, Valid: %d, Name: %s, Address: %p", currValue.value, sdbValue, - // currValue.getFormat(), dbUnit, currValue.valid, currValue.getName(), currValue); + // currValue.getFormat(), dbUnit, currValue.valid, currValue.getName(), currValue); getdisplay().fillRect(xPosVal - 1, yPosVal - 35, 128, 41, bgColor); // Clear area for TWS value getdisplay().drawRect(xPosVal, yPosVal - 34, 126, 40, fgColor); // Draw box for TWS value getdisplay().setFont(&DSEG7Classic_BoldItalic16pt7b); getdisplay().setCursor(xPosVal + 1, yPosVal); - getdisplay().print(sdbValue); // alue + getdisplay().print(sdbValue); // value getdisplay().setFont(&Ubuntu_Bold10pt8b); getdisplay().setCursor(xPosVal + 76, yPosVal - 17); - getdisplay().print(dbName); // Name + getdisplay().print(dbName.substring(0, 3)); // Name, limited to 3 characters getdisplay().setFont(&Ubuntu_Bold8pt8b); getdisplay().setCursor(xPosVal + 76, yPosVal + 0); getdisplay().print(dbUnit); // Unit } +// Get maximum difference of last of dataBuf ringbuffer values to center chart; for angle data only +template +double Chart::getAngleRng(double center, size_t amount) +{ + size_t count = dataBuf.getCurrentSize(); + + if (dataBuf.isEmpty() || amount <= 0) { + return dbMAX_VAL; + } + if (amount > count) + amount = count; + + double value = 0; + double range = 0; + double maxRng = dbMIN_VAL; + + // Start from the newest value (last) and go backwards x times + for (size_t i = 0; i < amount; i++) { + value = dataBuf.get(count - 1 - i); + + if (value == dbMAX_VAL) { + continue; // ignore invalid values + } + + range = abs(fmod((value - center + (M_TWOPI + M_PI)), M_TWOPI) - M_PI); + if (range > maxRng) + maxRng = range; + } + + if (maxRng > M_PI) { + maxRng = M_PI; + } + + return (maxRng != dbMIN_VAL ? maxRng : dbMAX_VAL); // Return range from to +} + // Explicitly instantiate class with required data types to avoid linker errors template class Chart; // --- Class Chart --------------- diff --git a/lib/obp60task/OBPcharts.h b/lib/obp60task/OBPcharts.h index 7740f91..9ce3017 100644 --- a/lib/obp60task/OBPcharts.h +++ b/lib/obp60task/OBPcharts.h @@ -22,6 +22,8 @@ protected: uint16_t fgColor; // color code for any screen writing uint16_t bgColor; // color code for screen background bool useSimuData; // flag to indicate if simulation data is active + String tempFormat; // user defined format for temperature + double zeroValue; // "0" SI value for temperature int top = 44; // chart gap at top of display (25 lines for standard gap + 19 lines for axis labels) int bottom = 25; // chart gap at bottom of display to keep space for status line @@ -50,30 +52,35 @@ protected: size_t currIdx; // Current index in TWD history buffer size_t lastIdx; // Last index of TWD history buffer size_t lastAddedIdx = 0; // Last index of TWD history buffer when new data was added + int numNoData; // Counter for multiple invalid data values in a row bool bufDataValid = false; // Flag to indicate if buffer data is valid int oldChrtIntv = 0; // remember recent user selection of data interval + double chrtPrevVal; // Last data value in chart area + int x, y; // x and y coordinates for drawing + int prevX, prevY; // Last x and y coordinates for drawing + void drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue); // Draw chart line - double getRng(double center, size_t amount); // Calculate range between chart center and edges - void calcChrtBorders(double& rngMid, double& rngMin, double& rngMax, double& rng); // Calculate chart points for value axis and return range between and + void calcChrtBorders(double& rngMin, double& rngMid, double& rngMax, double& rng); // Calculate chart points for value axis and return range between and void drawChrtTimeAxis(int8_t chrtIntv); // Draw time axis of chart, value and lines void drawChrtValAxis(); // Draw value axis of chart, value and lines void prntCurrValue(GwApi::BoatValue& currValue); // Add current boat data value to chart + double getAngleRng(double center, size_t amount); // Calculate range between chart center and edges public: // Define default chart range for each boat data type - static std::map dfltChartRng; + static std::map dfltChrtRng; Chart(RingBuffer& dataBuf, char chrtDir, int8_t chrtSz, double dfltRng, CommonData& common, bool useSimuData); // Chart object of data chart ~Chart(); - void showChrt(int8_t chrtIntv, GwApi::BoatValue currValue, bool showCurrValue); // Perform all actions to draw chart + void showChrt(GwApi::BoatValue currValue, int8_t chrtIntv, bool showCurrValue); // Perform all actions to draw chart }; template -std::map Chart::dfltChartRng = { +std::map Chart::dfltChrtRng = { { "formatWind", 60.0 * DEG_TO_RAD }, // default course range 60 degrees { "formatCourse", 60.0 * DEG_TO_RAD }, // default course range 60 degrees { "formatKnots", 5.1 }, // default speed range in m/s - { "formatDepth", 15 }, // default depth range in m - { "kelvinToC", 30 } // default temp range in °C/K + { "formatDepth", 15.0 }, // default depth range in m + { "kelvinToC", 30.0 } // default temp range in °C/K }; \ No newline at end of file diff --git a/lib/obp60task/PageOneValue.cpp b/lib/obp60task/PageOneValue.cpp index 8f1cc19..a9ce6ff 100644 --- a/lib/obp60task/PageOneValue.cpp +++ b/lib/obp60task/PageOneValue.cpp @@ -18,11 +18,12 @@ private: int dataIntv = 1; // Update interval for wind history chart: // (1)|(2)|(3)|(4)|(8) x 240 seconds for 4, 8, 12, 16, 32 min. history chart - String lengthformat; + //String lengthformat; bool useSimuData; bool holdValues; String flashLED; String backlightMode; + String tempFormat; // Old values for hold function String sValue1Old = ""; @@ -51,14 +52,14 @@ private: valueFnt3 = &DSEG7Classic_BoldItalic60pt7b; } else { // half size data and chart display nameXoff = 105; - nameYoff = -40; + nameYoff = -35; nameFnt = &Ubuntu_Bold20pt8b; - unitXoff = -33; - unitYoff = -40; + unitXoff = -35; + unitYoff = -102; unitFnt = &Ubuntu_Bold12pt8b; valueFnt1 = &Ubuntu_Bold12pt8b; value1Xoff = 105; - value1Yoff = -105; + value1Yoff = -102; valueFnt2 = &Ubuntu_Bold20pt8b; valueFnt3 = &DSEG7Classic_BoldItalic30pt7b; } @@ -79,11 +80,12 @@ private: // Show unit getdisplay().setFont(unitFnt); - getdisplay().setCursor(270 + unitXoff, 100 + unitYoff); + getdisplay().setCursor(305 + unitXoff, 240 + unitYoff); + if (holdValues) { - drawTextRalign(298 + unitXoff, 100 + unitYoff, unit1Old); + getdisplay().print(unit1Old); // name } else { - drawTextRalign(298 + unitXoff, 100 + unitYoff, unit1); // Unit + getdisplay().print(unit1); // name } // Switch font depending on value format and adjust position @@ -122,17 +124,23 @@ public: height = getdisplay().height(); // Screen height // Get config data - lengthformat = common.config->getString(common.config->lengthFormat); - useSimuData = common.config->getBool(common.config->useSimuData); - holdValues = common.config->getBool(common.config->holdvalues); - flashLED = common.config->getString(common.config->flashLED); - backlightMode = common.config->getString(common.config->backlight); + //lengthformat = commonData->config->getString(commonData->config->lengthFormat); + useSimuData = commonData->config->getBool(commonData->config->useSimuData); + holdValues = commonData->config->getBool(commonData->config->holdvalues); + flashLED = commonData->config->getString(commonData->config->flashLED); + backlightMode = commonData->config->getString(commonData->config->backlight); + tempFormat = commonData->config->getString(commonData->config->tempFormat); // [K|°C|°F] } virtual void setupKeys() { Page::setupKeys(); - commonData->keydata[0].label = "MODE"; + + if (dataHstryBuf) { // show "Mode" key only if chart supported boat data type is available + commonData->keydata[0].label = "MODE"; + } else { + commonData->keydata[0].label = ""; + } #if defined BOARD_OBP60S3 commonData->keydata[4].label = "ZOOM"; #elif defined BOARD_OBP40S3 @@ -192,8 +200,8 @@ public: setFlashLED(false); } #endif - // buffer initialization cannot be performed here, because is not executed at system start for default page - /* if (!dataFlChart) { // Create chart objects if they don't exist + // buffer initialization will fail, if page is default page, because is not executed at system start for default page + if (!dataFlChart) { // Create chart objects if they don't exist GwApi::BoatValue* bValue1 = pageData.values[0]; // Page boat data element String bValName1 = bValue1->getName(); // Value name String bValFormat = bValue1->getFormat(); // Value format @@ -201,21 +209,23 @@ public: dataHstryBuf = pageData.hstryBuffers->getBuffer(bValName1); if (dataHstryBuf) { - dataFlChart.reset(new Chart(*dataHstryBuf, 'H', 0, Chart::dfltChartRng[bValFormat], *commonData, useSimuData)); - dataHfChart.reset(new Chart(*dataHstryBuf, 'H', 2, Chart::dfltChartRng[bValFormat], *commonData, useSimuData)); + dataFlChart.reset(new Chart(*dataHstryBuf, 'H', 0, Chart::dfltChrtRng[bValFormat], *commonData, useSimuData)); + dataHfChart.reset(new Chart(*dataHstryBuf, 'H', 2, Chart::dfltChrtRng[bValFormat], *commonData, useSimuData)); LOG_DEBUG(GwLog::DEBUG, "PageOneValue: Created chart objects for %s", bValName1); } else { LOG_DEBUG(GwLog::DEBUG, "PageOneValue: No chart objects available for %s", bValName1); } - } */ + } + + setupKeys(); // adjust key depending on chart supported boat data type } int displayPage(PageData& pageData) { LOG_DEBUG(GwLog::LOG, "Display PageOneValue"); - GwConfigHandler* config = commonData->config; - GwLog* logger = commonData->logger; + //GwConfigHandler* config = commonData->config; + //GwLog* logger = commonData->logger; // Get boat value for page GwApi::BoatValue* bValue1 = pageData.values[0]; // Page boat data element @@ -226,7 +236,7 @@ public: setFlashLED(false); } - if (!dataFlChart) { // Create chart objects if they don't exist +/* if (!dataFlChart) { // Create chart objects if they don't exist GwApi::BoatValue* bValue1 = pageData.values[0]; // Page boat data element String bValName1 = bValue1->getName(); // Value name String bValFormat = bValue1->getFormat(); // Value format @@ -234,37 +244,37 @@ public: dataHstryBuf = pageData.hstryBuffers->getBuffer(bValName1); if (dataHstryBuf) { - dataFlChart.reset(new Chart(*dataHstryBuf, 'H', 0, Chart::dfltChartRng[bValFormat], *commonData, useSimuData)); - dataHfChart.reset(new Chart(*dataHstryBuf, 'H', 2, Chart::dfltChartRng[bValFormat], *commonData, useSimuData)); + dataFlChart.reset(new Chart(*dataHstryBuf, 'H', 0, Chart::dfltChrtRng[bValFormat], *commonData, useSimuData)); + dataHfChart.reset(new Chart(*dataHstryBuf, 'H', 2, Chart::dfltChrtRng[bValFormat], *commonData, useSimuData)); LOG_DEBUG(GwLog::DEBUG, "PageOneValue: Created chart objects for %s", bValName1); } else { LOG_DEBUG(GwLog::DEBUG, "PageOneValue: No chart objects available for %s", bValName1); } - } + } */ - // Logging boat values if (bValue1 == NULL) - return PAGE_OK; // WTF why this statement? + return PAGE_OK; // no data, no display of page - LOG_DEBUG(GwLog::DEBUG, "Drawing at PageOneValue, %s, %.3f", bValue1->getName().c_str(), bValue1->value); + LOG_DEBUG(GwLog::DEBUG, "PageOneValue: printing %s, %.3f", bValue1->getName().c_str(), bValue1->value); // Draw page //*********************************************************** getdisplay().setPartialWindow(0, 0, width, height); // Set partial update - if (pageMode == 'V') { // show only data value + if (pageMode == 'V' || dataHstryBuf == nullptr) { + // show only data value; ignore other pageMode options if no chart supported boat data history buffer is available showData(bValue1, 'F'); } else if (pageMode == 'C') { // show only data chart if (dataFlChart) { - dataFlChart->showChrt(dataIntv, *bValue1, true); + dataFlChart->showChrt(*bValue1, dataIntv, true); } } else if (pageMode == 'B') { // show data value and chart showData(bValue1, 'H'); if (dataHfChart) { - dataHfChart->showChrt(dataIntv, *bValue1, false); + dataHfChart->showChrt(*bValue1, dataIntv, false); } } diff --git a/lib/obp60task/PageWindPlot.cpp b/lib/obp60task/PageWindPlot.cpp index 088be08..50da88f 100644 --- a/lib/obp60task/PageWindPlot.cpp +++ b/lib/obp60task/PageWindPlot.cpp @@ -22,7 +22,7 @@ private: int dataIntv = 1; // Update interval for wind history chart: // (1)|(2)|(3)|(4)|(8) x 240 seconds for 4, 8, 12, 16, 32 min. history chart bool useSimuData; - //bool holdValues; + // bool holdValues; String flashLED; String backlightMode; @@ -65,7 +65,7 @@ public: // Get config data useSimuData = common.config->getBool(common.config->useSimuData); - //holdValues = common.config->getBool(common.config->holdvalues); + // holdValues = common.config->getBool(common.config->holdvalues); flashLED = common.config->getString(common.config->flashLED); backlightMode = common.config->getString(common.config->backlight); @@ -143,6 +143,7 @@ public: } #endif #ifdef BOARD_OBP40S3 + // we can only initialize user defined wind source here, because "pageData" is not available at object instantiation wndSrc = commonData->config->getString("page" + String(pageData.pageNumber) + "wndsrc"); if (wndSrc == "True wind") { showTruW = true; @@ -151,8 +152,8 @@ public: } oldShowTruW = !showTruW; // Force chart update in displayPage #endif - // buffer initialization cannot be performed here, because is not executed at system start for default page + // buffer initialization cannot be performed here, because is not executed at system start for default page /* if (!twdFlChart) { // Create true wind charts if they don't exist twdHstry = pageData.hstryBuffers->getBuffer("TWD"); twsHstry = pageData.hstryBuffers->getBuffer("TWS"); @@ -257,20 +258,20 @@ public: if (chrtMode == 'D') { if (wdFlChart) { - wdFlChart->showChrt(dataIntv, *wdBVal, true); + wdFlChart->showChrt(*wdBVal, dataIntv, true); } } else if (chrtMode == 'S') { if (wsFlChart) { - wsFlChart->showChrt(dataIntv, *wsBVal, true); + wsFlChart->showChrt(*wsBVal, dataIntv, true); } } else if (chrtMode == 'B') { if (wdHfChart) { - wdHfChart->showChrt(dataIntv, *wdBVal, true); + wdHfChart->showChrt(*wdBVal, dataIntv, true); } if (wsHfChart) { - wsHfChart->showChrt(dataIntv, *wsBVal, true); + wsHfChart->showChrt(*wsBVal, dataIntv, true); } } diff --git a/lib/obp60task/Pagedata.h b/lib/obp60task/Pagedata.h index 103082f..d6f342e 100644 --- a/lib/obp60task/Pagedata.h +++ b/lib/obp60task/Pagedata.h @@ -204,3 +204,6 @@ typedef struct{ // Formatter for boat values FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata); + +// Helper method for conversion of any data value from SI to user defined format (defined in OBP60Formatter) +double convertValue(const double &value, const String &format, CommonData &commondata); From 2b6fc09b7e7ae8cd527129e53eb05a0391930ef2 Mon Sep 17 00:00:00 2001 From: TobiasE-github Date: Sun, 4 Jan 2026 11:14:06 +0100 Subject: [PATCH 084/121] don't skip displayNew at startup (fixes issue 215) --- lib/obp60task/obp60task.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index 45c3752..a381590 100644 --- a/lib/obp60task/obp60task.cpp +++ b/lib/obp60task/obp60task.cpp @@ -432,7 +432,7 @@ void OBP60Task(GwApi *api){ #endif LOG_DEBUG(GwLog::LOG,"...done"); - int lastPage=pageNumber; + int lastPage=-1; // initialize with an impiossible value, so we can detect wether we are during startup and no page has been displayed yet BoatValueList boatValues; //all the boat values for the api query HstryBuf hstryBufList(1920); // Create ring buffers for history storage of some boat data (1920 seconds = 32 minutes) @@ -848,8 +848,10 @@ void OBP60Task(GwApi *api){ } else{ if (lastPage != pageNumber){ - pages[lastPage].page->leavePage(pages[lastPage].parameters); // call page cleanup code - if (hasFRAM) fram.write(FRAM_PAGE_NO, pageNumber); // remember new page for device restart + if (lastPage != -1){ // skip cleanup if we are during startup, and no page has been displayed yet. + pages[lastPage].page->leavePage(pages[lastPage].parameters); // call page cleanup code + if (hasFRAM) fram.write(FRAM_PAGE_NO, pageNumber); // remember new page for device restart + } currentPage->setupKeys(); currentPage->displayNew(pages[pageNumber].parameters); lastPage = pageNumber; From 559042da785233fa7af65349597f793fdd73dfb1 Mon Sep 17 00:00:00 2001 From: Ulrich Meine Date: Mon, 5 Jan 2026 23:19:12 +0100 Subject: [PATCH 085/121] Code rework for OBPcharts, part 1 --- lib/obp60task/OBP60Formatter.cpp | 13 +- lib/obp60task/OBPcharts.cpp | 673 ++++++++++++++++++------------- lib/obp60task/OBPcharts.h | 52 ++- lib/obp60task/PageOneValue.cpp | 28 +- lib/obp60task/PageWindPlot.cpp | 23 +- lib/obp60task/Pagedata.h | 3 + 6 files changed, 458 insertions(+), 334 deletions(-) diff --git a/lib/obp60task/OBP60Formatter.cpp b/lib/obp60task/OBP60Formatter.cpp index 830a846..06d3dc5 100644 --- a/lib/obp60task/OBP60Formatter.cpp +++ b/lib/obp60task/OBP60Formatter.cpp @@ -878,19 +878,26 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ } // Helper method for conversion of boat data values from SI to user defined format -double convertValue(const double &value, const String &format, CommonData &commondata) +double convertValue(const double &value, const String &name, const String &format, CommonData &commondata) { std::unique_ptr tmpBValue; // Temp variable to get converted data value from double result; // data value converted to user defined target data format - // prepare dummy BoatValue structure for use in - tmpBValue = std::unique_ptr(new GwApi::BoatValue("dummy")); // we don't need boat value name for pure value conversion + // prepare temporary BoatValue structure for use in + tmpBValue = std::unique_ptr(new GwApi::BoatValue(name)); // we don't need boat value name for pure value conversion tmpBValue->setFormat(format); tmpBValue->valid = true; tmpBValue->value = value; result = formatValue(tmpBValue.get(), commondata).cvalue; // get value (converted) + return result; +} +double convertValue(const double &value, const String &format, CommonData &commondata) +{ + double result; // data value converted to user defined target data format + + result = convertValue(value, "dummy", format, commondata); return result; } diff --git a/lib/obp60task/OBPcharts.cpp b/lib/obp60task/OBPcharts.cpp index 3143273..9830653 100644 --- a/lib/obp60task/OBPcharts.cpp +++ b/lib/obp60task/OBPcharts.cpp @@ -1,6 +1,6 @@ // Function lib for display of boat data in various chart formats #include "OBPcharts.h" -#include "OBP60Extensions.h" +// #include "OBP60Extensions.h" #include "OBPDataOperations.h" #include "OBPRingBuffer.h" @@ -25,18 +25,6 @@ Chart::Chart(RingBuffer& dataBuf, char chrtDir, int8_t chrtSz, double dflt fgColor = commonData->fgcolor; bgColor = commonData->bgcolor; - // we need "0" value for any user defined temperature format - tempFormat = commonData->config->getString(commonData->config->tempFormat); // [K|°C|°F] - if (tempFormat == "K") { - zeroValue = 0.0; - } else if (tempFormat == "C") { - zeroValue = 273.15; - } else if (tempFormat == "F") { - zeroValue = 255.37; - } - // LOG_DEBUG(GwLog::DEBUG, "Chart-init: fgColor: %d, bgColor: %d, tempFormat: %s, zeroValue: %.1f, &commonData: %p", fgColor, bgColor, tempFormat, zeroValue, commonData); - - // LOG_DEBUG(GwLog::DEBUG, "Chart Init: Chart::dataBuf: %p, passed dataBuf: %p", (void*)&this->dataBuf, (void*)&dataBuf); dWidth = getdisplay().width(); dHeight = getdisplay().height(); @@ -46,18 +34,18 @@ Chart::Chart(RingBuffer& dataBuf, char chrtDir, int8_t chrtSz, double dflt switch (chrtSz) { case 0: valAxis = dHeight - top - bottom; - cStart = { 0, top - 1 }; + cRoot = { 0, top - 1 }; break; case 1: valAxis = (dHeight - top - bottom) / 2 - hGap; - cStart = { 0, top - 1 }; + cRoot = { 0, top - 1 }; break; case 2: valAxis = (dHeight - top - bottom) / 2 - hGap; - cStart = { 0, top + (valAxis + hGap) + hGap - 1 }; + cRoot = { 0, top + (valAxis + hGap) + hGap - 1 }; break; default: - LOG_DEBUG(GwLog::ERROR, "displayChart: wrong init parameter"); + LOG_DEBUG(GwLog::ERROR, "obp60:Chart %s: wrong init parameter", dataBuf.getName()); return; } @@ -67,22 +55,22 @@ Chart::Chart(RingBuffer& dataBuf, char chrtDir, int8_t chrtSz, double dflt switch (chrtSz) { case 0: valAxis = dWidth - 1; - cStart = { 0, top - 1 }; + cRoot = { 0, top - 1 }; break; case 1: valAxis = dWidth / 2 - vGap; - cStart = { 0, top - 1 }; + cRoot = { 0, top - 1 }; break; case 2: valAxis = dWidth / 2 - vGap; - cStart = { dWidth / 2 + vGap - 1, top - 1 }; + cRoot = { dWidth / 2 + vGap - 1, top - 1 }; break; default: - LOG_DEBUG(GwLog::ERROR, "displayChart: wrong init parameter"); + LOG_DEBUG(GwLog::ERROR, "obp60:Chart %s: wrong init parameter", dataBuf.getName()); return; } } else { - LOG_DEBUG(GwLog::ERROR, "displayChart: wrong init parameter"); + LOG_DEBUG(GwLog::ERROR, "obp60:Chart %s: wrong init parameter", dataBuf.getName()); return; } @@ -91,34 +79,55 @@ Chart::Chart(RingBuffer& dataBuf, char chrtDir, int8_t chrtSz, double dflt dbMAX_VAL = dataBuf.getMaxVal(); bufSize = dataBuf.getCapacity(); + // Initialize chart data format; shorter version of standard format indicator if (dbFormat == "formatCourse" || dbFormat == "formatWind" || dbFormat == "formatRot") { - - if (dbFormat == "formatRot") { - chrtDataFmt = 'R'; // Chart is showing data of rotational format - } else { - chrtDataFmt = 'W'; // Chart is showing data of course / wind format - } - rngStep = M_TWOPI / 360.0 * 10.0; // +/-10 degrees on each end of chrtMid; we are calculating with SI values - + chrtDataFmt = 'W'; // Chart is showing data of course / wind format + } else if (dbFormat == "formatRot") { + chrtDataFmt = 'R'; // Chart is showing data of rotational format + } else if (dbFormat == "formatKnots") { + chrtDataFmt = 'S'; // Chart is showing data of speed or windspeed format + } else if (dbFormat == "formatDepth") { + chrtDataFmt = 'D'; // Chart ist showing data of format + } else if (dbFormat == "kelvinToC") { + chrtDataFmt = 'T'; // Chart ist showing data of format } else { - if (dbFormat == "formatDepth") { - chrtDataFmt = 'D'; // Chart ist showing data of format - } else if (dbFormat == "kelvinToC") { - chrtDataFmt = 'T'; // Chart ist showing data of format - } else { - chrtDataFmt = 'S'; // Chart is showing any other data format - } - rngStep = 10.0; // +/- 10 for all other values (eg. m/s, m, K, mBar) + chrtDataFmt = 'O'; // Chart is showing any other data format } - chrtMin = dbMIN_VAL; - chrtMax = dbMAX_VAL; - chrtMid = dbMAX_VAL; + // "0" value is the same for any data format but for user defined temperature format + zeroValue = 0.0; + if (chrtDataFmt == 'T') { + tempFormat = commonData->config->getString(commonData->config->tempFormat); // [K|°C|°F] + if (tempFormat == "K") { + zeroValue = 0.0; + } else if (tempFormat == "C") { + zeroValue = 273.15; + } else if (tempFormat == "F") { + zeroValue = 255.37; + } + } + + // Read default range and range step for this chart type + if (dfltChrtDta.count(dbFormat)) { + dfltRng = dfltChrtDta[dbFormat].range; + rngStep = dfltChrtDta[dbFormat].step; + } else { + dfltRng = 15.0; + rngStep = 5.0; + } + + //chrtMin = dbMIN_VAL; + //chrtMax = dbMAX_VAL; + //chrtMid = dbMAX_VAL; + // Initialize chart range values + chrtMin = zeroValue; + chrtMax = chrtMin + dfltRng; + chrtMid = (chrtMin + chrtMax) / 2; chrtRng = dfltRng; recalcRngCntr = true; // initialize and chart borders on first screen call - LOG_DEBUG(GwLog::DEBUG, "Chart Init: dWidth: %d, dHeight: %d, timAxis: %d, valAxis: %d, cStart {x,y}: %d, %d, dbname: %s, rngStep: %.4f, chrtDataFmt: %c", - dWidth, dHeight, timAxis, valAxis, cStart.x, cStart.y, dbName, rngStep, chrtDataFmt); + LOG_DEBUG(GwLog::DEBUG, "Chart Init: dWidth: %d, dHeight: %d, timAxis: %d, valAxis: %d, cRoot {x,y}: %d, %d, dbname: %s, rngStep: %.4f, chrtDataFmt: %c", + dWidth, dHeight, timAxis, valAxis, cRoot.x, cRoot.y, dbName, rngStep, chrtDataFmt); }; template @@ -129,67 +138,34 @@ Chart::~Chart() // Perform all actions to draw chart // Parameters: chart time interval, current boat data value to be printed, current boat data shall be shown yes/no template -void Chart::showChrt(GwApi::BoatValue currValue, int8_t chrtIntv, bool showCurrValue) +void Chart::showChrt(GwApi::BoatValue currValue, int8_t& chrtIntv, const bool showCurrValue) { drawChrt(chrtIntv, currValue); drawChrtTimeAxis(chrtIntv); drawChrtValAxis(); - if (bufDataValid) { - if (showCurrValue) { - // uses BoatValue temp variable to format latest buffer value - // doesn't work unfortunately when 'simulation data' is active, because OBP60Formatter generates own simulation values in that case - currValue.value = dataBuf.getLast(); - currValue.valid = currValue.value != dbMAX_VAL; - Chart::prntCurrValue(currValue); - } + if (!bufDataValid) { // No valid data available + prntNoValidData(); + return; + } - } else { // No valid data available -> print message - getdisplay().setFont(&Ubuntu_Bold10pt8b); - - int pX, pY; - if (chrtDir == 'H') { - pX = cStart.x + (timAxis / 2); - pY = cStart.y + (valAxis / 2) - 10; - } else { - pX = cStart.x + (valAxis / 2); - pY = cStart.y + (timAxis / 2) - 10; - } - - getdisplay().fillRect(pX - 37, pY - 10, 78, 24, bgColor); // Clear area for message - drawTextCenter(pX, pY, "No data"); - LOG_DEBUG(GwLog::LOG, "Page chart: No valid data available"); + if (showCurrValue) { // shows latest value from history buffer; usually this should be the most current one + currValue.value = dataBuf.getLast(); + currValue.valid = currValue.value != dbMAX_VAL; + Chart::prntCurrValue(currValue); } } // draw chart template -void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) +void Chart::drawChrt(int8_t& chrtIntv, GwApi::BoatValue& currValue) { double chrtVal; // Current data value double chrtScl; // Scale for data values in pixels per value int x, y; // x and y coordinates for drawing - // Identify buffer size and buffer start position for chart - count = dataBuf.getCurrentSize(); - currIdx = dataBuf.getLastIdx(); - numAddedBufVals = (currIdx - lastAddedIdx + bufSize) % bufSize; // Number of values added to buffer since last display - - if (chrtIntv != oldChrtIntv || count == 1) { - // new data interval selected by user; this is only x * 230 values instead of 240 seconds (4 minutes) per interval step - numBufVals = min(count, (timAxis - 60) * chrtIntv); // keep free or release 60 values on chart for plotting of new values - bufStart = max(0, count - numBufVals); - lastAddedIdx = currIdx; - oldChrtIntv = chrtIntv; - - } else { - numBufVals = numBufVals + numAddedBufVals; - lastAddedIdx = currIdx; - if (count == bufSize) { - bufStart = max(0, bufStart - numAddedBufVals); - } - } + getBufStartNSize(chrtIntv); // LOG_DEBUG(GwLog::DEBUG, "PageOneValue:drawChart: min: %.1f, mid: %.1f, max: %.1f, rng: %.1f", chrtMin, chrtMid, chrtMax, chrtRng); calcChrtBorders(chrtMin, chrtMid, chrtMax, chrtRng); @@ -204,7 +180,7 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) } else if (!currValue.valid && !useSimuData) { // currently no valid boat data available and no simulation mode numNoData++; bufDataValid = true; - if (numNoData > 3) { // If more than 4 invalid values in a row, send message + if (numNoData > THRESHOLD_NO_DATA) { // If more than 4 invalid values in a row, send message bufDataValid = false; } } else { @@ -222,23 +198,24 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) } else { if (chrtDir == 'H') { // horizontal chart - x = cStart.x + i; // Position in chart area + x = cRoot.x + i; // Position in chart area - if (chrtDataFmt == 'S' or chrtDataFmt == 'T') { // speed data format -> print low values at bottom - y = cStart.y + valAxis - static_cast(((chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round - } else if (chrtDataFmt == 'D') { - y = cStart.y + static_cast(((chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round - } else { // degree type value - y = cStart.y + static_cast((WindUtils::to2PI(chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round + if (chrtDataFmt == 'S' or chrtDataFmt == 'T') { // speed or temperature data format -> print low values at bottom + y = cRoot.y + valAxis - static_cast(((chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round + } else if (chrtDataFmt == 'W' || chrtDataFmt == 'R') { // degree type value + y = cRoot.y + static_cast((WindUtils::to2PI(chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round + } else { // any other data format + y = cRoot.y + static_cast(((chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round } } else { // vertical chart - y = cStart.y + timAxis - i; // Position in chart area + y = cRoot.y + timAxis - i; // Position in chart area - if (chrtDataFmt == 'S' || chrtDataFmt == 'D' || chrtDataFmt == 'T') { - x = cStart.x + static_cast(((chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round - } else { // degree type value - x = cStart.x + static_cast((WindUtils::to2PI(chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round + // if (chrtDataFmt == 'S' || chrtDataFmt == 'D' || chrtDataFmt == 'T') { + if (chrtDataFmt == 'W' || chrtDataFmt == 'R') { // degree type value + x = cRoot.x + static_cast((WindUtils::to2PI(chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round + } else { + x = cRoot.x + static_cast(((chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round } } @@ -261,45 +238,44 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) // LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Chart: crossedBorders: %d, chrtVal: %.2f, chrtPrevVal: %.2f", crossedBorders, chrtVal, chrtPrevVal); bool wrappingFromHighToLow = normCurr < normPrev; // Determine which edge we're crossing if (chrtDir == 'H') { - int ySplit = wrappingFromHighToLow ? (cStart.y + valAxis) : cStart.y; - getdisplay().drawLine(prevX, prevY, x, ySplit, fgColor); - if (x != prevX) { // line with some horizontal trend - getdisplay().drawLine(prevX, prevY - 1, x, ySplit - 1, fgColor); - } else { - getdisplay().drawLine(prevX, prevY - 1, x - 1, ySplit, fgColor); - } - prevY = wrappingFromHighToLow ? cStart.y : (cStart.y + valAxis); + int ySplit = wrappingFromHighToLow ? (cRoot.y + valAxis) : cRoot.y; + drawBoldLine(prevX, prevY, x, ySplit); + prevY = wrappingFromHighToLow ? cRoot.y : (cRoot.y + valAxis); } else { // vertical chart - int xSplit = wrappingFromHighToLow ? (cStart.x + valAxis) : cStart.x; - getdisplay().drawLine(prevX, prevY, xSplit, y, fgColor); - getdisplay().drawLine(prevX, prevY - 1, ((xSplit != prevX) ? xSplit : xSplit - 1), ((xSplit != prevX) ? y - 1 : y), fgColor); - prevX = wrappingFromHighToLow ? cStart.x : (cStart.x + valAxis); + int xSplit = wrappingFromHighToLow ? (cRoot.x + valAxis) : cRoot.x; + drawBoldLine(prevX, prevY, xSplit, y); + prevX = wrappingFromHighToLow ? cRoot.x : (cRoot.x + valAxis); } } } - // Draw line with 2 pixels width + make sure vertical lines are drawn correctly - if (chrtDir == 'H' || x == prevX) { // horizontal chart & vertical line - if (chrtDataFmt == 'D') { - getdisplay().drawLine(x, y, x, cStart.y + valAxis, fgColor); - getdisplay().drawLine(x - 1, y, x - 1, cStart.y + valAxis, fgColor); + if (chrtDataFmt == 'D') { + if (chrtDir == 'H') { // horizontal chart + drawBoldLine(x, y, x, cRoot.y + valAxis); + } else { // vertical chart + drawBoldLine(x, y, cRoot.x + valAxis, y); } - getdisplay().drawLine(prevX, prevY, x, y, fgColor); - getdisplay().drawLine(prevX - 1, prevY, x - 1, y, fgColor); - } else if (chrtDir == 'V' || x != prevX) { // vertical chart & line with some horizontal trend -> normal state - if (chrtDataFmt == 'D') { - getdisplay().drawLine(x, y, cStart.x + valAxis, y, fgColor); - getdisplay().drawLine(x, y - 1, cStart.x + valAxis, y - 1, fgColor); - } - getdisplay().drawLine(prevX, prevY, x, y, fgColor); - getdisplay().drawLine(prevX, prevY - 1, x, y - 1, fgColor); + } else { + drawBoldLine(prevX, prevY, x, y); } + + /* if (chrtDir == 'H' || x == prevX) { // horizontal chart & vertical line + if (chrtDataFmt == 'D') { + drawBoldLine(x, y, x, cRoot.y + valAxis); + } + drawBoldLine(prevX, prevY, x, y); + } else if (chrtDir == 'V' || x != prevX) { // vertical chart & line with some horizontal trend -> normal state + if (chrtDataFmt == 'D') { + drawBoldLine(x, y, cRoot.x + valAxis, y); + } + drawBoldLine(prevX, prevY, x, y); + } */ chrtPrevVal = chrtVal; prevX = x; prevY = y; } - // Reaching chart area bottom end + // Reaching chart area top end if (i >= timAxis - 1) { oldChrtIntv = 0; // force reset of buffer start and number of values to show in next display loop @@ -313,6 +289,30 @@ void Chart::drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue) } } +// Identify buffer size and buffer start position for chart +template +void Chart::getBufStartNSize(int8_t& chrtIntv) +{ + count = dataBuf.getCurrentSize(); + currIdx = dataBuf.getLastIdx(); + numAddedBufVals = (currIdx - lastAddedIdx + bufSize) % bufSize; // Number of values added to buffer since last display + + if (chrtIntv != oldChrtIntv || count == 1) { + // new data interval selected by user; this is only x * 230 values instead of 240 seconds (4 minutes) per interval step + numBufVals = min(count, (timAxis - MIN_FREE_VALUES) * chrtIntv); // keep free or release MIN_FREE_VALUES on chart for plotting of new values + bufStart = max(0, count - numBufVals); + lastAddedIdx = currIdx; + oldChrtIntv = chrtIntv; + + } else { + numBufVals = numBufVals + numAddedBufVals; + lastAddedIdx = currIdx; + if (count == bufSize) { + bufStart = max(0, bufStart - numAddedBufVals); + } + } +} + // check and adjust chart range and set range borders and range middle template void Chart::calcChrtBorders(double& rngMin, double& rngMid, double& rngMax, double& rng) @@ -371,94 +371,90 @@ void Chart::calcChrtBorders(double& rngMin, double& rngMid, double& rngMax, d rngMax = WindUtils::to2PI(rngMax); rng = halfRng * 2.0; - LOG_DEBUG(GwLog::DEBUG, "calcChrtBorders: rngMid: %.1f°, rngMin: %.1f°, rngMax: %.1f°, diffRng: %.1f°, rng: %.1f°, rngStep: %.1f°", rngMid * RAD_TO_DEG, rngMin * RAD_TO_DEG, rngMax * RAD_TO_DEG, - diffRng * RAD_TO_DEG, rng * RAD_TO_DEG, rngStep * RAD_TO_DEG); + LOG_DEBUG(GwLog::DEBUG, "calcChrtBorders: rngMin: %.1f°, rngMid: %.1f°, rngMax: %.1f°, diffRng: %.1f°, rng: %.1f°, rngStep: %.1f°", rngMin * RAD_TO_DEG, rngMid * RAD_TO_DEG, rngMax * RAD_TO_DEG, + diffRng * RAD_TO_DEG, rng * RAD_TO_DEG, rngStep * RAD_TO_DEG); } else { // chart data is of any other type double oldRngMin = rngMin; double oldRngMax = rngMax; - // calculate low end range value double currMinVal = dataBuf.getMin(numBufVals); - LOG_DEBUG(GwLog::DEBUG, "calcChrtBorders: currMinVal: %.1f, currMaxVal: %.1f, rngMin: %.1f, rngMid: %.1f, rngMax: %.1f, rng: %.1f, rngStep: %.1f, zeroValue: %.1f, oldRngMin: %.1f, oldRngMax: %.1f, dfltRng: %.1f, dbMIN_VAL: %.1f", - currMinVal, dataBuf.getMax(numBufVals), rngMin, rngMid, rngMax, rng, rngStep, zeroValue, oldRngMin, oldRngMax, dfltRng, dbMIN_VAL); - - if (currMinVal != dbMAX_VAL) { // current min value is valid - - if (currMinVal < oldRngMin || (currMinVal > (oldRngMin + rngStep))) { // recalculate rngMin if required or increase if lowest value is higher than old rngMin - // rngMin = std::floor(currMinVal / rngStep) * rngStep; // align low range to lowest buffer value and nearest range interval - rngMin = currMinVal; - LOG_DEBUG(GwLog::DEBUG, "calcChrtBorders2: currMinVal: %.1f, rngMin: %.1f, oldRngMin: %.1f, zeroValue: %.1f", currMinVal, rngMin, oldRngMin, zeroValue); - } - if (rngMin > zeroValue && dbMIN_VAL <= zeroValue) { // Chart range starts at least at '0' if minimum data value allows it - rngMin = zeroValue; - LOG_DEBUG(GwLog::DEBUG, "calcChrtBorders3: currMinVal: %.1f, rngMin: %.1f, oldRngMin: %.1f, zeroValue: %.1f", currMinVal, rngMin, oldRngMin, zeroValue); - } - } // otherwise keep rngMin unchanged - double currMaxVal = dataBuf.getMax(numBufVals); - if (currMaxVal != dbMAX_VAL) { // current max value is valid - if ((currMaxVal > oldRngMax) || (currMaxVal < (oldRngMax - rngStep))) { // increase rngMax if required or decrease if lowest value is lower than old rngMax - // rngMax = std::ceil(currMaxVal / rngStep) * rngStep; - rngMax = currMaxVal; - rngMax = std::max(rngMax, rngMin + dfltRng); // keep at least default chart range - } - } // otherwise keep rngMax unchanged + + if (currMinVal == dbMAX_VAL || currMaxVal == dbMAX_VAL) { + return; // no valid data + } + + // check if current chart border have to be adjusted + if (currMinVal < oldRngMin || (currMinVal > (oldRngMin + rngStep))) { // decrease rngMin if required or increase if lowest value is higher than old rngMin + rngMin = std::floor(currMinVal / rngStep) * rngStep; // align low range to lowest buffer value and nearest range interval + } + if ((currMaxVal > oldRngMax) || (currMaxVal < (oldRngMax - rngStep))) { // increase rngMax if required or decrease if lowest value is lower than old rngMax + rngMax = std::ceil(currMaxVal / rngStep) * rngStep; + } + + // Chart range starts at least at '0' if minimum data value allows it + if (rngMin > zeroValue && dbMIN_VAL <= zeroValue) { + rngMin = zeroValue; + } + + // ensure minimum chart range in user format + if ((rngMax - rngMin) < dfltRng) { + rngMax = rngMin + dfltRng; + } rngMid = (rngMin + rngMax) / 2.0; rng = rngMax - rngMin; - // LOG_DEBUG(GwLog::DEBUG, "calcChrtRange-end: currMinVal: %.1f, currMaxVal: %.1f, rngMin: %.1f, rngMid: %.1f, rngMax: %.1f, rng: %.1f, rngStep: %.1f, oldRngMin: %.1f, oldRngMax: %.1f, dfltRng: %.1f, numBufVals: %d", - // currMinVal, currMaxVal, rngMin, rngMid, rngMax, rng, rngStep, oldRngMin, oldRngMax, dfltRng, numBufVals); + + LOG_DEBUG(GwLog::DEBUG, "calcChrtRange-end: currMinVal: %.1f, currMaxVal: %.1f, rngMin: %.1f, rngMid: %.1f, rngMax: %.1f, rng: %.1f, rngStep: %.1f, zeroValue: %.1f, dbMIN_VAL: %.1f", + currMinVal, currMaxVal, rngMin, rngMid, rngMax, rng, rngStep, zeroValue, dbMIN_VAL); } } // chart time axis label + lines template -void Chart::drawChrtTimeAxis(int8_t chrtIntv) +void Chart::drawChrtTimeAxis(int8_t& chrtIntv) { - float slots, intv, i; + float axSlots, intv, i; char sTime[6]; int timeRng = chrtIntv * 4; // chart time interval: [1] 4 min., [2] 8 min., [3] 12 min., [4] 16 min., [8] 32 min. getdisplay().setFont(&Ubuntu_Bold8pt8b); getdisplay().setTextColor(fgColor); - if (chrtDir == 'H') { // horizontal chart - getdisplay().fillRect(0, cStart.y, dWidth, 2, fgColor); + axSlots = 5; // number of axis labels + intv = timAxis / (axSlots - 1); // minutes per chart axis interval (interval is 1 less than axSlots) + i = timeRng; // Chart axis label start at -32, -16, -12, ... minutes - slots = 5; // number of axis labels - intv = timAxis / (slots - 1); // minutes per chart axis interval (interval is 1 less than slots) - i = timeRng; // Chart axis label start at -32, -16, -12, ... minutes + if (chrtDir == 'H') { // horizontal chart + getdisplay().fillRect(0, cRoot.y, dWidth, 2, fgColor); for (float j = 0; j < timAxis - 1; j += intv) { // fill time axis with values but keep area free on right hand side for value label // draw text with appropriate offset int tOffset = j == 0 ? 13 : -4; snprintf(sTime, sizeof(sTime), "-%.0f", i); - drawTextCenter(cStart.x + j + tOffset, cStart.y - 8, sTime); - getdisplay().drawLine(cStart.x + j, cStart.y, cStart.x + j, cStart.y + 5, fgColor); // draw short vertical time mark + drawTextCenter(cRoot.x + j + tOffset, cRoot.y - 8, sTime); + getdisplay().drawLine(cRoot.x + j, cRoot.y, cRoot.x + j, cRoot.y + 5, fgColor); // draw short vertical time mark i -= chrtIntv; } } else { // vertical chart - slots = 5; // number of axis labels - intv = timAxis / (slots - 1); // minutes per chart axis interval (interval is 1 less than slots) - i = timeRng; // Chart axis label start at -32, -16, -12, ... minutes for (float j = intv; j < timAxis - 1; j += intv) { // don't print time label at upper and lower end of time axis i -= chrtIntv; // we start not at top chart position snprintf(sTime, sizeof(sTime), "-%.0f", i); - getdisplay().drawLine(cStart.x, cStart.y + j, cStart.x + valAxis, cStart.y + j, fgColor); // Grid line + getdisplay().drawLine(cRoot.x, cRoot.y + j, cRoot.x + valAxis, cRoot.y + j, fgColor); // Grid line if (chrtSz == 0) { // full size chart - getdisplay().fillRect(0, cStart.y + j - 9, 32, 15, bgColor); // clear small area to remove potential chart lines - getdisplay().setCursor((4 - strlen(sTime)) * 7, cStart.y + j + 3); // time value; print left screen; value right-formated + getdisplay().fillRect(0, cRoot.y + j - 9, 32, 15, bgColor); // clear small area to remove potential chart lines + getdisplay().setCursor((4 - strlen(sTime)) * 7, cRoot.y + j + 3); // time value; print left screen; value right-formated getdisplay().printf("%s", sTime); // Range value } else if (chrtSz == 2) { // half size chart; right side - drawTextCenter(dWidth / 2, cStart.y + j, sTime); // time value; print mid screen + drawTextCenter(dWidth / 2, cRoot.y + j, sTime); // time value; print mid screen } } } @@ -468,142 +464,75 @@ void Chart::drawChrtTimeAxis(int8_t chrtIntv) template void Chart::drawChrtValAxis() { - double slots; - int i, intv; - double cVal, cChrtRng; - char sVal[6]; - int sLen; + double axLabel; + double cVal; + // char sVal[6]; + + getdisplay().setTextColor(fgColor); if (chrtDir == 'H') { - // adjust value range to user defined data format - if (chrtDataFmt == 'T') { - if (tempFormat == "F") { - cChrtRng = chrtRng * (9 / 5); - } else { - // data steps for Kelvin and Celsius are identical and '1' - cChrtRng = chrtRng; - } - } else { - // any other data format can be converted with standard rules - cChrtRng = convertValue(chrtRng, dataBuf.getFormat(), *commonData); - } - if (useSimuData) { - // cannot use in this case, because that would change the range value to some random data - cChrtRng = chrtRng; // take SI value in this case -> need to be improved - } - - slots = valAxis / 60.0; // number of axis labels - intv = static_cast(round(cChrtRng / slots)); - i = static_cast(convertValue(chrtMin, dataBuf.getFormat(), *commonData) + intv + 0.5); // convert and round lower chart value end - LOG_DEBUG(GwLog::DEBUG, "Chart::drawChrtValAxis: chrtRng: %.2f, cChrtRng: %.2f, slots: %.2f, intv: %d, chrtMin: %.2f, chrtMid: %.2f, chrtMax: %.2f", chrtRng, cChrtRng, slots, intv, chrtMin, chrtMid, chrtMax); - - if (chrtSz == 0 && chrtDataFmt != 'W') { // full size chart -> print multiple value lines - getdisplay().setFont(&Ubuntu_Bold12pt8b); - - int loopStrt, loopEnd, loopStp; - if (chrtDataFmt == 'S' || chrtDataFmt == 'T') { - loopStrt = valAxis - 60; - loopEnd = 30; - loopStp = -60; - } else { - loopStrt = 60; - loopEnd = valAxis - 30; - loopStp = 60; - } - - for (int j = loopStrt; (loopStp > 0) ? (j < loopEnd) : (j > loopEnd); j += loopStp) { - getdisplay().drawLine(cStart.x, cStart.y + j, cStart.x + timAxis, cStart.y + j, fgColor); - - getdisplay().fillRect(cStart.x, cStart.y + j - 11, 42, 21, bgColor); // Clear small area to remove potential chart lines - String sVal = String(i); - getdisplay().setCursor((3 - sVal.length()) * 10, cStart.y + j + 7); // value right-formated - getdisplay().printf("%s", sVal); // Range value - - i += intv; - } - - } else { // half size chart or degree values -> print just edge values + middle chart line - LOG_DEBUG(GwLog::DEBUG, "Chart::drawChrtValAxis: chrtDataFmt: %c, chrtMin: %.2f, chrtMid: %.2f, chrtMax: %.2f", chrtDataFmt, chrtMin, chrtMid, chrtMax); - getdisplay().setFont(&Ubuntu_Bold10pt8b); - - // cVal = (chrtDataFmt == 'D') ? chrtMin : chrtMax; - cVal = (chrtDataFmt == 'S' || chrtDataFmt == 'T') ? chrtMax : chrtMin; - cVal = convertValue(cVal, dataBuf.getFormat(), *commonData); // value (converted) - if (useSimuData) { // dirty fix for problem that OBP60Formatter can only be used without data simulation -> returns random values in simulation mode - // cVal = (chrtDataFmt == 'D') ? chrtMin : chrtMax; // no value conversion - cVal = (chrtDataFmt == 'S' || chrtDataFmt == 'T') ? chrtMax : chrtMin; // no value conversion - } - sLen = snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); - getdisplay().fillRect(cStart.x, cStart.y + 2, 42, 16, bgColor); // Clear small area to remove potential chart lines - getdisplay().setCursor(cStart.x + ((3 - sLen) * 10), cStart.y + 16); - getdisplay().printf("%s", sVal); // Range low end - - cVal = chrtMid; - cVal = convertValue(cVal, dataBuf.getFormat(), *commonData); // value (converted) - if (useSimuData) { // dirty fix for problem that OBP60Formatter can only be used without data simulation -> returns random values in simulation mode - cVal = chrtMid; // no value conversion - } - sLen = snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); - getdisplay().fillRect(cStart.x, cStart.y + (valAxis / 2) - 9, 42, 16, bgColor); // Clear small area to remove potential chart lines - getdisplay().setCursor(cStart.x + ((3 - sLen) * 10), cStart.y + (valAxis / 2) + 5); - getdisplay().printf("%s", sVal); // Range mid value - getdisplay().drawLine(cStart.x + 43, cStart.y + (valAxis / 2), cStart.x + timAxis, cStart.y + (valAxis / 2), fgColor); - - // cVal = (chrtDataFmt == 'D') ? chrtMax : chrtMin; - cVal = (chrtDataFmt == 'S' || chrtDataFmt == 'T') ? chrtMin : chrtMax; - cVal = convertValue(cVal, dataBuf.getFormat(), *commonData); // value (converted) - if (useSimuData) { // dirty fix for problem that OBP60Formatter can only be used without data simulation -> returns random values in simulation mode - // cVal = (chrtDataFmt == 'D') ? chrtMax : chrtMin; // no value conversion - cVal = (chrtDataFmt == 'S' || chrtDataFmt == 'T') ? chrtMax : chrtMin; // no value conversion - } - sLen = snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); - getdisplay().fillRect(cStart.x, cStart.y + valAxis - 16, 42, 16, bgColor); // Clear small area to remove potential chart lines - getdisplay().setCursor(cStart.x + ((3 - sLen) * 10), cStart.y + valAxis - 1); - getdisplay().printf("%s", sVal); // Range high end - getdisplay().drawLine(cStart.x + 43, cStart.y + valAxis, cStart.x + timAxis, cStart.y + valAxis, fgColor); - } - + // print buffer data name on right hand side of time axis (max. size 5 characters) getdisplay().setFont(&Ubuntu_Bold12pt8b); - drawTextRalign(cStart.x + timAxis, cStart.y - 3, dbName.substring(0, 4)); // buffer data name (max. size 4 characters) + drawTextRalign(cRoot.x + timAxis, cRoot.y - 3, dbName.substring(0, 5)); + + if (chrtSz == 0) { // full size chart + + if (chrtDataFmt == 'W') { + prntHorizThreeValueAxisLabel(&Ubuntu_Bold12pt8b); + return; + } + + // for any other data formats print multiple axis value lines on full charts + prntHorizMultiValueAxisLabel(&Ubuntu_Bold12pt8b); + return; + + } else { // half size chart -> just print edge values + middle chart line + LOG_DEBUG(GwLog::DEBUG, "Chart::drawChrtValAxis: chrtDataFmt: %c, chrtMin: %.2f, chrtMid: %.2f, chrtMax: %.2f", chrtDataFmt, chrtMin, chrtMid, chrtMax); + + prntHorizThreeValueAxisLabel(&Ubuntu_Bold10pt8b); + return; + } } else { // vertical chart - if (chrtSz == 0) { // full size chart -> use larger font - getdisplay().setFont(&Ubuntu_Bold12pt8b); - drawTextRalign(cStart.x + (valAxis * 0.42), cStart.y - 2, dbName.substring(0, 6)); // buffer data name (max. size 5 characters) + char sVal[6]; + + if (chrtSz == 0) { // full size chart + getdisplay().setFont(&Ubuntu_Bold12pt8b); // use larger font + drawTextRalign(cRoot.x + (valAxis * 0.42), cRoot.y - 2, dbName.substring(0, 6)); // print buffer data name (max. size 5 characters) } else { - getdisplay().setFont(&Ubuntu_Bold10pt8b); + getdisplay().setFont(&Ubuntu_Bold10pt8b); // use smaller font } - getdisplay().fillRect(cStart.x, cStart.y, valAxis, 2, fgColor); // top chart line + getdisplay().fillRect(cRoot.x, cRoot.y, valAxis, 2, fgColor); // top chart line cVal = chrtMin; - cVal = convertValue(cVal, dataBuf.getFormat(), *commonData); + cVal = convertValue(cVal, dbName, dbFormat, *commonData); // value (converted) if (useSimuData) { // dirty fix for problem that OBP60Formatter can only be used without data simulation -> returns random values in simulation mode cVal = chrtMin; // no value conversion } snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); - getdisplay().setCursor(cStart.x, cStart.y - 2); + getdisplay().setCursor(cRoot.x, cRoot.y - 2); getdisplay().printf("%s", sVal); // Range low end cVal = chrtMid; - cVal = convertValue(cVal, dataBuf.getFormat(), *commonData); + cVal = convertValue(cVal, dbName, dbFormat, *commonData); // value (converted) if (useSimuData) { // dirty fix for problem that OBP60Formatter can only be used without data simulation -> returns random values in simulation mode cVal = chrtMid; // no value conversion } snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); - drawTextCenter(cStart.x + (valAxis / 2), cStart.y - 9, sVal); // Range mid end + drawTextCenter(cRoot.x + (valAxis / 2), cRoot.y - 9, sVal); // Range mid end cVal = chrtMax; - cVal = convertValue(cVal, dataBuf.getFormat(), *commonData); + cVal = convertValue(cVal, dbName, dbFormat, *commonData); // value (converted) if (useSimuData) { // dirty fix for problem that OBP60Formatter can only be used without data simulation -> returns random values in simulation mode cVal = chrtMax; // no value conversion } snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); - drawTextRalign(cStart.x + valAxis - 2, cStart.y - 2, sVal); // Range high end + drawTextRalign(cRoot.x + valAxis - 2, cRoot.y - 2, sVal); // Range high end // draw vertical grid lines for each axis label for (int j = 0; j <= valAxis; j += (valAxis / 2)) { - getdisplay().drawLine(cStart.x + j, cStart.y, cStart.x + j, cStart.y + timAxis, fgColor); + getdisplay().drawLine(cRoot.x + j, cRoot.y, cRoot.x + j, cRoot.y + timAxis, fgColor); } } } @@ -612,8 +541,8 @@ void Chart::drawChrtValAxis() template void Chart::prntCurrValue(GwApi::BoatValue& currValue) { - const int xPosVal = (chrtDir == 'H') ? cStart.x + (timAxis / 2) - 56 : cStart.x + 32; - const int yPosVal = (chrtDir == 'H') ? cStart.y + valAxis - 7 : cStart.y + timAxis - 7; + const int xPosVal = (chrtDir == 'H') ? cRoot.x + (timAxis / 2) - 56 : cRoot.x + 32; + const int yPosVal = (chrtDir == 'H') ? cRoot.y + valAxis - 7 : cRoot.y + timAxis - 7; FormattedData frmtDbData = formatValue(&currValue, *commonData); String sdbValue = frmtDbData.svalue; // value as formatted string @@ -636,6 +565,28 @@ void Chart::prntCurrValue(GwApi::BoatValue& currValue) getdisplay().print(dbUnit); // Unit } +// print message for no valid data availabletemplate +template +void Chart::prntNoValidData() +{ + int pX, pY; + + getdisplay().setFont(&Ubuntu_Bold10pt8b); + + if (chrtDir == 'H') { + pX = cRoot.x + (timAxis / 2); + pY = cRoot.y + (valAxis / 2) - 10; + } else { + pX = cRoot.x + (valAxis / 2); + pY = cRoot.y + (timAxis / 2) - 10; + } + + getdisplay().fillRect(pX - 37, pY - 10, 78, 24, bgColor); // Clear area for message + drawTextCenter(pX, pY, "No data"); + + LOG_DEBUG(GwLog::LOG, "Page chart <%s>: No valid data available", dbName); +} + // Get maximum difference of last of dataBuf ringbuffer values to center chart; for angle data only template double Chart::getAngleRng(double center, size_t amount) @@ -672,6 +623,170 @@ double Chart::getAngleRng(double center, size_t amount) return (maxRng != dbMIN_VAL ? maxRng : dbMAX_VAL); // Return range from to } +// print horizontal axis label with only three values: top, mid, and bottom +template +void Chart::prntHorizThreeValueAxisLabel(const GFXfont* font) +{ + double axLabel; + double chrtMin, chrtMid, chrtMax; + int xOffset, yOffset; // offset for text position of x axis label for different font sizes + String sVal; + + if (font == &Ubuntu_Bold10pt8b) { + xOffset = 39; + yOffset = 15; + } else if (font == &Ubuntu_Bold12pt8b) { + xOffset = 51; + yOffset = 17; + } + getdisplay().setFont(font); + + // convert & round chart bottom+top label to next range step + chrtMin = convertValue(this->chrtMin, dbName, dbFormat, *commonData); + chrtMid = convertValue(this->chrtMid, dbName, dbFormat, *commonData); + chrtMax = convertValue(this->chrtMax, dbName, dbFormat, *commonData); + chrtMin = std::round(chrtMin * 100.0) / 100.0; + chrtMid = std::round(chrtMid * 100.0) / 100.0; + chrtMax = std::round(chrtMax * 100.0) / 100.0; + + // print top axis label + axLabel = (chrtDataFmt == 'S' || chrtDataFmt == 'T') ? chrtMax : chrtMin; + sVal = formatLabel(axLabel); + getdisplay().fillRect(cRoot.x, cRoot.y + 2, xOffset + 4, yOffset, bgColor); // Clear small area to remove potential chart lines + drawTextRalign(cRoot.x + xOffset, cRoot.y + yOffset, sVal); // range value + + // print mid axis label + axLabel = chrtMid; + sVal = formatLabel(axLabel); + getdisplay().fillRect(cRoot.x, cRoot.y + (valAxis / 2) - 9, xOffset + 4, 16, bgColor); // Clear small area to remove potential chart lines + drawTextRalign(cRoot.x + xOffset, cRoot.y + (valAxis / 2) + 5, sVal); // range value + getdisplay().drawLine(cRoot.x + xOffset + 4, cRoot.y + (valAxis / 2), cRoot.x + timAxis, cRoot.y + (valAxis / 2), fgColor); + + // print bottom axis label + axLabel = (chrtDataFmt == 'S' || chrtDataFmt == 'T') ? chrtMin : chrtMax; + sVal = formatLabel(axLabel); + getdisplay().fillRect(cRoot.x, cRoot.y + valAxis - 16, xOffset + 3, 16, bgColor); // Clear small area to remove potential chart lines + drawTextRalign(cRoot.x + xOffset, cRoot.y + valAxis, sVal); // range value + getdisplay().drawLine(cRoot.x + xOffset + 2, cRoot.y + valAxis, cRoot.x + timAxis, cRoot.y + valAxis, fgColor); +} + +// print horizontal axis label with multiple axis lines +template +void Chart::prntHorizMultiValueAxisLabel(const GFXfont* font) +{ + double chrtMin, chrtMax, chrtRng; + double axSlots, axIntv, axLabel; + int xOffset; // offset for text position of x axis label for different font sizes + String sVal; + + if (font == &Ubuntu_Bold10pt8b) { + xOffset = 38; + } else if (font == &Ubuntu_Bold12pt8b) { + xOffset = 50; + } + getdisplay().setFont(font); + + chrtMin = convertValue(this->chrtMin, dbName, dbFormat, *commonData); + // chrtMin = std::floor(chrtMin / rngStep) * rngStep; + chrtMin = std::round(chrtMin * 100.0) / 100.0; + chrtMax = convertValue(this->chrtMax, dbName, dbFormat, *commonData); + // chrtMax = std::ceil(chrtMax / rngStep) * rngStep; + chrtMax = std::round(chrtMax * 100.0) / 100.0; + chrtRng = std::round((chrtMax - chrtMin) * 100) / 100; + + axSlots = valAxis / static_cast(VALAXIS_STEP); // number of axis labels (and we want to have a double calculation, no integer) + axIntv = chrtRng / axSlots; + axLabel = chrtMin + axIntv; + LOG_DEBUG(GwLog::DEBUG, "Chart::printHorizMultiValueAxisLabel: chrtRng: %.2f, th-chrtRng: %.2f, axSlots: %.2f, axIntv: %.2f, axLabel: %.2f, chrtMin: %.2f, chrtMid: %.2f, chrtMax: %.2f", chrtRng, this->chrtRng, axSlots, axIntv, axLabel, this->chrtMin, chrtMid, chrtMax); + + int loopStrt, loopEnd, loopStp; + if (chrtDataFmt == 'S' || chrtDataFmt == 'T' || chrtDataFmt == 'O') { + // High value at top + loopStrt = valAxis - VALAXIS_STEP; + loopEnd = VALAXIS_STEP / 2; + loopStp = VALAXIS_STEP * -1; + } else { + // Low value at top + loopStrt = VALAXIS_STEP; + loopEnd = valAxis - (VALAXIS_STEP / 2); + loopStp = VALAXIS_STEP; + } + + for (int j = loopStrt; (loopStp > 0) ? (j < loopEnd) : (j > loopEnd); j += loopStp) { + sVal = formatLabel(axLabel); + // sVal = convNformatLabel(axLabel); + getdisplay().fillRect(cRoot.x, cRoot.y + j - 11, xOffset + 4, 21, bgColor); // Clear small area to remove potential chart lines + drawTextRalign(cRoot.x + xOffset, cRoot.y + j + 7, sVal); // range value + getdisplay().drawLine(cRoot.x + xOffset + 3, cRoot.y + j, cRoot.x + timAxis, cRoot.y + j, fgColor); + + axLabel += axIntv; + } +} + +// Draw chart line with thickness of 2px +template +void Chart::drawBoldLine(int16_t x1, int16_t y1, int16_t x2, int16_t y2) +{ + + int16_t dx = std::abs(x2 - x1); + int16_t dy = std::abs(y2 - y1); + + getdisplay().drawLine(x1, y1, x2, y2, fgColor); + + if (dx >= dy) { // line has horizontal tendency + getdisplay().drawLine(x1, y1 - 1, x2, y2 - 1, fgColor); + } else { // line has vertical tendency + getdisplay().drawLine(x1 - 1, y1, x2 - 1, y2, fgColor); + } +} + +// Convert and format current axis label to user defined format; helper function for easier handling of OBP60Formatter +template +String Chart::convNformatLabel(double label) +{ + GwApi::BoatValue tmpBVal(dbName); // temporary boat value for string formatter + String sVal; + + tmpBVal.setFormat(dbFormat); + tmpBVal.valid = true; + tmpBVal.value = label; + sVal = formatValue(&tmpBVal, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places + if (sVal.length() > 0 && sVal[0] == '!') { + sVal = sVal.substring(1); // cut leading "!" created at OBPFormatter for use with other font than 7SEG + } + + return sVal; +} + +// Format current axis label for printing w/o data format conversion (has been done earlier) +template +String Chart::formatLabel(const double& label) +{ + char sVal[11]; + + if (dbFormat == "formatCourse" || dbFormat == "formatWind") { + // Format 3 numbers with prefix zero + snprintf(sVal, sizeof(sVal), "%03.0f", label); + + } else if (dbFormat == "formatRot") { + if (label > -10 && label < 10) { + snprintf(sVal, sizeof(sVal), "%3.2f", label); + } else { + snprintf(sVal, sizeof(sVal), "%3.0f", label); + } + } + + else { + if (label < 10) { + snprintf(sVal, sizeof(sVal), "%3.1f", label); + } else { + snprintf(sVal, sizeof(sVal), "%3.0f", label); + } + } + + return String(sVal); +} + // Explicitly instantiate class with required data types to avoid linker errors template class Chart; // --- Class Chart --------------- diff --git a/lib/obp60task/OBPcharts.h b/lib/obp60task/OBPcharts.h index 9ce3017..db62a3c 100644 --- a/lib/obp60task/OBPcharts.h +++ b/lib/obp60task/OBPcharts.h @@ -1,16 +1,24 @@ // Function lib for display of boat data in various graphical chart formats #pragma once #include "Pagedata.h" +#include "OBP60Extensions.h" struct Pos { int x; int y; }; -template class RingBuffer; +struct ChartProps { + double range; + double step; +}; + +template +class RingBuffer; class GwLog; -template class Chart { +template +class Chart { protected: CommonData* commonData; GwLog* logger; @@ -25,14 +33,14 @@ protected: String tempFormat; // user defined format for temperature double zeroValue; // "0" SI value for temperature + int dWidth; // Display width + int dHeight; // Display height int top = 44; // chart gap at top of display (25 lines for standard gap + 19 lines for axis labels) int bottom = 25; // chart gap at bottom of display to keep space for status line int hGap = 11; // gap between 2 horizontal charts; actual gap is 2x int vGap = 17; // gap between 2 vertical charts; actual gap is 2x - int dWidth; // Display width - int dHeight; // Display height int timAxis, valAxis; // size of time and value chart axis - Pos cStart; // start point of chart area + Pos cRoot; // start point of chart area double chrtRng; // Range of buffer values from min to max value double chrtMin; // Range low end value double chrtMax; // Range high end value @@ -60,27 +68,39 @@ protected: int x, y; // x and y coordinates for drawing int prevX, prevY; // Last x and y coordinates for drawing - void drawChrt(int8_t chrtIntv, GwApi::BoatValue& currValue); // Draw chart line + static constexpr int8_t MIN_FREE_VALUES = 60; + static constexpr int8_t THRESHOLD_NO_DATA = 3; + static constexpr int8_t VALAXIS_STEP = 60; + + void drawChrt(int8_t& chrtIntv, GwApi::BoatValue& currValue); // Draw chart line + void getBufStartNSize(int8_t& chrtIntv); // Identify buffer size and buffer start position for chart void calcChrtBorders(double& rngMin, double& rngMid, double& rngMax, double& rng); // Calculate chart points for value axis and return range between and - void drawChrtTimeAxis(int8_t chrtIntv); // Draw time axis of chart, value and lines + void drawChrtTimeAxis(int8_t& chrtIntv); // Draw time axis of chart, value and lines void drawChrtValAxis(); // Draw value axis of chart, value and lines void prntCurrValue(GwApi::BoatValue& currValue); // Add current boat data value to chart + void prntNoValidData(); // print message for no valid data available double getAngleRng(double center, size_t amount); // Calculate range between chart center and edges + void prntHorizThreeValueAxisLabel(const GFXfont* font); // print horizontal axis label with only three values: top, mid, and bottom + void prntHorizMultiValueAxisLabel(const GFXfont* font); // print horizontal axis label with multiple axis lines + void drawBoldLine(int16_t x1, int16_t y1, int16_t x2, int16_t y2); // Draw chart line with thickness of 2px + String convNformatLabel(double label); // Convert and format current axis label to user defined format; helper function for easier handling of OBP60Formatter + String formatLabel(const double& label); // Format current axis label for printing w/o data format conversion (has been done earlier) public: - // Define default chart range for each boat data type - static std::map dfltChrtRng; + // Define default chart range and range step for each boat data type + static std::map dfltChrtDta; Chart(RingBuffer& dataBuf, char chrtDir, int8_t chrtSz, double dfltRng, CommonData& common, bool useSimuData); // Chart object of data chart ~Chart(); - void showChrt(GwApi::BoatValue currValue, int8_t chrtIntv, bool showCurrValue); // Perform all actions to draw chart + void showChrt(GwApi::BoatValue currValue, int8_t& chrtIntv, bool showCurrValue); // Perform all actions to draw chart }; template -std::map Chart::dfltChrtRng = { - { "formatWind", 60.0 * DEG_TO_RAD }, // default course range 60 degrees - { "formatCourse", 60.0 * DEG_TO_RAD }, // default course range 60 degrees - { "formatKnots", 5.1 }, // default speed range in m/s - { "formatDepth", 15.0 }, // default depth range in m - { "kelvinToC", 30.0 } // default temp range in °C/K +std::map Chart::dfltChrtDta = { + { "formatWind", { 60.0 * DEG_TO_RAD, 10.0 * DEG_TO_RAD } }, // default course range 60 degrees + { "formatCourse", { 60.0 * DEG_TO_RAD, 10.0 * DEG_TO_RAD } }, // default course range 60 degrees + //{ "formatKnots", { 7.71, 2.57 } }, // default speed range in m/s + { "formatKnots", { 7.71, 2.56 } }, // default speed range in m/s + { "formatDepth", { 15.0, 5.0 } }, // default depth range in m + { "kelvinToC", { 30.0, 5.0 } } // default temp range in °C/K }; \ No newline at end of file diff --git a/lib/obp60task/PageOneValue.cpp b/lib/obp60task/PageOneValue.cpp index a9ce6ff..57175f1 100644 --- a/lib/obp60task/PageOneValue.cpp +++ b/lib/obp60task/PageOneValue.cpp @@ -15,8 +15,8 @@ private: bool keylock = false; // Keylock char pageMode = 'V'; // Page mode: 'V' for value, 'C' for chart, 'B' for both - int dataIntv = 1; // Update interval for wind history chart: - // (1)|(2)|(3)|(4)|(8) x 240 seconds for 4, 8, 12, 16, 32 min. history chart + int8_t dataIntv = 1; // Update interval for wind history chart: + // (1)|(2)|(3)|(4)|(8) x 240 seconds for 4, 8, 12, 16, 32 min. history chart //String lengthformat; bool useSimuData; @@ -209,8 +209,8 @@ public: dataHstryBuf = pageData.hstryBuffers->getBuffer(bValName1); if (dataHstryBuf) { - dataFlChart.reset(new Chart(*dataHstryBuf, 'H', 0, Chart::dfltChrtRng[bValFormat], *commonData, useSimuData)); - dataHfChart.reset(new Chart(*dataHstryBuf, 'H', 2, Chart::dfltChrtRng[bValFormat], *commonData, useSimuData)); + dataFlChart.reset(new Chart(*dataHstryBuf, 'H', 0, Chart::dfltChrtDta[bValFormat].range, *commonData, useSimuData)); + dataHfChart.reset(new Chart(*dataHstryBuf, 'H', 2, Chart::dfltChrtDta[bValFormat].range, *commonData, useSimuData)); LOG_DEBUG(GwLog::DEBUG, "PageOneValue: Created chart objects for %s", bValName1); } else { LOG_DEBUG(GwLog::DEBUG, "PageOneValue: No chart objects available for %s", bValName1); @@ -224,8 +224,6 @@ public: { LOG_DEBUG(GwLog::LOG, "Display PageOneValue"); - //GwConfigHandler* config = commonData->config; - //GwLog* logger = commonData->logger; // Get boat value for page GwApi::BoatValue* bValue1 = pageData.values[0]; // Page boat data element @@ -236,24 +234,8 @@ public: setFlashLED(false); } -/* if (!dataFlChart) { // Create chart objects if they don't exist - GwApi::BoatValue* bValue1 = pageData.values[0]; // Page boat data element - String bValName1 = bValue1->getName(); // Value name - String bValFormat = bValue1->getFormat(); // Value format - - dataHstryBuf = pageData.hstryBuffers->getBuffer(bValName1); - - if (dataHstryBuf) { - dataFlChart.reset(new Chart(*dataHstryBuf, 'H', 0, Chart::dfltChrtRng[bValFormat], *commonData, useSimuData)); - dataHfChart.reset(new Chart(*dataHstryBuf, 'H', 2, Chart::dfltChrtRng[bValFormat], *commonData, useSimuData)); - LOG_DEBUG(GwLog::DEBUG, "PageOneValue: Created chart objects for %s", bValName1); - } else { - LOG_DEBUG(GwLog::DEBUG, "PageOneValue: No chart objects available for %s", bValName1); - } - } */ - if (bValue1 == NULL) - return PAGE_OK; // no data, no display of page + return PAGE_OK; // no data, no page to display LOG_DEBUG(GwLog::DEBUG, "PageOneValue: printing %s, %.3f", bValue1->getName().c_str(), bValue1->value); diff --git a/lib/obp60task/PageWindPlot.cpp b/lib/obp60task/PageWindPlot.cpp index 50da88f..49d11d3 100644 --- a/lib/obp60task/PageWindPlot.cpp +++ b/lib/obp60task/PageWindPlot.cpp @@ -19,8 +19,8 @@ private: bool showTruW = true; // Show true wind or apparent wind in chart area bool oldShowTruW = false; // remember recent user selection of wind data type - int dataIntv = 1; // Update interval for wind history chart: - // (1)|(2)|(3)|(4)|(8) x 240 seconds for 4, 8, 12, 16, 32 min. history chart + int8_t dataIntv = 1; // Update interval for wind history chart: + // (1)|(2)|(3)|(4)|(8) x 240 seconds for 4, 8, 12, 16, 32 min. history chart bool useSimuData; // bool holdValues; String flashLED; @@ -50,9 +50,6 @@ private: GwApi::BoatValue* wdBVal = nullptr; GwApi::BoatValue* wsBVal = nullptr; - const double dfltRngWd = 60.0 * DEG_TO_RAD; // default range for course chart from min to max value in RAD - const double dfltRngWs = 7.5; // default range for wind speed chart from min to max value in m/s - public: PageWindPlot(CommonData& common) { @@ -198,12 +195,12 @@ public: twsHstry = pageData.hstryBuffers->getBuffer("TWS"); if (twdHstry) { - twdFlChart.reset(new Chart(*twdHstry, 'V', 0, dfltRngWd, *commonData, useSimuData)); - twdHfChart.reset(new Chart(*twdHstry, 'V', 1, dfltRngWd, *commonData, useSimuData)); + twdFlChart.reset(new Chart(*twdHstry, 'V', 0, Chart::dfltChrtDta["formatCourse"].range, *commonData, useSimuData)); + twdHfChart.reset(new Chart(*twdHstry, 'V', 1, Chart::dfltChrtDta["formatCourse"].range, *commonData, useSimuData)); } if (twsHstry) { - twsFlChart.reset(new Chart(*twsHstry, 'H', 0, dfltRngWs, *commonData, useSimuData)); - twsHfChart.reset(new Chart(*twsHstry, 'V', 2, dfltRngWs, *commonData, useSimuData)); + twsFlChart.reset(new Chart(*twsHstry, 'H', 0, Chart::dfltChrtDta["formatKnots"].range, *commonData, useSimuData)); + twsHfChart.reset(new Chart(*twsHstry, 'V', 2, Chart::dfltChrtDta["formatKnots"].range, *commonData, useSimuData)); } } @@ -212,12 +209,12 @@ public: awsHstry = pageData.hstryBuffers->getBuffer("AWS"); if (awdHstry) { - awdFlChart.reset(new Chart(*awdHstry, 'V', 0, dfltRngWd, *commonData, useSimuData)); - awdHfChart.reset(new Chart(*awdHstry, 'V', 1, dfltRngWd, *commonData, useSimuData)); + awdFlChart.reset(new Chart(*awdHstry, 'V', 0, Chart::dfltChrtDta["formatCourse"].range, *commonData, useSimuData)); + awdHfChart.reset(new Chart(*awdHstry, 'V', 1, Chart::dfltChrtDta["formatCourse"].range, *commonData, useSimuData)); } if (awsHstry) { - awsFlChart.reset(new Chart(*awsHstry, 'H', 0, dfltRngWs, *commonData, useSimuData)); - awsHfChart.reset(new Chart(*awsHstry, 'V', 2, dfltRngWs, *commonData, useSimuData)); + awsFlChart.reset(new Chart(*awsHstry, 'H', 0, Chart::dfltChrtDta["formatKnots"].range, *commonData, useSimuData)); + awsHfChart.reset(new Chart(*awsHstry, 'V', 2, Chart::dfltChrtDta["formatKnots"].range, *commonData, useSimuData)); } if (twdHstry && twsHstry && awdHstry && awsHstry) { LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Created wind charts"); diff --git a/lib/obp60task/Pagedata.h b/lib/obp60task/Pagedata.h index d6f342e..79d1ad9 100644 --- a/lib/obp60task/Pagedata.h +++ b/lib/obp60task/Pagedata.h @@ -207,3 +207,6 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata); // Helper method for conversion of any data value from SI to user defined format (defined in OBP60Formatter) double convertValue(const double &value, const String &format, CommonData &commondata); +double convertValue(const double &value, const String &name, const String &format, CommonData &commondata); +// Helper method for conversion of boat data values from user defined format to SI (defined in OBP60Formatter) +double convertToSItemp(const double &value, const String &name, const String &format, CommonData &commondata); From 2d4f49659d4daa7023841b0fc0dd076f82cc9de5 Mon Sep 17 00:00:00 2001 From: Ulrich Meine Date: Tue, 6 Jan 2026 22:57:07 +0100 Subject: [PATCH 086/121] Code rework for OBPcharts, part 2 --- lib/obp60task/OBPcharts.cpp | 237 ++++++++++++++++++++------------- lib/obp60task/OBPcharts.h | 34 ++--- lib/obp60task/PageOneValue.cpp | 94 +++++++------ lib/obp60task/PageWindPlot.cpp | 140 ++++++++++--------- 4 files changed, 273 insertions(+), 232 deletions(-) diff --git a/lib/obp60task/OBPcharts.cpp b/lib/obp60task/OBPcharts.cpp index 9830653..2e4aaea 100644 --- a/lib/obp60task/OBPcharts.cpp +++ b/lib/obp60task/OBPcharts.cpp @@ -4,6 +4,15 @@ #include "OBPDataOperations.h" #include "OBPRingBuffer.h" +std::map Chart::dfltChrtDta = { + { "formatWind", { 60.0 * DEG_TO_RAD, 10.0 * DEG_TO_RAD } }, // default course range 60 degrees + { "formatCourse", { 60.0 * DEG_TO_RAD, 10.0 * DEG_TO_RAD } }, // default course range 60 degrees + //{ "formatKnots", { 7.71, 2.57 } }, // default speed range in m/s + { "formatKnots", { 7.71, 2.56 } }, // default speed range in m/s + { "formatDepth", { 15.0, 5.0 } }, // default depth range in m + { "kelvinToC", { 30.0, 5.0 } } // default temp range in °C/K +}; + // --- Class Chart --------------- // Chart - object holding the actual chart, incl. data buffer and format definition @@ -12,11 +21,11 @@ // default range of chart, e.g. 30 = [0..30]; // common program data; required for logger and color data // flag to indicate if simulation data is active -template -Chart::Chart(RingBuffer& dataBuf, char chrtDir, int8_t chrtSz, double dfltRng, CommonData& common, bool useSimuData) +// Chart::Chart(RingBuffer& dataBuf, char chrtDir, int8_t chrtSz, double dfltRng, CommonData& common, bool useSimuData) +Chart::Chart(RingBuffer& dataBuf, double dfltRng, CommonData& common, bool useSimuData) : dataBuf(dataBuf) - , chrtDir(chrtDir) - , chrtSz(chrtSz) + //, chrtDir(chrtDir) + //, chrtSz(chrtSz) , dfltRng(dfltRng) , commonData(&common) , useSimuData(useSimuData) @@ -28,51 +37,51 @@ Chart::Chart(RingBuffer& dataBuf, char chrtDir, int8_t chrtSz, double dflt dWidth = getdisplay().width(); dHeight = getdisplay().height(); - if (chrtDir == 'H') { - // horizontal chart timeline direction - timAxis = dWidth - 1; - switch (chrtSz) { - case 0: - valAxis = dHeight - top - bottom; - cRoot = { 0, top - 1 }; - break; - case 1: - valAxis = (dHeight - top - bottom) / 2 - hGap; - cRoot = { 0, top - 1 }; - break; - case 2: - valAxis = (dHeight - top - bottom) / 2 - hGap; - cRoot = { 0, top + (valAxis + hGap) + hGap - 1 }; - break; - default: - LOG_DEBUG(GwLog::ERROR, "obp60:Chart %s: wrong init parameter", dataBuf.getName()); - return; - } + /* if (chrtDir == 'H') { + // horizontal chart timeline direction + timAxis = dWidth - 1; + switch (chrtSz) { + case 0: + valAxis = dHeight - top - bottom; + cRoot = { 0, top - 1 }; + break; + case 1: + valAxis = (dHeight - top - bottom) / 2 - hGap; + cRoot = { 0, top - 1 }; + break; + case 2: + valAxis = (dHeight - top - bottom) / 2 - hGap; + cRoot = { 0, top + (valAxis + hGap) + hGap - 1 }; + break; + default: + LOG_DEBUG(GwLog::ERROR, "obp60:Chart %s: wrong init parameter", dataBuf.getName()); + return; + } - } else if (chrtDir == 'V') { - // vertical chart timeline direction - timAxis = dHeight - top - bottom; - switch (chrtSz) { - case 0: - valAxis = dWidth - 1; - cRoot = { 0, top - 1 }; - break; - case 1: - valAxis = dWidth / 2 - vGap; - cRoot = { 0, top - 1 }; - break; - case 2: - valAxis = dWidth / 2 - vGap; - cRoot = { dWidth / 2 + vGap - 1, top - 1 }; - break; - default: + } else if (chrtDir == 'V') { + // vertical chart timeline direction + timAxis = dHeight - top - bottom; + switch (chrtSz) { + case 0: + valAxis = dWidth - 1; + cRoot = { 0, top - 1 }; + break; + case 1: + valAxis = dWidth / 2 - vGap; + cRoot = { 0, top - 1 }; + break; + case 2: + valAxis = dWidth / 2 - vGap; + cRoot = { dWidth / 2 + vGap - 1, top - 1 }; + break; + default: + LOG_DEBUG(GwLog::ERROR, "obp60:Chart %s: wrong init parameter", dataBuf.getName()); + return; + } + } else { LOG_DEBUG(GwLog::ERROR, "obp60:Chart %s: wrong init parameter", dataBuf.getName()); return; - } - } else { - LOG_DEBUG(GwLog::ERROR, "obp60:Chart %s: wrong init parameter", dataBuf.getName()); - return; - } + } */ dataBuf.getMetaData(dbName, dbFormat); dbMIN_VAL = dataBuf.getMinVal(); @@ -116,9 +125,6 @@ Chart::Chart(RingBuffer& dataBuf, char chrtDir, int8_t chrtSz, double dflt rngStep = 5.0; } - //chrtMin = dbMIN_VAL; - //chrtMax = dbMAX_VAL; - //chrtMid = dbMAX_VAL; // Initialize chart range values chrtMin = zeroValue; chrtMax = chrtMin + dfltRng; @@ -130,35 +136,93 @@ Chart::Chart(RingBuffer& dataBuf, char chrtDir, int8_t chrtSz, double dflt dWidth, dHeight, timAxis, valAxis, cRoot.x, cRoot.y, dbName, rngStep, chrtDataFmt); }; -template -Chart::~Chart() +Chart::~Chart() { } // Perform all actions to draw chart -// Parameters: chart time interval, current boat data value to be printed, current boat data shall be shown yes/no -template -void Chart::showChrt(GwApi::BoatValue currValue, int8_t& chrtIntv, const bool showCurrValue) +// Parameters: : chart timeline direction: 'H' = horizontal, 'V' = vertical +// : chart size: [0] = full size, [1] = half size left/top, [2] half size right/bottom +// : chart timeline interval +// : current boat data shall be shown [true/false] +// : current boat data value to be printed +// void Chart::showChrt(GwApi::BoatValue currValue, int8_t& chrtIntv, const bool showCurrValue) +void Chart::showChrt(char chrtDir, int8_t chrtSz, int8_t& chrtIntv, bool showCurrValue, GwApi::BoatValue currValue) { - drawChrt(chrtIntv, currValue); - drawChrtTimeAxis(chrtIntv); - drawChrtValAxis(); + // this->chrtDir = chrtDir; + // this->chrtSz = chrtSz; + + if (!setChartDimensions(chrtDir, chrtSz)) { + return; // wrong chart dimension parameters + } + + drawChrt(chrtDir, chrtIntv, currValue); + drawChrtTimeAxis(chrtDir, chrtSz, chrtIntv); + drawChrtValAxis(chrtDir, chrtSz); if (!bufDataValid) { // No valid data available - prntNoValidData(); + prntNoValidData(chrtDir); return; } - if (showCurrValue) { // shows latest value from history buffer; usually this should be the most current one + if (showCurrValue) { // show latest value from history buffer; usually this should be the most current one currValue.value = dataBuf.getLast(); currValue.valid = currValue.value != dbMAX_VAL; - Chart::prntCurrValue(currValue); + prntCurrValue(chrtDir, currValue); } } +// define dimensions and start points for chart +bool Chart::setChartDimensions(const char direction, const int8_t size) +{ + if ((direction != 'H' && direction != 'V') || (size < 0 || size > 2)) { + LOG_DEBUG(GwLog::ERROR, "obp60:setChartDimensions %s: wrong parameters", dataBuf.getName()); + return false; + } + + if (direction == 'H') { + // horizontal chart timeline direction + timAxis = dWidth - 1; + switch (size) { + case 0: + valAxis = dHeight - top - bottom; + cRoot = { 0, top - 1 }; + break; + case 1: + valAxis = (dHeight - top - bottom) / 2 - hGap; + cRoot = { 0, top - 1 }; + break; + case 2: + valAxis = (dHeight - top - bottom) / 2 - hGap; + cRoot = { 0, top + (valAxis + hGap) + hGap - 1 }; + break; + } + + } else if (direction == 'V') { + // vertical chart timeline direction + timAxis = dHeight - top - bottom; + switch (size) { + case 0: + valAxis = dWidth - 1; + cRoot = { 0, top - 1 }; + break; + case 1: + valAxis = dWidth / 2 - vGap; + cRoot = { 0, top - 1 }; + break; + case 2: + valAxis = dWidth / 2 - vGap; + cRoot = { dWidth / 2 + vGap - 1, top - 1 }; + break; + } + } + LOG_DEBUG(GwLog::ERROR, "obp60:setChartDimensions %s: direction: %c, size: %d, dWidth: %d, dHeight: %d, timAxis: %d, valAxis: %d, cRoot{%d, %d}, top: %d, bottom: %d, hGap: %d, vGap: %d", + dataBuf.getName(), direction, size, dWidth, dHeight, timAxis, valAxis, cRoot.x, cRoot.y, top, bottom, hGap, vGap); + return true; +} + // draw chart -template -void Chart::drawChrt(int8_t& chrtIntv, GwApi::BoatValue& currValue) +void Chart::drawChrt(const char chrtDir, int8_t& chrtIntv, GwApi::BoatValue& currValue) { double chrtVal; // Current data value double chrtScl; // Scale for data values in pixels per value @@ -167,9 +231,9 @@ void Chart::drawChrt(int8_t& chrtIntv, GwApi::BoatValue& currValue) getBufStartNSize(chrtIntv); - // LOG_DEBUG(GwLog::DEBUG, "PageOneValue:drawChart: min: %.1f, mid: %.1f, max: %.1f, rng: %.1f", chrtMin, chrtMid, chrtMax, chrtRng); + // LOG_DEBUG(GwLog::DEBUG, "Chart:drawChart: min: %.1f, mid: %.1f, max: %.1f, rng: %.1f", chrtMin, chrtMid, chrtMax, chrtRng); calcChrtBorders(chrtMin, chrtMid, chrtMax, chrtRng); - LOG_DEBUG(GwLog::DEBUG, "PageOneValue:drawChart2: min: %.1f, mid: %.1f, max: %.1f, rng: %.1f", chrtMin, chrtMid, chrtMax, chrtRng); + LOG_DEBUG(GwLog::DEBUG, "Chart:drawChart2: min: %.1f, mid: %.1f, max: %.1f, rng: %.1f", chrtMin, chrtMid, chrtMax, chrtRng); // Chart scale: pixels per value step chrtScl = double(valAxis) / chrtRng; @@ -290,8 +354,7 @@ void Chart::drawChrt(int8_t& chrtIntv, GwApi::BoatValue& currValue) } // Identify buffer size and buffer start position for chart -template -void Chart::getBufStartNSize(int8_t& chrtIntv) +void Chart::getBufStartNSize(int8_t& chrtIntv) { count = dataBuf.getCurrentSize(); currIdx = dataBuf.getLastIdx(); @@ -314,8 +377,7 @@ void Chart::getBufStartNSize(int8_t& chrtIntv) } // check and adjust chart range and set range borders and range middle -template -void Chart::calcChrtBorders(double& rngMin, double& rngMid, double& rngMax, double& rng) +void Chart::calcChrtBorders(double& rngMin, double& rngMid, double& rngMax, double& rng) { if (chrtDataFmt == 'W' || chrtDataFmt == 'R') { // Chart data is of type 'course', 'wind' or 'rot' @@ -413,8 +475,7 @@ void Chart::calcChrtBorders(double& rngMin, double& rngMid, double& rngMax, d } // chart time axis label + lines -template -void Chart::drawChrtTimeAxis(int8_t& chrtIntv) +void Chart::drawChrtTimeAxis(const char chrtDir, const int8_t chrtSz, int8_t& chrtIntv) { float axSlots, intv, i; char sTime[6]; @@ -461,8 +522,7 @@ void Chart::drawChrtTimeAxis(int8_t& chrtIntv) } // chart value axis labels + lines -template -void Chart::drawChrtValAxis() +void Chart::drawChrtValAxis(const char chrtDir, const int8_t chrtSz) { double axLabel; double cVal; @@ -538,8 +598,7 @@ void Chart::drawChrtValAxis() } // Print current data value -template -void Chart::prntCurrValue(GwApi::BoatValue& currValue) +void Chart::prntCurrValue(const char chrtDir, GwApi::BoatValue& currValue) { const int xPosVal = (chrtDir == 'H') ? cRoot.x + (timAxis / 2) - 56 : cRoot.x + 32; const int yPosVal = (chrtDir == 'H') ? cRoot.y + valAxis - 7 : cRoot.y + timAxis - 7; @@ -566,8 +625,7 @@ void Chart::prntCurrValue(GwApi::BoatValue& currValue) } // print message for no valid data availabletemplate -template -void Chart::prntNoValidData() +void Chart::prntNoValidData(const char chrtDir) { int pX, pY; @@ -588,8 +646,7 @@ void Chart::prntNoValidData() } // Get maximum difference of last of dataBuf ringbuffer values to center chart; for angle data only -template -double Chart::getAngleRng(double center, size_t amount) +double Chart::getAngleRng(double center, size_t amount) { size_t count = dataBuf.getCurrentSize(); @@ -624,8 +681,7 @@ double Chart::getAngleRng(double center, size_t amount) } // print horizontal axis label with only three values: top, mid, and bottom -template -void Chart::prntHorizThreeValueAxisLabel(const GFXfont* font) +void Chart::prntHorizThreeValueAxisLabel(const GFXfont* font) { double axLabel; double chrtMin, chrtMid, chrtMax; @@ -637,7 +693,7 @@ void Chart::prntHorizThreeValueAxisLabel(const GFXfont* font) yOffset = 15; } else if (font == &Ubuntu_Bold12pt8b) { xOffset = 51; - yOffset = 17; + yOffset = 18; } getdisplay().setFont(font); @@ -658,8 +714,8 @@ void Chart::prntHorizThreeValueAxisLabel(const GFXfont* font) // print mid axis label axLabel = chrtMid; sVal = formatLabel(axLabel); - getdisplay().fillRect(cRoot.x, cRoot.y + (valAxis / 2) - 9, xOffset + 4, 16, bgColor); // Clear small area to remove potential chart lines - drawTextRalign(cRoot.x + xOffset, cRoot.y + (valAxis / 2) + 5, sVal); // range value + getdisplay().fillRect(cRoot.x, cRoot.y + (valAxis / 2) - 8, xOffset + 4, 16, bgColor); // Clear small area to remove potential chart lines + drawTextRalign(cRoot.x + xOffset, cRoot.y + (valAxis / 2) + 6, sVal); // range value getdisplay().drawLine(cRoot.x + xOffset + 4, cRoot.y + (valAxis / 2), cRoot.x + timAxis, cRoot.y + (valAxis / 2), fgColor); // print bottom axis label @@ -671,8 +727,7 @@ void Chart::prntHorizThreeValueAxisLabel(const GFXfont* font) } // print horizontal axis label with multiple axis lines -template -void Chart::prntHorizMultiValueAxisLabel(const GFXfont* font) +void Chart::prntHorizMultiValueAxisLabel(const GFXfont* font) { double chrtMin, chrtMax, chrtRng; double axSlots, axIntv, axLabel; @@ -724,8 +779,7 @@ void Chart::prntHorizMultiValueAxisLabel(const GFXfont* font) } // Draw chart line with thickness of 2px -template -void Chart::drawBoldLine(int16_t x1, int16_t y1, int16_t x2, int16_t y2) +void Chart::drawBoldLine(int16_t x1, int16_t y1, int16_t x2, int16_t y2) { int16_t dx = std::abs(x2 - x1); @@ -741,8 +795,7 @@ void Chart::drawBoldLine(int16_t x1, int16_t y1, int16_t x2, int16_t y2) } // Convert and format current axis label to user defined format; helper function for easier handling of OBP60Formatter -template -String Chart::convNformatLabel(double label) +String Chart::convNformatLabel(double label) { GwApi::BoatValue tmpBVal(dbName); // temporary boat value for string formatter String sVal; @@ -759,8 +812,7 @@ String Chart::convNformatLabel(double label) } // Format current axis label for printing w/o data format conversion (has been done earlier) -template -String Chart::formatLabel(const double& label) +String Chart::formatLabel(const double& label) { char sVal[11]; @@ -786,7 +838,4 @@ String Chart::formatLabel(const double& label) return String(sVal); } - -// Explicitly instantiate class with required data types to avoid linker errors -template class Chart; // --- Class Chart --------------- diff --git a/lib/obp60task/OBPcharts.h b/lib/obp60task/OBPcharts.h index db62a3c..c6d24b2 100644 --- a/lib/obp60task/OBPcharts.h +++ b/lib/obp60task/OBPcharts.h @@ -17,15 +17,14 @@ template class RingBuffer; class GwLog; -template class Chart { protected: CommonData* commonData; GwLog* logger; - RingBuffer& dataBuf; // Buffer to display - char chrtDir; // Chart timeline direction: 'H' = horizontal, 'V' = vertical - int8_t chrtSz; // Chart size: [0] = full size, [1] = half size left/top, [2] half size right/bottom + RingBuffer& dataBuf; // Buffer to display + //char chrtDir; // Chart timeline direction: 'H' = horizontal, 'V' = vertical + //int8_t chrtSz; // Chart size: [0] = full size, [1] = half size left/top, [2] half size right/bottom double dfltRng; // Default range of chart, e.g. 30 = [0..30] uint16_t fgColor; // color code for any screen writing uint16_t bgColor; // color code for screen background @@ -72,13 +71,14 @@ protected: static constexpr int8_t THRESHOLD_NO_DATA = 3; static constexpr int8_t VALAXIS_STEP = 60; - void drawChrt(int8_t& chrtIntv, GwApi::BoatValue& currValue); // Draw chart line + bool setChartDimensions(const char direction, const int8_t size); //define dimensions and start points for chart + void drawChrt(const char chrtDir, int8_t& chrtIntv, GwApi::BoatValue& currValue); // Draw chart line void getBufStartNSize(int8_t& chrtIntv); // Identify buffer size and buffer start position for chart void calcChrtBorders(double& rngMin, double& rngMid, double& rngMax, double& rng); // Calculate chart points for value axis and return range between and - void drawChrtTimeAxis(int8_t& chrtIntv); // Draw time axis of chart, value and lines - void drawChrtValAxis(); // Draw value axis of chart, value and lines - void prntCurrValue(GwApi::BoatValue& currValue); // Add current boat data value to chart - void prntNoValidData(); // print message for no valid data available + void drawChrtTimeAxis(const char chrtDir, const int8_t chrtSz, int8_t& chrtIntv); // Draw time axis of chart, value and lines + void drawChrtValAxis(const char chrtDir, const int8_t chrtSz); // Draw value axis of chart, value and lines + void prntCurrValue(const char chrtDir, GwApi::BoatValue& currValue); // Add current boat data value to chart + void prntNoValidData(const char chrtDir); // print message for no valid data available double getAngleRng(double center, size_t amount); // Calculate range between chart center and edges void prntHorizThreeValueAxisLabel(const GFXfont* font); // print horizontal axis label with only three values: top, mid, and bottom void prntHorizMultiValueAxisLabel(const GFXfont* font); // print horizontal axis label with multiple axis lines @@ -90,17 +90,9 @@ public: // Define default chart range and range step for each boat data type static std::map dfltChrtDta; - Chart(RingBuffer& dataBuf, char chrtDir, int8_t chrtSz, double dfltRng, CommonData& common, bool useSimuData); // Chart object of data chart + // Chart(RingBuffer& dataBuf, char chrtDir, int8_t chrtSz, double dfltRng, CommonData& common, bool useSimuData); // Chart object of data chart + Chart(RingBuffer& dataBuf, double dfltRng, CommonData& common, bool useSimuData); // Chart object of data chart ~Chart(); - void showChrt(GwApi::BoatValue currValue, int8_t& chrtIntv, bool showCurrValue); // Perform all actions to draw chart + // void showChrt(GwApi::BoatValue currValue, int8_t& chrtIntv, bool showCurrValue); // Perform all actions to draw chart + void showChrt(char chrtDir, int8_t chrtSz, int8_t& chrtIntv, bool showCurrValue, GwApi::BoatValue currValue); // Perform all actions to draw chart }; - -template -std::map Chart::dfltChrtDta = { - { "formatWind", { 60.0 * DEG_TO_RAD, 10.0 * DEG_TO_RAD } }, // default course range 60 degrees - { "formatCourse", { 60.0 * DEG_TO_RAD, 10.0 * DEG_TO_RAD } }, // default course range 60 degrees - //{ "formatKnots", { 7.71, 2.57 } }, // default speed range in m/s - { "formatKnots", { 7.71, 2.56 } }, // default speed range in m/s - { "formatDepth", { 15.0, 5.0 } }, // default depth range in m - { "kelvinToC", { 30.0, 5.0 } } // default temp range in °C/K -}; \ No newline at end of file diff --git a/lib/obp60task/PageOneValue.cpp b/lib/obp60task/PageOneValue.cpp index 57175f1..59a02bf 100644 --- a/lib/obp60task/PageOneValue.cpp +++ b/lib/obp60task/PageOneValue.cpp @@ -2,8 +2,8 @@ #include "Pagedata.h" #include "OBP60Extensions.h" -#include "OBPDataOperations.h" #include "BoatDataCalibration.h" +#include "OBPDataOperations.h" #include "OBPcharts.h" class PageOneValue : public Page { @@ -18,7 +18,7 @@ private: int8_t dataIntv = 1; // Update interval for wind history chart: // (1)|(2)|(3)|(4)|(8) x 240 seconds for 4, 8, 12, 16, 32 min. history chart - //String lengthformat; + // String lengthformat; bool useSimuData; bool holdValues; String flashLED; @@ -31,7 +31,7 @@ private: // Data buffer pointer (owned by HstryBuffers) RingBuffer* dataHstryBuf = nullptr; - std::unique_ptr> dataFlChart, dataHfChart; // Chart object, full and half size + std::unique_ptr dataChart; // Chart object, full and half size void showData(GwApi::BoatValue* bValue1, char size) { @@ -124,7 +124,7 @@ public: height = getdisplay().height(); // Screen height // Get config data - //lengthformat = commonData->config->getString(commonData->config->lengthFormat); + // lengthformat = commonData->config->getString(commonData->config->lengthFormat); useSimuData = commonData->config->getBool(commonData->config->useSimuData); holdValues = commonData->config->getBool(commonData->config->holdvalues); flashLED = commonData->config->getString(commonData->config->flashLED); @@ -136,51 +136,57 @@ public: { Page::setupKeys(); +#if defined BOARD_OBP60S3 + constexpr int ZOOM_IDX = 4; +#elif defined BOARD_OBP40S3 + constexpr int ZOOM_IDX = 1; +#endif + if (dataHstryBuf) { // show "Mode" key only if chart supported boat data type is available commonData->keydata[0].label = "MODE"; + commonData->keydata[ZOOM_IDX].label = "ZOOM"; } else { commonData->keydata[0].label = ""; + commonData->keydata[ZOOM_IDX].label = ""; } -#if defined BOARD_OBP60S3 - commonData->keydata[4].label = "ZOOM"; -#elif defined BOARD_OBP40S3 - commonData->keydata[1].label = "ZOOM"; -#endif } // Key functions virtual int handleKey(int key) { - // Set page mode value | full chart | value/half chart - if (key == 1) { - if (pageMode == 'V') { - pageMode = 'C'; - } else if (pageMode == 'C') { - pageMode = 'B'; - } else { - pageMode = 'V'; - } - return 0; // Commit the key - } + if (dataHstryBuf) { // if boat data type supports charts - // Set interval for history chart update time (interval) -#if defined BOARD_OBP60S3 - if (key == 5) { -#elif defined BOARD_OBP40S3 - if (key == 2) { -#endif - if (dataIntv == 1) { - dataIntv = 2; - } else if (dataIntv == 2) { - dataIntv = 3; - } else if (dataIntv == 3) { - dataIntv = 4; - } else if (dataIntv == 4) { - dataIntv = 8; - } else { - dataIntv = 1; + // Set page mode value | full chart | value/half chart + if (key == 1) { + if (pageMode == 'V') { + pageMode = 'C'; + } else if (pageMode == 'C') { + pageMode = 'B'; + } else { + pageMode = 'V'; + } + return 0; // Commit the key + } + + // Set time frame to show for history chart +#if defined BOARD_OBP60S3 + if (key == 5) { +#elif defined BOARD_OBP40S3 + if (key == 2) { +#endif + if (dataIntv == 1) { + dataIntv = 2; + } else if (dataIntv == 2) { + dataIntv = 3; + } else if (dataIntv == 3) { + dataIntv = 4; + } else if (dataIntv == 4) { + dataIntv = 8; + } else { + dataIntv = 1; + } + return 0; // Commit the key } - return 0; // Commit the key } // Keylock function @@ -201,7 +207,7 @@ public: } #endif // buffer initialization will fail, if page is default page, because is not executed at system start for default page - if (!dataFlChart) { // Create chart objects if they don't exist + if (!dataChart) { // Create chart objects if they don't exist GwApi::BoatValue* bValue1 = pageData.values[0]; // Page boat data element String bValName1 = bValue1->getName(); // Value name String bValFormat = bValue1->getFormat(); // Value format @@ -209,8 +215,8 @@ public: dataHstryBuf = pageData.hstryBuffers->getBuffer(bValName1); if (dataHstryBuf) { - dataFlChart.reset(new Chart(*dataHstryBuf, 'H', 0, Chart::dfltChrtDta[bValFormat].range, *commonData, useSimuData)); - dataHfChart.reset(new Chart(*dataHstryBuf, 'H', 2, Chart::dfltChrtDta[bValFormat].range, *commonData, useSimuData)); + dataChart.reset(new Chart(*dataHstryBuf, Chart::dfltChrtDta[bValFormat].range, *commonData, useSimuData)); + //dataHfChart.reset(new Chart(*dataHstryBuf, 'H', 2, Chart::dfltChrtDta[bValFormat].range, *commonData, useSimuData)); LOG_DEBUG(GwLog::DEBUG, "PageOneValue: Created chart objects for %s", bValName1); } else { LOG_DEBUG(GwLog::DEBUG, "PageOneValue: No chart objects available for %s", bValName1); @@ -249,14 +255,14 @@ public: showData(bValue1, 'F'); } else if (pageMode == 'C') { // show only data chart - if (dataFlChart) { - dataFlChart->showChrt(*bValue1, dataIntv, true); + if (dataChart) { + dataChart->showChrt('H', 0, dataIntv, true, *bValue1); } } else if (pageMode == 'B') { // show data value and chart showData(bValue1, 'H'); - if (dataHfChart) { - dataHfChart->showChrt(*bValue1, dataIntv, false); + if (dataChart) { + dataChart->showChrt('H', 2, dataIntv, false, *bValue1); } } diff --git a/lib/obp60task/PageWindPlot.cpp b/lib/obp60task/PageWindPlot.cpp index 49d11d3..d90bab9 100644 --- a/lib/obp60task/PageWindPlot.cpp +++ b/lib/obp60task/PageWindPlot.cpp @@ -11,6 +11,15 @@ class PageWindPlot : public Page { private: GwLog* logger; + static constexpr char SHOW_WIND_DIR = 'D'; + static constexpr char SHOW_WIND_SPEED = 'S'; + static constexpr char SHOW_BOTH = 'B'; + static constexpr char HORIZONTAL = 'H'; + static constexpr char VERTICAL = 'V'; + static constexpr int8_t FULL_SIZE = 0; + static constexpr int8_t HALF_SIZE_TOP = 1; + static constexpr int8_t HALF_SIZE_BOTTOM = 2; + int width; // Screen width int height; // Screen height @@ -37,16 +46,12 @@ private: RingBuffer* awsHstry = nullptr; // Chart objects - std::unique_ptr> twdFlChart, awdFlChart; // Chart object for wind direction, full size - std::unique_ptr> twsFlChart, awsFlChart; // Chart object for wind speed, full size - std::unique_ptr> twdHfChart, awdHfChart; // Chart object for wind direction, half size - std::unique_ptr> twsHfChart, awsHfChart; // Chart object for wind speed, half size + std::unique_ptr twdChart, awdChart; // Chart object for wind direction, full size + std::unique_ptr twsChart, awsChart; // Chart object for wind speed, full size // Active charts and values - Chart* wdFlChart = nullptr; - Chart* wsFlChart = nullptr; - Chart* wdHfChart = nullptr; - Chart* wsHfChart = nullptr; + Chart* wdChart = nullptr; + Chart* wsChart = nullptr; GwApi::BoatValue* wdBVal = nullptr; GwApi::BoatValue* wsBVal = nullptr; @@ -86,12 +91,12 @@ public: { // Set chart mode TWD | TWS if (key == 1) { - if (chrtMode == 'D') { - chrtMode = 'S'; - } else if (chrtMode == 'S') { - chrtMode = 'B'; + if (chrtMode == SHOW_WIND_DIR) { + chrtMode = SHOW_WIND_SPEED; + } else if (chrtMode == SHOW_WIND_SPEED) { + chrtMode = SHOW_BOTH; } else { - chrtMode = 'D'; + chrtMode = SHOW_WIND_DIR; } return 0; // Commit the key } @@ -150,39 +155,36 @@ public: oldShowTruW = !showTruW; // Force chart update in displayPage #endif - // buffer initialization cannot be performed here, because is not executed at system start for default page - /* if (!twdFlChart) { // Create true wind charts if they don't exist + // With chart object initialization being performed here, PageWindPlot won't properly work as default page, + // because is not executed at system start for default page + if (!twdChart) { // Create true wind charts if they don't exist twdHstry = pageData.hstryBuffers->getBuffer("TWD"); twsHstry = pageData.hstryBuffers->getBuffer("TWS"); if (twdHstry) { - twdFlChart.reset(new Chart(*twdHstry, 'V', 0, dfltRngWd, *commonData, useSimuData)); - twdHfChart.reset(new Chart(*twdHstry, 'V', 1, dfltRngWd, *commonData, useSimuData)); + twdChart.reset(new Chart(*twdHstry, Chart::dfltChrtDta["formatCourse"].range, *commonData, useSimuData)); } if (twsHstry) { - twsFlChart.reset(new Chart(*twsHstry, 'H', 0, dfltRngWs, *commonData, useSimuData)); - twsHfChart.reset(new Chart(*twsHstry, 'V', 2, dfltRngWs, *commonData, useSimuData)); + twsChart.reset(new Chart(*twsHstry, Chart::dfltChrtDta["formatKnots"].range, *commonData, useSimuData)); } } - if (!awdFlChart) { // Create apparent wind charts if they don't exist + if (!awdChart) { // Create apparent wind charts if they don't exist awdHstry = pageData.hstryBuffers->getBuffer("AWD"); awsHstry = pageData.hstryBuffers->getBuffer("AWS"); if (awdHstry) { - awdFlChart.reset(new Chart(*awdHstry, 'V', 0, dfltRngWd, *commonData, useSimuData)); - awdHfChart.reset(new Chart(*awdHstry, 'V', 1, dfltRngWd, *commonData, useSimuData)); + awdChart.reset(new Chart(*awdHstry, Chart::dfltChrtDta["formatCourse"].range, *commonData, useSimuData)); } if (awsHstry) { - awsFlChart.reset(new Chart(*awsHstry, 'H', 0, dfltRngWs, *commonData, useSimuData)); - awsHfChart.reset(new Chart(*awsHstry, 'V', 2, dfltRngWs, *commonData, useSimuData)); + awsChart.reset(new Chart(*awsHstry, Chart::dfltChrtDta["formatKnots"].range, *commonData, useSimuData)); } if (twdHstry && twsHstry && awdHstry && awsHstry) { LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Created wind charts"); } else { LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Some/all chart objects for wind data missing"); } - } */ + } } int displayPage(PageData& pageData) @@ -190,54 +192,46 @@ public: LOG_DEBUG(GwLog::LOG, "Display PageWindPlot"); ulong pageTime = millis(); - if (!twdFlChart) { // Create true wind charts if they don't exist - twdHstry = pageData.hstryBuffers->getBuffer("TWD"); - twsHstry = pageData.hstryBuffers->getBuffer("TWS"); + /* if (!twdChart) { // Create true wind charts if they don't exist + twdHstry = pageData.hstryBuffers->getBuffer("TWD"); + twsHstry = pageData.hstryBuffers->getBuffer("TWS"); - if (twdHstry) { - twdFlChart.reset(new Chart(*twdHstry, 'V', 0, Chart::dfltChrtDta["formatCourse"].range, *commonData, useSimuData)); - twdHfChart.reset(new Chart(*twdHstry, 'V', 1, Chart::dfltChrtDta["formatCourse"].range, *commonData, useSimuData)); - } - if (twsHstry) { - twsFlChart.reset(new Chart(*twsHstry, 'H', 0, Chart::dfltChrtDta["formatKnots"].range, *commonData, useSimuData)); - twsHfChart.reset(new Chart(*twsHstry, 'V', 2, Chart::dfltChrtDta["formatKnots"].range, *commonData, useSimuData)); - } - } + if (twdHstry) { + twdChart.reset(new Chart(*twdHstry, Chart::dfltChrtDta["formatCourse"].range, *commonData, useSimuData)); + } + if (twsHstry) { + twsChart.reset(new Chart(*twsHstry, Chart::dfltChrtDta["formatKnots"].range, *commonData, useSimuData)); + } + } - if (!awdFlChart) { // Create apparent wind charts if they don't exist - awdHstry = pageData.hstryBuffers->getBuffer("AWD"); - awsHstry = pageData.hstryBuffers->getBuffer("AWS"); + if (!awdChart) { // Create apparent wind charts if they don't exist + awdHstry = pageData.hstryBuffers->getBuffer("AWD"); + awsHstry = pageData.hstryBuffers->getBuffer("AWS"); - if (awdHstry) { - awdFlChart.reset(new Chart(*awdHstry, 'V', 0, Chart::dfltChrtDta["formatCourse"].range, *commonData, useSimuData)); - awdHfChart.reset(new Chart(*awdHstry, 'V', 1, Chart::dfltChrtDta["formatCourse"].range, *commonData, useSimuData)); - } - if (awsHstry) { - awsFlChart.reset(new Chart(*awsHstry, 'H', 0, Chart::dfltChrtDta["formatKnots"].range, *commonData, useSimuData)); - awsHfChart.reset(new Chart(*awsHstry, 'V', 2, Chart::dfltChrtDta["formatKnots"].range, *commonData, useSimuData)); - } - if (twdHstry && twsHstry && awdHstry && awsHstry) { - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Created wind charts"); - } else { - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Some/all chart objects for wind data missing"); - } - } + if (awdHstry) { + awdChart.reset(new Chart(*awdHstry, Chart::dfltChrtDta["formatCourse"].range, *commonData, useSimuData)); + } + if (awsHstry) { + awsChart.reset(new Chart(*awsHstry, Chart::dfltChrtDta["formatKnots"].range, *commonData, useSimuData)); + } + if (twdHstry && twsHstry && awdHstry && awsHstry) { + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Created wind charts"); + } else { + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Some/all chart objects for wind data missing"); + } + } */ if (showTruW != oldShowTruW) { // Switch active charts based on showTruW if (showTruW) { - wdFlChart = twdFlChart.get(); - wsFlChart = twsFlChart.get(); - wdHfChart = twdHfChart.get(); - wsHfChart = twsHfChart.get(); + wdChart = twdChart.get(); + wsChart = twsChart.get(); wdBVal = pageData.values[0]; wsBVal = pageData.values[1]; } else { - wdFlChart = awdFlChart.get(); - wsFlChart = awsFlChart.get(); - wdHfChart = awdHfChart.get(); - wsHfChart = awsHfChart.get(); + wdChart = awdChart.get(); + wsChart = awsChart.get(); wdBVal = pageData.values[2]; wsBVal = pageData.values[3]; } @@ -253,22 +247,22 @@ public: getdisplay().setPartialWindow(0, 0, width, height); // Set partial update getdisplay().setTextColor(commonData->fgcolor); - if (chrtMode == 'D') { - if (wdFlChart) { - wdFlChart->showChrt(*wdBVal, dataIntv, true); + if (chrtMode == SHOW_WIND_DIR) { + if (wdChart) { + wdChart->showChrt(VERTICAL, FULL_SIZE, dataIntv, true, *wdBVal); } - } else if (chrtMode == 'S') { - if (wsFlChart) { - wsFlChart->showChrt(*wsBVal, dataIntv, true); + } else if (chrtMode == SHOW_WIND_SPEED) { + if (wsChart) { + wsChart->showChrt(HORIZONTAL, FULL_SIZE, dataIntv, true, *wsBVal); } - } else if (chrtMode == 'B') { - if (wdHfChart) { - wdHfChart->showChrt(*wdBVal, dataIntv, true); + } else if (chrtMode == SHOW_BOTH) { + if (wdChart) { + wdChart->showChrt(VERTICAL, HALF_SIZE_TOP, dataIntv, true, *wdBVal); } - if (wsHfChart) { - wsHfChart->showChrt(*wsBVal, dataIntv, true); + if (wsChart) { + wsChart->showChrt(VERTICAL, HALF_SIZE_BOTTOM, dataIntv, true, *wsBVal); } } From 84736e6769c7f51b5c3ab2ca3795f4f24213d008 Mon Sep 17 00:00:00 2001 From: Ulrich Meine Date: Sat, 10 Jan 2026 01:50:19 +0100 Subject: [PATCH 087/121] OBP60Formatter: add option to switch of creation of simulation data and use pure conversion/formatting function --- lib/obp60task/OBP60Formatter.cpp | 24 ++++++++++++++++++++---- lib/obp60task/Pagedata.h | 3 +-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/lib/obp60task/OBP60Formatter.cpp b/lib/obp60task/OBP60Formatter.cpp index 456eab0..ebb2142 100644 --- a/lib/obp60task/OBP60Formatter.cpp +++ b/lib/obp60task/OBP60Formatter.cpp @@ -49,7 +49,15 @@ String formatLongitude(double lon) { return String(degree, 0) + "\x90 " + String(minute, 4) + "' " + ((lon > 0) ? "E" : "W"); } -FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ +// Convert and format boat value from SI to user defined format (definition for compatibility purposes) +FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata) { + + return formatValue(value, commondata, false); // call with standard handling of user setting for simulation data +} + +// Convert and format boat value from SI to user defined format +// generate random simulation data; can be deselected to use conversion+formatting function even in simulation mode +FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool ignoreSimuDataSetting){ GwLog *logger = commondata.logger; FormattedData result; static int dayoffset = 0; @@ -66,9 +74,15 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ String windspeedFormat = commondata.config->getString(commondata.config->windspeedFormat); // [m/s|km/h|kn|bft] String tempFormat = commondata.config->getString(commondata.config->tempFormat); // [K|°C|°F] String dateFormat = commondata.config->getString(commondata.config->dateFormat); // [DE|GB|US] - bool usesimudata = commondata.config->getBool(commondata.config->useSimuData); // [on|off] String precision = commondata.config->getString(commondata.config->valueprecision); // [1|2] + bool usesimudata; + if (ignoreSimuDataSetting){ + usesimudata = false; // ignore user setting for simulation data; we want to format the boat value passed to this function + } else { + usesimudata = commondata.config->getBool(commondata.config->useSimuData); // [on|off] + } + // If boat value not valid if (! value->valid && !usesimudata){ result.svalue = "---"; @@ -881,11 +895,12 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata){ return result; } -// Helper method for conversion of boat data values from SI to user defined format +// Helper method for conversion of any data value from SI to user defined format double convertValue(const double &value, const String &name, const String &format, CommonData &commondata) { std::unique_ptr tmpBValue; // Temp variable to get converted data value from double result; // data value converted to user defined target data format + constexpr bool NO_SIMUDATA = true; // switch off simulation feature of function // prepare temporary BoatValue structure for use in tmpBValue = std::unique_ptr(new GwApi::BoatValue(name)); // we don't need boat value name for pure value conversion @@ -893,10 +908,11 @@ double convertValue(const double &value, const String &name, const String &forma tmpBValue->valid = true; tmpBValue->value = value; - result = formatValue(tmpBValue.get(), commondata).cvalue; // get value (converted) + result = formatValue(tmpBValue.get(), commondata, NO_SIMUDATA).cvalue; // get value (converted); ignore any simulation data setting return result; } +// Helper method for conversion of any data value from SI to user defined format double convertValue(const double &value, const String &format, CommonData &commondata) { double result; // data value converted to user defined target data format diff --git a/lib/obp60task/Pagedata.h b/lib/obp60task/Pagedata.h index 79d1ad9..02afba9 100644 --- a/lib/obp60task/Pagedata.h +++ b/lib/obp60task/Pagedata.h @@ -204,9 +204,8 @@ typedef struct{ // Formatter for boat values FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata); +FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool ignoreSimuDataSetting); // Helper method for conversion of any data value from SI to user defined format (defined in OBP60Formatter) double convertValue(const double &value, const String &format, CommonData &commondata); double convertValue(const double &value, const String &name, const String &format, CommonData &commondata); -// Helper method for conversion of boat data values from user defined format to SI (defined in OBP60Formatter) -double convertToSItemp(const double &value, const String &name, const String &format, CommonData &commondata); From 4747336a699f9b8b960bedc40edb56fbdc4b642f Mon Sep 17 00:00:00 2001 From: Ulrich Meine Date: Sat, 10 Jan 2026 12:31:37 +0100 Subject: [PATCH 088/121] Code rework for OBPcharts, part 3 --- lib/obp60task/OBPDataOperations.h | 52 +-- lib/obp60task/OBPcharts.cpp | 576 ++++++++++++++---------------- lib/obp60task/OBPcharts.h | 54 ++- lib/obp60task/PageOneValue.cpp | 78 ++-- lib/obp60task/PageWindPlot.cpp | 79 ++-- 5 files changed, 413 insertions(+), 426 deletions(-) diff --git a/lib/obp60task/OBPDataOperations.h b/lib/obp60task/OBPDataOperations.h index 7cb4320..0fb8647 100644 --- a/lib/obp60task/OBPDataOperations.h +++ b/lib/obp60task/OBPDataOperations.h @@ -1,8 +1,8 @@ // Function lib for history buffer handling, true wind calculation, and other operations on boat data #pragma once #include "OBPRingBuffer.h" -#include "obp60task.h" #include "Pagedata.h" +#include "obp60task.h" #include class HstryBuf { @@ -11,8 +11,8 @@ private: String boatDataName; double hstryMin; double hstryMax; - GwApi::BoatValue *boatValue; - GwLog *logger; + GwApi::BoatValue* boatValue; + GwLog* logger; friend class HstryBuffers; @@ -32,31 +32,32 @@ private: GwApi::BoatValue *awaBVal, *hdtBVal, *hdmBVal, *varBVal, *cogBVal, *sogBVal, *awdBVal; // boat values for true wind calculation struct HistoryParams { - int hstryUpdFreq; - int mltplr; - double bufferMinVal; - double bufferMaxVal; - String format; + int hstryUpdFreq; // update frequency of history buffer (documentation only) + int mltplr; // specifies actual value precision being storable: + // [10000: 0 - 6.5535 | 1000: 0 - 65.535 | 100: 0 - 650.35 | 10: 0 - 6503.5 + double bufferMinVal; // minimum valid data value + double bufferMaxVal; // maximum valid data value + String format; // format of data type }; // Define buffer parameters for supported boat data type std::map bufferParams = { - {"AWA", {1000, 10000, 0.0, M_TWOPI, "formatWind"}}, - {"AWD", {1000, 10000, 0.0, M_TWOPI, "formatCourse"}}, - {"AWS", {1000, 1000, 0.0, 65.0, "formatKnots"}}, - {"COG", {1000, 10000, 0.0, M_TWOPI, "formatCourse"}}, - {"DBS", {1000, 100, 0.0, 650.0, "formatDepth"}}, - {"DBT", {1000, 100, 0.0, 650.0, "formatDepth"}}, - {"DPT", {1000, 100, 0.0, 650.0, "formatDepth"}}, - {"HDM", {1000, 10000, 0.0, M_TWOPI, "formatCourse"}}, - {"HDT", {1000, 10000, 0.0, M_TWOPI, "formatCourse"}}, - {"ROT", {1000, 10000, -M_PI / 180.0 * 99.0, M_PI / 180.0 * 99.0, "formatRot"}}, // min/max is -/+ 99 degrees for "rate of turn" - {"SOG", {1000, 1000, 0.0, 65.0, "formatKnots"}}, - {"STW", {1000, 1000, 0.0, 65.0, "formatKnots"}}, - {"TWA", {1000, 10000, 0.0, M_TWOPI, "formatWind"}}, - {"TWD", {1000, 10000, 0.0, M_TWOPI, "formatCourse"}}, - {"TWS", {1000, 1000, 0.0, 65.0, "formatKnots"}}, - {"WTemp", {1000, 100, 233.0, 650.0, "kelvinToC"}} // [-50..376] °C + { "AWA", { 1000, 10000, 0.0, M_TWOPI, "formatWind" } }, + { "AWD", { 1000, 10000, 0.0, M_TWOPI, "formatCourse" } }, + { "AWS", { 1000, 1000, 0.0, 65.0, "formatKnots" } }, + { "COG", { 1000, 10000, 0.0, M_TWOPI, "formatCourse" } }, + { "DBS", { 1000, 100, 0.0, 650.0, "formatDepth" } }, + { "DBT", { 1000, 100, 0.0, 650.0, "formatDepth" } }, + { "DPT", { 1000, 100, 0.0, 650.0, "formatDepth" } }, + { "HDM", { 1000, 10000, 0.0, M_TWOPI, "formatCourse" } }, + { "HDT", { 1000, 10000, 0.0, M_TWOPI, "formatCourse" } }, + { "ROT", { 1000, 10000, -M_PI / 180.0 * 99.0, M_PI / 180.0 * 99.0, "formatRot" } }, // min/max is -/+ 99 degrees for "rate of turn" + { "SOG", { 1000, 1000, 0.0, 65.0, "formatKnots" } }, + { "STW", { 1000, 1000, 0.0, 65.0, "formatKnots" } }, + { "TWA", { 1000, 10000, 0.0, M_TWOPI, "formatWind" } }, + { "TWD", { 1000, 10000, 0.0, M_TWOPI, "formatCourse" } }, + { "TWS", { 1000, 1000, 0.0, 65.0, "formatKnots" } }, + { "WTemp", { 1000, 100, 233.0, 650.0, "kelvinToC" } } // [-50..376] °C }; public: @@ -75,7 +76,8 @@ private: public: WindUtils(BoatValueList* boatValues, GwLog* log) - : logger(log) { + : logger(log) + { twaBVal = boatValues->findValueOrCreate("TWA"); twsBVal = boatValues->findValueOrCreate("TWS"); twdBVal = boatValues->findValueOrCreate("TWD"); diff --git a/lib/obp60task/OBPcharts.cpp b/lib/obp60task/OBPcharts.cpp index 2e4aaea..e15fc83 100644 --- a/lib/obp60task/OBPcharts.cpp +++ b/lib/obp60task/OBPcharts.cpp @@ -1,13 +1,11 @@ // Function lib for display of boat data in various chart formats #include "OBPcharts.h" -// #include "OBP60Extensions.h" #include "OBPDataOperations.h" #include "OBPRingBuffer.h" std::map Chart::dfltChrtDta = { { "formatWind", { 60.0 * DEG_TO_RAD, 10.0 * DEG_TO_RAD } }, // default course range 60 degrees { "formatCourse", { 60.0 * DEG_TO_RAD, 10.0 * DEG_TO_RAD } }, // default course range 60 degrees - //{ "formatKnots", { 7.71, 2.57 } }, // default speed range in m/s { "formatKnots", { 7.71, 2.56 } }, // default speed range in m/s { "formatDepth", { 15.0, 5.0 } }, // default depth range in m { "kelvinToC", { 30.0, 5.0 } } // default temp range in °C/K @@ -16,16 +14,12 @@ std::map Chart::dfltChrtDta = { // --- Class Chart --------------- // Chart - object holding the actual chart, incl. data buffer and format definition -// Parameters: chart timeline direction: 'H' = horizontal, 'V' = vertical; -// chart size: [0] = full size, [1] = half size left/top, [2] half size right/bottom; -// default range of chart, e.g. 30 = [0..30]; +// Parameters: the history data buffer for the chart +// default range of chart, e.g. 30 = [0..30] // common program data; required for logger and color data // flag to indicate if simulation data is active -// Chart::Chart(RingBuffer& dataBuf, char chrtDir, int8_t chrtSz, double dfltRng, CommonData& common, bool useSimuData) Chart::Chart(RingBuffer& dataBuf, double dfltRng, CommonData& common, bool useSimuData) : dataBuf(dataBuf) - //, chrtDir(chrtDir) - //, chrtSz(chrtSz) , dfltRng(dfltRng) , commonData(&common) , useSimuData(useSimuData) @@ -37,52 +31,6 @@ Chart::Chart(RingBuffer& dataBuf, double dfltRng, CommonData& common, dWidth = getdisplay().width(); dHeight = getdisplay().height(); - /* if (chrtDir == 'H') { - // horizontal chart timeline direction - timAxis = dWidth - 1; - switch (chrtSz) { - case 0: - valAxis = dHeight - top - bottom; - cRoot = { 0, top - 1 }; - break; - case 1: - valAxis = (dHeight - top - bottom) / 2 - hGap; - cRoot = { 0, top - 1 }; - break; - case 2: - valAxis = (dHeight - top - bottom) / 2 - hGap; - cRoot = { 0, top + (valAxis + hGap) + hGap - 1 }; - break; - default: - LOG_DEBUG(GwLog::ERROR, "obp60:Chart %s: wrong init parameter", dataBuf.getName()); - return; - } - - } else if (chrtDir == 'V') { - // vertical chart timeline direction - timAxis = dHeight - top - bottom; - switch (chrtSz) { - case 0: - valAxis = dWidth - 1; - cRoot = { 0, top - 1 }; - break; - case 1: - valAxis = dWidth / 2 - vGap; - cRoot = { 0, top - 1 }; - break; - case 2: - valAxis = dWidth / 2 - vGap; - cRoot = { dWidth / 2 + vGap - 1, top - 1 }; - break; - default: - LOG_DEBUG(GwLog::ERROR, "obp60:Chart %s: wrong init parameter", dataBuf.getName()); - return; - } - } else { - LOG_DEBUG(GwLog::ERROR, "obp60:Chart %s: wrong init parameter", dataBuf.getName()); - return; - } */ - dataBuf.getMetaData(dbName, dbFormat); dbMIN_VAL = dataBuf.getMinVal(); dbMAX_VAL = dataBuf.getMaxVal(); @@ -90,22 +38,22 @@ Chart::Chart(RingBuffer& dataBuf, double dfltRng, CommonData& common, // Initialize chart data format; shorter version of standard format indicator if (dbFormat == "formatCourse" || dbFormat == "formatWind" || dbFormat == "formatRot") { - chrtDataFmt = 'W'; // Chart is showing data of course / wind format + chrtDataFmt = WIND; // Chart is showing data of course / wind format } else if (dbFormat == "formatRot") { - chrtDataFmt = 'R'; // Chart is showing data of rotational format + chrtDataFmt = ROTATION; // Chart is showing data of rotational format } else if (dbFormat == "formatKnots") { - chrtDataFmt = 'S'; // Chart is showing data of speed or windspeed format + chrtDataFmt = SPEED; // Chart is showing data of speed or windspeed format } else if (dbFormat == "formatDepth") { - chrtDataFmt = 'D'; // Chart ist showing data of format + chrtDataFmt = DEPTH; // Chart ist showing data of format } else if (dbFormat == "kelvinToC") { - chrtDataFmt = 'T'; // Chart ist showing data of format + chrtDataFmt = TEMPERATURE; // Chart ist showing data of format } else { - chrtDataFmt = 'O'; // Chart is showing any other data format + chrtDataFmt = OTHER; // Chart is showing any other data format } // "0" value is the same for any data format but for user defined temperature format zeroValue = 0.0; - if (chrtDataFmt == 'T') { + if (chrtDataFmt == TEMPERATURE) { tempFormat = commonData->config->getString(commonData->config->tempFormat); // [K|°C|°F] if (tempFormat == "K") { zeroValue = 0.0; @@ -130,9 +78,9 @@ Chart::Chart(RingBuffer& dataBuf, double dfltRng, CommonData& common, chrtMax = chrtMin + dfltRng; chrtMid = (chrtMin + chrtMax) / 2; chrtRng = dfltRng; - recalcRngCntr = true; // initialize and chart borders on first screen call + recalcRngMid = true; // initialize and chart borders on first screen call - LOG_DEBUG(GwLog::DEBUG, "Chart Init: dWidth: %d, dHeight: %d, timAxis: %d, valAxis: %d, cRoot {x,y}: %d, %d, dbname: %s, rngStep: %.4f, chrtDataFmt: %c", + LOG_DEBUG(GwLog::DEBUG, "Chart Init: dWidth: %d, dHeight: %d, timAxis: %d, valAxis: %d, cRoot {x,y}: %d, %d, dbname: %s, rngStep: %.4f, chrtDataFmt: %d", dWidth, dHeight, timAxis, valAxis, cRoot.x, cRoot.y, dbName, rngStep, chrtDataFmt); }; @@ -144,28 +92,25 @@ Chart::~Chart() // Parameters: : chart timeline direction: 'H' = horizontal, 'V' = vertical // : chart size: [0] = full size, [1] = half size left/top, [2] half size right/bottom // : chart timeline interval -// : current boat data shall be shown [true/false] -// : current boat data value to be printed -// void Chart::showChrt(GwApi::BoatValue currValue, int8_t& chrtIntv, const bool showCurrValue) -void Chart::showChrt(char chrtDir, int8_t chrtSz, int8_t& chrtIntv, bool showCurrValue, GwApi::BoatValue currValue) +// ; print data name on horizontal half chart [true|false] +// : print current boat data value [true|false] +// : current boat data value; used only for test on valid data +void Chart::showChrt(char chrtDir, int8_t chrtSz, const int8_t chrtIntv, bool prntName, bool showCurrValue, GwApi::BoatValue currValue) { - // this->chrtDir = chrtDir; - // this->chrtSz = chrtSz; - if (!setChartDimensions(chrtDir, chrtSz)) { return; // wrong chart dimension parameters } drawChrt(chrtDir, chrtIntv, currValue); drawChrtTimeAxis(chrtDir, chrtSz, chrtIntv); - drawChrtValAxis(chrtDir, chrtSz); + drawChrtValAxis(chrtDir, chrtSz, prntName); if (!bufDataValid) { // No valid data available prntNoValidData(chrtDir); return; } - if (showCurrValue) { // show latest value from history buffer; usually this should be the most current one + if (showCurrValue) { // show latest value from history buffer; this should be the most current one currValue.value = dataBuf.getLast(); currValue.valid = currValue.value != dbMAX_VAL; prntCurrValue(chrtDir, currValue); @@ -175,12 +120,12 @@ void Chart::showChrt(char chrtDir, int8_t chrtSz, int8_t& chrtIntv, bool showCur // define dimensions and start points for chart bool Chart::setChartDimensions(const char direction, const int8_t size) { - if ((direction != 'H' && direction != 'V') || (size < 0 || size > 2)) { + if ((direction != HORIZONTAL && direction != VERTICAL) || (size < 0 || size > 2)) { LOG_DEBUG(GwLog::ERROR, "obp60:setChartDimensions %s: wrong parameters", dataBuf.getName()); return false; } - if (direction == 'H') { + if (direction == HORIZONTAL) { // horizontal chart timeline direction timAxis = dWidth - 1; switch (size) { @@ -198,7 +143,7 @@ bool Chart::setChartDimensions(const char direction, const int8_t size) break; } - } else if (direction == 'V') { + } else if (direction == VERTICAL) { // vertical chart timeline direction timAxis = dHeight - top - bottom; switch (size) { @@ -222,139 +167,41 @@ bool Chart::setChartDimensions(const char direction, const int8_t size) } // draw chart -void Chart::drawChrt(const char chrtDir, int8_t& chrtIntv, GwApi::BoatValue& currValue) +void Chart::drawChrt(const char chrtDir, const int8_t chrtIntv, GwApi::BoatValue& currValue) { - double chrtVal; // Current data value - double chrtScl; // Scale for data values in pixels per value + double chrtScale; // Scale for data values in pixels per value - int x, y; // x and y coordinates for drawing - - getBufStartNSize(chrtIntv); + getBufferStartNSize(chrtIntv); // LOG_DEBUG(GwLog::DEBUG, "Chart:drawChart: min: %.1f, mid: %.1f, max: %.1f, rng: %.1f", chrtMin, chrtMid, chrtMax, chrtRng); calcChrtBorders(chrtMin, chrtMid, chrtMax, chrtRng); - LOG_DEBUG(GwLog::DEBUG, "Chart:drawChart2: min: %.1f, mid: %.1f, max: %.1f, rng: %.1f", chrtMin, chrtMid, chrtMax, chrtRng); - - // Chart scale: pixels per value step - chrtScl = double(valAxis) / chrtRng; + chrtScale = double(valAxis) / chrtRng; // Chart scale: pixels per value step + LOG_DEBUG(GwLog::DEBUG, "Chart:drawChart: min: %.1f, mid: %.1f, max: %.1f, rng: %.1f", chrtMin, chrtMid, chrtMax, chrtRng); // Do we have valid buffer data? if (dataBuf.getMax() == dbMAX_VAL) { // only values in buffer -> no valid wind data available bufDataValid = false; - } else if (!currValue.valid && !useSimuData) { // currently no valid boat data available and no simulation mode + return; + + } else if (currValue.valid || useSimuData) { // latest boat data valid or simulation mode + numNoData = 0; // reset data error counter + bufDataValid = true; + + } else { // currently no valid data numNoData++; bufDataValid = true; - if (numNoData > THRESHOLD_NO_DATA) { // If more than 4 invalid values in a row, send message + + if (numNoData > THRESHOLD_NO_DATA) { // If more than 4 invalid values in a row, flag for invalid data bufDataValid = false; - } - } else { - numNoData = 0; // reset data error counter - bufDataValid = true; // At least some wind data available - } - - // Draw wind values in chart - //*********************************************************************** - if (bufDataValid) { - for (int i = 0; i < (numBufVals / chrtIntv); i++) { - chrtVal = dataBuf.get(bufStart + (i * chrtIntv)); // show the latest wind values in buffer; keep 1st value constant in a rolling buffer - if (chrtVal == dbMAX_VAL) { - chrtPrevVal = dbMAX_VAL; - } else { - - if (chrtDir == 'H') { // horizontal chart - x = cRoot.x + i; // Position in chart area - - if (chrtDataFmt == 'S' or chrtDataFmt == 'T') { // speed or temperature data format -> print low values at bottom - y = cRoot.y + valAxis - static_cast(((chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round - } else if (chrtDataFmt == 'W' || chrtDataFmt == 'R') { // degree type value - y = cRoot.y + static_cast((WindUtils::to2PI(chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round - } else { // any other data format - y = cRoot.y + static_cast(((chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round - } - - } else { // vertical chart - y = cRoot.y + timAxis - i; // Position in chart area - - // if (chrtDataFmt == 'S' || chrtDataFmt == 'D' || chrtDataFmt == 'T') { - if (chrtDataFmt == 'W' || chrtDataFmt == 'R') { // degree type value - x = cRoot.x + static_cast((WindUtils::to2PI(chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round - } else { - x = cRoot.x + static_cast(((chrtVal - chrtMin) * chrtScl) + 0.5); // calculate chart point and round - } - } - - // if (i >= (numBufVals / chrtIntv) - 5) // log chart data of 1 line (adjust for test purposes) - // LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Chart: i: %d, chrtVal: %.2f, chrtMin: %.2f, {x,y} {%d,%d}", i, chrtVal, chrtMin, x, y); - - if ((i == 0) || (chrtPrevVal == dbMAX_VAL)) { - // just a dot for 1st chart point or after some invalid values - prevX = x; - prevY = y; - - } else if (chrtDataFmt == 'W' || chrtDataFmt == 'R') { - // cross borders check for degree values; shift values to [-PI..0..PI]; when crossing borders, range is 2x PI degrees - double normCurr = WindUtils::to2PI(chrtVal - chrtMin); - double normPrev = WindUtils::to2PI(chrtPrevVal - chrtMin); - // Check if pixel positions are far apart (crossing chart boundary); happens when one value is near chrtMax and the other near chrtMin - bool crossedBorders = std::abs(normCurr - normPrev) > (chrtRng / 2.0); - - if (crossedBorders) { // If current value crosses chart borders compared to previous value, split line - // LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Chart: crossedBorders: %d, chrtVal: %.2f, chrtPrevVal: %.2f", crossedBorders, chrtVal, chrtPrevVal); - bool wrappingFromHighToLow = normCurr < normPrev; // Determine which edge we're crossing - if (chrtDir == 'H') { - int ySplit = wrappingFromHighToLow ? (cRoot.y + valAxis) : cRoot.y; - drawBoldLine(prevX, prevY, x, ySplit); - prevY = wrappingFromHighToLow ? cRoot.y : (cRoot.y + valAxis); - } else { // vertical chart - int xSplit = wrappingFromHighToLow ? (cRoot.x + valAxis) : cRoot.x; - drawBoldLine(prevX, prevY, xSplit, y); - prevX = wrappingFromHighToLow ? cRoot.x : (cRoot.x + valAxis); - } - } - } - - if (chrtDataFmt == 'D') { - if (chrtDir == 'H') { // horizontal chart - drawBoldLine(x, y, x, cRoot.y + valAxis); - } else { // vertical chart - drawBoldLine(x, y, cRoot.x + valAxis, y); - } - } else { - drawBoldLine(prevX, prevY, x, y); - } - - /* if (chrtDir == 'H' || x == prevX) { // horizontal chart & vertical line - if (chrtDataFmt == 'D') { - drawBoldLine(x, y, x, cRoot.y + valAxis); - } - drawBoldLine(prevX, prevY, x, y); - } else if (chrtDir == 'V' || x != prevX) { // vertical chart & line with some horizontal trend -> normal state - if (chrtDataFmt == 'D') { - drawBoldLine(x, y, cRoot.x + valAxis, y); - } - drawBoldLine(prevX, prevY, x, y); - } */ - chrtPrevVal = chrtVal; - prevX = x; - prevY = y; - } - - // Reaching chart area top end - if (i >= timAxis - 1) { - oldChrtIntv = 0; // force reset of buffer start and number of values to show in next display loop - - if (chrtDataFmt == 'W') { // degree of course or wind - recalcRngCntr = true; - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: chart end: timAxis: %d, i: %d, bufStart: %d, numBufVals: %d, recalcRngCntr: %d", timAxis, i, bufStart, numBufVals, recalcRngCntr); - } - break; - } + return; } } + + drawChartLines(chrtDir, chrtIntv, chrtScale); } // Identify buffer size and buffer start position for chart -void Chart::getBufStartNSize(int8_t& chrtIntv) +void Chart::getBufferStartNSize(const int8_t chrtIntv) { count = dataBuf.getCurrentSize(); currIdx = dataBuf.getLastIdx(); @@ -379,19 +226,24 @@ void Chart::getBufStartNSize(int8_t& chrtIntv) // check and adjust chart range and set range borders and range middle void Chart::calcChrtBorders(double& rngMin, double& rngMid, double& rngMax, double& rng) { - if (chrtDataFmt == 'W' || chrtDataFmt == 'R') { - // Chart data is of type 'course', 'wind' or 'rot' + if (chrtDataFmt == WIND || chrtDataFmt == ROTATION) { - if (chrtDataFmt == 'W') { - // Chart data is of type 'course' or 'wind' + if (chrtDataFmt == ROTATION) { + // if chart data is of type 'rotation', we want to have always to be '0' + rngMid = 0; + } else { // WIND: Chart data is of type 'course' or 'wind' + + // initialize if data buffer has just been started filling if ((count == 1 && rngMid == 0) || rngMid == dbMAX_VAL) { - recalcRngCntr = true; // initialize + recalcRngMid = true; } - // Set rngMid - if (recalcRngCntr) { + if (recalcRngMid) { + // Set rngMid + rngMid = dataBuf.getMid(numBufVals); + if (rngMid == dbMAX_VAL) { rngMid = 0; } else { @@ -401,31 +253,32 @@ void Chart::calcChrtBorders(double& rngMin, double& rngMid, double& rngMax, doub rngMin = dataBuf.getMin(numBufVals); rngMax = dataBuf.getMax(numBufVals); rng = (rngMax >= rngMin ? rngMax - rngMin : M_TWOPI - rngMin + rngMax); - rng = max(rng, dfltRng); // keep at least default chart range + rng = std::max(rng, dfltRng); // keep at least default chart range + if (rng > M_PI) { // If wind range > 180°, adjust wndCenter to smaller wind range end rngMid = WindUtils::to2PI(rngMid + M_PI); } } - recalcRngCntr = false; // Reset flag for determination - LOG_DEBUG(GwLog::DEBUG, "calcChrtRange: rngMid: %.1f°, rngMin: %.1f°, rngMax: %.1f°, rng: %.1f°, rngStep: %.1f°", rngMid * RAD_TO_DEG, rngMin * RAD_TO_DEG, rngMax * RAD_TO_DEG, + recalcRngMid = false; // Reset flag for determination + + LOG_DEBUG(GwLog::DEBUG, "calcChrtRange: rngMin: %.1f°, rngMid: %.1f°, rngMax: %.1f°, rng: %.1f°, rngStep: %.1f°", rngMin * RAD_TO_DEG, rngMid * RAD_TO_DEG, rngMax * RAD_TO_DEG, rng * RAD_TO_DEG, rngStep * RAD_TO_DEG); } - - } else if (chrtDataFmt == 'R') { - // Chart data is of type 'rotation'; then we want to have always to be '0' - rngMid = 0; } - // check and adjust range between left, center, and right chart limit + // check and adjust range between left, mid, and right chart limit double halfRng = rng / 2.0; // we calculate with range between and edges - double diffRng = getAngleRng(rngMid, numBufVals); - diffRng = (diffRng == dbMAX_VAL ? 0 : std::ceil(diffRng / rngStep) * rngStep); - // LOG_DEBUG(GwLog::DEBUG, "calcChrtBorders: diffRng: %.1f°, halfRng: %.1f°", diffRng * RAD_TO_DEG, halfRng * RAD_TO_DEG); + double tmpRng = getAngleRng(rngMid, numBufVals); + tmpRng = (tmpRng == dbMAX_VAL ? 0 : std::ceil(tmpRng / rngStep) * rngStep); - if (diffRng > halfRng) { - halfRng = diffRng; // round to next value - } else if (diffRng + rngStep < halfRng) { // Reduce chart range for higher resolution if possible - halfRng = max(dfltRng / 2.0, diffRng); + // LOG_DEBUG(GwLog::DEBUG, "calcChrtBorders: tmpRng: %.1f°, halfRng: %.1f°", tmpRng * RAD_TO_DEG, halfRng * RAD_TO_DEG); + + if (tmpRng > halfRng) { // expand chart range to new value + halfRng = tmpRng; + } + + else if (tmpRng + rngStep < halfRng) { // Contract chart range for higher resolution if possible + halfRng = std::max(dfltRng / 2.0, tmpRng); } rngMin = WindUtils::to2PI(rngMid - halfRng); @@ -433,14 +286,12 @@ void Chart::calcChrtBorders(double& rngMin, double& rngMid, double& rngMax, doub rngMax = WindUtils::to2PI(rngMax); rng = halfRng * 2.0; - LOG_DEBUG(GwLog::DEBUG, "calcChrtBorders: rngMin: %.1f°, rngMid: %.1f°, rngMax: %.1f°, diffRng: %.1f°, rng: %.1f°, rngStep: %.1f°", rngMin * RAD_TO_DEG, rngMid * RAD_TO_DEG, rngMax * RAD_TO_DEG, - diffRng * RAD_TO_DEG, rng * RAD_TO_DEG, rngStep * RAD_TO_DEG); + + LOG_DEBUG(GwLog::DEBUG, "calcChrtBorders: rngMin: %.1f°, rngMid: %.1f°, rngMax: %.1f°, tmpRng: %.1f°, rng: %.1f°, rngStep: %.1f°", rngMin * RAD_TO_DEG, rngMid * RAD_TO_DEG, rngMax * RAD_TO_DEG, + tmpRng * RAD_TO_DEG, rng * RAD_TO_DEG, rngStep * RAD_TO_DEG); } else { // chart data is of any other type - double oldRngMin = rngMin; - double oldRngMax = rngMax; - double currMinVal = dataBuf.getMin(numBufVals); double currMaxVal = dataBuf.getMax(numBufVals); @@ -449,10 +300,10 @@ void Chart::calcChrtBorders(double& rngMin, double& rngMid, double& rngMax, doub } // check if current chart border have to be adjusted - if (currMinVal < oldRngMin || (currMinVal > (oldRngMin + rngStep))) { // decrease rngMin if required or increase if lowest value is higher than old rngMin + if (currMinVal < rngMin || (currMinVal > (rngMin + rngStep))) { // decrease rngMin if required or increase if lowest value is higher than old rngMin rngMin = std::floor(currMinVal / rngStep) * rngStep; // align low range to lowest buffer value and nearest range interval } - if ((currMaxVal > oldRngMax) || (currMaxVal < (oldRngMax - rngStep))) { // increase rngMax if required or decrease if lowest value is lower than old rngMax + if ((currMaxVal > rngMax) || (currMaxVal < (rngMax - rngStep))) { // increase rngMax if required or decrease if lowest value is lower than old rngMax rngMax = std::ceil(currMaxVal / rngStep) * rngStep; } @@ -474,8 +325,112 @@ void Chart::calcChrtBorders(double& rngMin, double& rngMid, double& rngMax, doub } } +// Draw chart graph +void Chart::drawChartLines(const char direction, const int8_t chrtIntv, const double chrtScale) +{ + double chrtVal; // Current data value + Pos point, prevPoint; // current and previous chart point + + for (int i = 0; i < (numBufVals / chrtIntv); i++) { + + chrtVal = dataBuf.get(bufStart + (i * chrtIntv)); // show the latest wind values in buffer; keep 1st value constant in a rolling buffer + + if (chrtVal == dbMAX_VAL) { + chrtPrevVal = dbMAX_VAL; + } else { + + point = setCurrentChartPoint(i, direction, chrtVal, chrtScale); + + // if (i >= (numBufVals / chrtIntv) - 5) // log chart data of 1 line (adjust for test purposes) + // LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Chart: i: %d, chrtVal: %.2f, chrtMin: %.2f, {x,y} {%d,%d}", i, chrtVal, chrtMin, x, y); + + if ((i == 0) || (chrtPrevVal == dbMAX_VAL)) { + // just a dot for 1st chart point or after some invalid values + prevPoint = point; + + } else if (chrtDataFmt == WIND || chrtDataFmt == ROTATION) { + // cross borders check for degree values; shift values to [-PI..0..PI]; when crossing borders, range is 2x PI degrees + + double normCurrVal = WindUtils::to2PI(chrtVal - chrtMin); + double normPrevVal = WindUtils::to2PI(chrtPrevVal - chrtMin); + // Check if pixel positions are far apart (crossing chart boundary); happens when one value is near chrtMax and the other near chrtMin + bool crossedBorders = std::abs(normCurrVal - normPrevVal) > (chrtRng / 2.0); + + if (crossedBorders) { // If current value crosses chart borders compared to previous value, split line + // LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Chart: crossedBorders: %d, chrtVal: %.2f, chrtPrevVal: %.2f", crossedBorders, chrtVal, chrtPrevVal); + bool wrappingFromHighToLow = normCurrVal < normPrevVal; // Determine which edge we're crossing + + if (direction == HORIZONTAL) { + int ySplit = wrappingFromHighToLow ? (cRoot.y + valAxis) : cRoot.y; + drawBoldLine(prevPoint.x, prevPoint.y, point.x, ySplit); + prevPoint.y = wrappingFromHighToLow ? cRoot.y : (cRoot.y + valAxis); + + } else { // vertical chart + int xSplit = wrappingFromHighToLow ? (cRoot.x + valAxis) : cRoot.x; + drawBoldLine(prevPoint.x, prevPoint.y, xSplit, point.y); + prevPoint.x = wrappingFromHighToLow ? cRoot.x : (cRoot.x + valAxis); + } + } + } + + if (chrtDataFmt == DEPTH) { + if (direction == HORIZONTAL) { // horizontal chart + drawBoldLine(point.x, point.y, point.x, cRoot.y + valAxis); + } else { // vertical chart + drawBoldLine(point.x, point.y, cRoot.x + valAxis, point.y); + } + } else { + drawBoldLine(prevPoint.x, prevPoint.y, point.x, point.y); + } + + chrtPrevVal = chrtVal; + prevPoint = point; + } + + // Reaching chart area top end + if (i >= timAxis - 1) { + oldChrtIntv = 0; // force reset of buffer start and number of values to show in next display loop + + if (chrtDataFmt == WIND) { // degree of course or wind + recalcRngMid = true; + LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: chart end: timAxis: %d, i: %d, bufStart: %d, numBufVals: %d, recalcRngCntr: %d", timAxis, i, bufStart, numBufVals, recalcRngMid); + } + break; + } + } +} + +// Set current chart point to draw +Pos Chart::setCurrentChartPoint(const int i, const char direction, const double chrtVal, const double chrtScale) +{ + Pos currentPoint; + + if (direction == HORIZONTAL) { + currentPoint.x = cRoot.x + i; // Position in chart area + + if (chrtDataFmt == WIND || chrtDataFmt == ROTATION) { // degree type value + currentPoint.y = cRoot.y + static_cast((WindUtils::to2PI(chrtVal - chrtMin) * chrtScale) + 0.5); // calculate chart point and round + } else if (chrtDataFmt == SPEED or chrtDataFmt == TEMPERATURE) { // speed or temperature data format -> print low values at bottom + currentPoint.y = cRoot.y + valAxis - static_cast(((chrtVal - chrtMin) * chrtScale) + 0.5); // calculate chart point and round + } else { // any other data format + currentPoint.y = cRoot.y + static_cast(((chrtVal - chrtMin) * chrtScale) + 0.5); // calculate chart point and round + } + + } else { // vertical chart + currentPoint.y = cRoot.y + timAxis - i; // Position in chart area + + if (chrtDataFmt == WIND || chrtDataFmt == ROTATION) { // degree type value + currentPoint.x = cRoot.x + static_cast((WindUtils::to2PI(chrtVal - chrtMin) * chrtScale) + 0.5); // calculate chart point and round + } else { + currentPoint.x = cRoot.x + static_cast(((chrtVal - chrtMin) * chrtScale) + 0.5); // calculate chart point and round + } + } + + return currentPoint; +} + // chart time axis label + lines -void Chart::drawChrtTimeAxis(const char chrtDir, const int8_t chrtSz, int8_t& chrtIntv) +void Chart::drawChrtTimeAxis(const char chrtDir, const int8_t chrtSz, const int8_t chrtIntv) { float axSlots, intv, i; char sTime[6]; @@ -488,7 +443,7 @@ void Chart::drawChrtTimeAxis(const char chrtDir, const int8_t chrtSz, int8_t& ch intv = timAxis / (axSlots - 1); // minutes per chart axis interval (interval is 1 less than axSlots) i = timeRng; // Chart axis label start at -32, -16, -12, ... minutes - if (chrtDir == 'H') { // horizontal chart + if (chrtDir == HORIZONTAL) { getdisplay().fillRect(0, cRoot.y, dWidth, 2, fgColor); for (float j = 0; j < timAxis - 1; j += intv) { // fill time axis with values but keep area free on right hand side for value label @@ -510,11 +465,11 @@ void Chart::drawChrtTimeAxis(const char chrtDir, const int8_t chrtSz, int8_t& ch snprintf(sTime, sizeof(sTime), "-%.0f", i); getdisplay().drawLine(cRoot.x, cRoot.y + j, cRoot.x + valAxis, cRoot.y + j, fgColor); // Grid line - if (chrtSz == 0) { // full size chart + if (chrtSz == FULL_SIZE) { // full size chart getdisplay().fillRect(0, cRoot.y + j - 9, 32, 15, bgColor); // clear small area to remove potential chart lines getdisplay().setCursor((4 - strlen(sTime)) * 7, cRoot.y + j + 3); // time value; print left screen; value right-formated getdisplay().printf("%s", sTime); // Range value - } else if (chrtSz == 2) { // half size chart; right side + } else if (chrtSz == HALF_SIZE_RIGHT) { // half size chart; right side drawTextCenter(dWidth / 2, cRoot.y + j, sTime); // time value; print mid screen } } @@ -522,92 +477,72 @@ void Chart::drawChrtTimeAxis(const char chrtDir, const int8_t chrtSz, int8_t& ch } // chart value axis labels + lines -void Chart::drawChrtValAxis(const char chrtDir, const int8_t chrtSz) +void Chart::drawChrtValAxis(const char chrtDir, const int8_t chrtSz, bool prntName) { - double axLabel; - double cVal; - // char sVal[6]; + const GFXfont* font; + constexpr bool NO_LABEL = false; + constexpr bool LABEL = true; getdisplay().setTextColor(fgColor); - if (chrtDir == 'H') { + if (chrtDir == HORIZONTAL) { - // print buffer data name on right hand side of time axis (max. size 5 characters) - getdisplay().setFont(&Ubuntu_Bold12pt8b); - drawTextRalign(cRoot.x + timAxis, cRoot.y - 3, dbName.substring(0, 5)); + if (chrtSz == FULL_SIZE) { - if (chrtSz == 0) { // full size chart + font = &Ubuntu_Bold12pt8b; - if (chrtDataFmt == 'W') { - prntHorizThreeValueAxisLabel(&Ubuntu_Bold12pt8b); + // print buffer data name on right hand side of time axis (max. size 5 characters) + getdisplay().setFont(font); + drawTextRalign(cRoot.x + timAxis, cRoot.y - 3, dbName.substring(0, 5)); + + if (chrtDataFmt == WIND) { + prntHorizChartThreeValueAxisLabel(font); return; } // for any other data formats print multiple axis value lines on full charts - prntHorizMultiValueAxisLabel(&Ubuntu_Bold12pt8b); + prntHorizChartMultiValueAxisLabel(font); return; } else { // half size chart -> just print edge values + middle chart line - LOG_DEBUG(GwLog::DEBUG, "Chart::drawChrtValAxis: chrtDataFmt: %c, chrtMin: %.2f, chrtMid: %.2f, chrtMax: %.2f", chrtDataFmt, chrtMin, chrtMid, chrtMax); - prntHorizThreeValueAxisLabel(&Ubuntu_Bold10pt8b); + font = &Ubuntu_Bold10pt8b; + + if (prntName) { + // print buffer data name on right hand side of time axis (max. size 5 characters) + getdisplay().setFont(font); + drawTextRalign(cRoot.x + timAxis, cRoot.y - 3, dbName.substring(0, 5)); + } + + prntHorizChartThreeValueAxisLabel(font); return; } } else { // vertical chart - char sVal[6]; - if (chrtSz == 0) { // full size chart - getdisplay().setFont(&Ubuntu_Bold12pt8b); // use larger font + if (chrtSz == FULL_SIZE) { + font = &Ubuntu_Bold12pt8b; + getdisplay().setFont(font); // use larger font drawTextRalign(cRoot.x + (valAxis * 0.42), cRoot.y - 2, dbName.substring(0, 6)); // print buffer data name (max. size 5 characters) + } else { - getdisplay().setFont(&Ubuntu_Bold10pt8b); // use smaller font - } - getdisplay().fillRect(cRoot.x, cRoot.y, valAxis, 2, fgColor); // top chart line - cVal = chrtMin; - cVal = convertValue(cVal, dbName, dbFormat, *commonData); // value (converted) - if (useSimuData) { // dirty fix for problem that OBP60Formatter can only be used without data simulation -> returns random values in simulation mode - cVal = chrtMin; // no value conversion + font = &Ubuntu_Bold10pt8b; } - snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); - getdisplay().setCursor(cRoot.x, cRoot.y - 2); - getdisplay().printf("%s", sVal); // Range low end - cVal = chrtMid; - cVal = convertValue(cVal, dbName, dbFormat, *commonData); // value (converted) - if (useSimuData) { // dirty fix for problem that OBP60Formatter can only be used without data simulation -> returns random values in simulation mode - cVal = chrtMid; // no value conversion - } - snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); - drawTextCenter(cRoot.x + (valAxis / 2), cRoot.y - 9, sVal); // Range mid end - - cVal = chrtMax; - cVal = convertValue(cVal, dbName, dbFormat, *commonData); // value (converted) - if (useSimuData) { // dirty fix for problem that OBP60Formatter can only be used without data simulation -> returns random values in simulation mode - cVal = chrtMax; // no value conversion - } - snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); - drawTextRalign(cRoot.x + valAxis - 2, cRoot.y - 2, sVal); // Range high end - - // draw vertical grid lines for each axis label - for (int j = 0; j <= valAxis; j += (valAxis / 2)) { - getdisplay().drawLine(cRoot.x + j, cRoot.y, cRoot.x + j, cRoot.y + timAxis, fgColor); - } + prntVerticChartThreeValueAxisLabel(font); } } // Print current data value -void Chart::prntCurrValue(const char chrtDir, GwApi::BoatValue& currValue) +void Chart::prntCurrValue(const char direction, GwApi::BoatValue& currValue) { - const int xPosVal = (chrtDir == 'H') ? cRoot.x + (timAxis / 2) - 56 : cRoot.x + 32; - const int yPosVal = (chrtDir == 'H') ? cRoot.y + valAxis - 7 : cRoot.y + timAxis - 7; + const int xPosVal = (direction == HORIZONTAL) ? cRoot.x + (timAxis / 2) - 56 : cRoot.x + 32; + const int yPosVal = (direction == HORIZONTAL) ? cRoot.y + valAxis - 7 : cRoot.y + timAxis - 7; - FormattedData frmtDbData = formatValue(&currValue, *commonData); + FormattedData frmtDbData = formatValue(&currValue, *commonData, NO_SIMUDATA); String sdbValue = frmtDbData.svalue; // value as formatted string String dbUnit = frmtDbData.unit; // Unit of value; limit length to 3 characters - // LOG_DEBUG(GwLog::DEBUG, "Chart CurrValue: dbValue: %.2f, sdbValue: %s, dbFormat: %s, dbUnit: %s, Valid: %d, Name: %s, Address: %p", currValue.value, sdbValue, - // currValue.getFormat(), dbUnit, currValue.valid, currValue.getName(), currValue); getdisplay().fillRect(xPosVal - 1, yPosVal - 35, 128, 41, bgColor); // Clear area for TWS value getdisplay().drawRect(xPosVal, yPosVal - 34, 126, 40, fgColor); // Draw box for TWS value @@ -625,28 +560,28 @@ void Chart::prntCurrValue(const char chrtDir, GwApi::BoatValue& currValue) } // print message for no valid data availabletemplate -void Chart::prntNoValidData(const char chrtDir) +void Chart::prntNoValidData(const char direction) { - int pX, pY; + Pos p; getdisplay().setFont(&Ubuntu_Bold10pt8b); - if (chrtDir == 'H') { - pX = cRoot.x + (timAxis / 2); - pY = cRoot.y + (valAxis / 2) - 10; + if (direction == HORIZONTAL) { + p.x = cRoot.x + (timAxis / 2); + p.y = cRoot.y + (valAxis / 2) - 10; } else { - pX = cRoot.x + (valAxis / 2); - pY = cRoot.y + (timAxis / 2) - 10; + p.x = cRoot.x + (valAxis / 2); + p.y = cRoot.y + (timAxis / 2) - 10; } - getdisplay().fillRect(pX - 37, pY - 10, 78, 24, bgColor); // Clear area for message - drawTextCenter(pX, pY, "No data"); + getdisplay().fillRect(p.x - 37, p.y - 10, 78, 24, bgColor); // Clear area for message + drawTextCenter(p.x, p.y, "No data"); LOG_DEBUG(GwLog::LOG, "Page chart <%s>: No valid data available", dbName); } // Get maximum difference of last of dataBuf ringbuffer values to center chart; for angle data only -double Chart::getAngleRng(double center, size_t amount) +double Chart::getAngleRng(const double center, size_t amount) { size_t count = dataBuf.getCurrentSize(); @@ -680,8 +615,39 @@ double Chart::getAngleRng(double center, size_t amount) return (maxRng != dbMIN_VAL ? maxRng : dbMAX_VAL); // Return range from to } -// print horizontal axis label with only three values: top, mid, and bottom -void Chart::prntHorizThreeValueAxisLabel(const GFXfont* font) + // print value axis label with only three values: top, mid, and bottom for vertical chart + void Chart::prntVerticChartThreeValueAxisLabel(const GFXfont* font) +{ + double cVal; + char sVal[7]; + + getdisplay().fillRect(cRoot.x, cRoot.y, valAxis, 2, fgColor); // top chart line + getdisplay().setFont(font); + + cVal = chrtMin; + cVal = convertValue(cVal, dbName, dbFormat, *commonData); // value (converted) + snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); + getdisplay().setCursor(cRoot.x, cRoot.y - 2); + getdisplay().printf("%s", sVal); // Range low end + + cVal = chrtMid; + cVal = convertValue(cVal, dbName, dbFormat, *commonData); // value (converted) + snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); + drawTextCenter(cRoot.x + (valAxis / 2), cRoot.y - 9, sVal); // Range mid end + + cVal = chrtMax; + cVal = convertValue(cVal, dbName, dbFormat, *commonData); // value (converted) + snprintf(sVal, sizeof(sVal), "%.0f", round(cVal)); + drawTextRalign(cRoot.x + valAxis - 2, cRoot.y - 2, sVal); // Range high end + + // draw vertical grid lines for each axis label + for (int j = 0; j <= valAxis; j += (valAxis / 2)) { + getdisplay().drawLine(cRoot.x + j, cRoot.y, cRoot.x + j, cRoot.y + timAxis, fgColor); + } +} + +// print value axis label with only three values: top, mid, and bottom for horizontal chart +void Chart::prntHorizChartThreeValueAxisLabel(const GFXfont* font) { double axLabel; double chrtMin, chrtMid, chrtMax; @@ -706,28 +672,28 @@ void Chart::prntHorizThreeValueAxisLabel(const GFXfont* font) chrtMax = std::round(chrtMax * 100.0) / 100.0; // print top axis label - axLabel = (chrtDataFmt == 'S' || chrtDataFmt == 'T') ? chrtMax : chrtMin; + axLabel = (chrtDataFmt == SPEED || chrtDataFmt == TEMPERATURE) ? chrtMax : chrtMin; sVal = formatLabel(axLabel); - getdisplay().fillRect(cRoot.x, cRoot.y + 2, xOffset + 4, yOffset, bgColor); // Clear small area to remove potential chart lines + getdisplay().fillRect(cRoot.x, cRoot.y + 2, xOffset + 3, yOffset, bgColor); // Clear small area to remove potential chart lines drawTextRalign(cRoot.x + xOffset, cRoot.y + yOffset, sVal); // range value // print mid axis label axLabel = chrtMid; sVal = formatLabel(axLabel); - getdisplay().fillRect(cRoot.x, cRoot.y + (valAxis / 2) - 8, xOffset + 4, 16, bgColor); // Clear small area to remove potential chart lines + getdisplay().fillRect(cRoot.x, cRoot.y + (valAxis / 2) - 8, xOffset + 3, 16, bgColor); // Clear small area to remove potential chart lines drawTextRalign(cRoot.x + xOffset, cRoot.y + (valAxis / 2) + 6, sVal); // range value - getdisplay().drawLine(cRoot.x + xOffset + 4, cRoot.y + (valAxis / 2), cRoot.x + timAxis, cRoot.y + (valAxis / 2), fgColor); + getdisplay().drawLine(cRoot.x + xOffset + 3, cRoot.y + (valAxis / 2), cRoot.x + timAxis, cRoot.y + (valAxis / 2), fgColor); // print bottom axis label - axLabel = (chrtDataFmt == 'S' || chrtDataFmt == 'T') ? chrtMin : chrtMax; + axLabel = (chrtDataFmt == SPEED || chrtDataFmt == TEMPERATURE) ? chrtMin : chrtMax; sVal = formatLabel(axLabel); - getdisplay().fillRect(cRoot.x, cRoot.y + valAxis - 16, xOffset + 3, 16, bgColor); // Clear small area to remove potential chart lines + getdisplay().fillRect(cRoot.x, cRoot.y + valAxis - 14, xOffset + 3, 15, bgColor); // Clear small area to remove potential chart lines drawTextRalign(cRoot.x + xOffset, cRoot.y + valAxis, sVal); // range value - getdisplay().drawLine(cRoot.x + xOffset + 2, cRoot.y + valAxis, cRoot.x + timAxis, cRoot.y + valAxis, fgColor); + getdisplay().drawLine(cRoot.x + xOffset + 3, cRoot.y + valAxis, cRoot.x + timAxis, cRoot.y + valAxis, fgColor); } -// print horizontal axis label with multiple axis lines -void Chart::prntHorizMultiValueAxisLabel(const GFXfont* font) +// print value axis label with multiple axis lines for horizontal chart +void Chart::prntHorizChartMultiValueAxisLabel(const GFXfont* font) { double chrtMin, chrtMax, chrtRng; double axSlots, axIntv, axLabel; @@ -755,7 +721,7 @@ void Chart::prntHorizMultiValueAxisLabel(const GFXfont* font) LOG_DEBUG(GwLog::DEBUG, "Chart::printHorizMultiValueAxisLabel: chrtRng: %.2f, th-chrtRng: %.2f, axSlots: %.2f, axIntv: %.2f, axLabel: %.2f, chrtMin: %.2f, chrtMid: %.2f, chrtMax: %.2f", chrtRng, this->chrtRng, axSlots, axIntv, axLabel, this->chrtMin, chrtMid, chrtMax); int loopStrt, loopEnd, loopStp; - if (chrtDataFmt == 'S' || chrtDataFmt == 'T' || chrtDataFmt == 'O') { + if (chrtDataFmt == SPEED || chrtDataFmt == TEMPERATURE || chrtDataFmt == OTHER) { // High value at top loopStrt = valAxis - VALAXIS_STEP; loopEnd = VALAXIS_STEP / 2; @@ -769,8 +735,7 @@ void Chart::prntHorizMultiValueAxisLabel(const GFXfont* font) for (int j = loopStrt; (loopStp > 0) ? (j < loopEnd) : (j > loopEnd); j += loopStp) { sVal = formatLabel(axLabel); - // sVal = convNformatLabel(axLabel); - getdisplay().fillRect(cRoot.x, cRoot.y + j - 11, xOffset + 4, 21, bgColor); // Clear small area to remove potential chart lines + getdisplay().fillRect(cRoot.x, cRoot.y + j - 11, xOffset + 3, 21, bgColor); // Clear small area to remove potential chart lines drawTextRalign(cRoot.x + xOffset, cRoot.y + j + 7, sVal); // range value getdisplay().drawLine(cRoot.x + xOffset + 3, cRoot.y + j, cRoot.x + timAxis, cRoot.y + j, fgColor); @@ -779,9 +744,8 @@ void Chart::prntHorizMultiValueAxisLabel(const GFXfont* font) } // Draw chart line with thickness of 2px -void Chart::drawBoldLine(int16_t x1, int16_t y1, int16_t x2, int16_t y2) +void Chart::drawBoldLine(const int16_t x1, const int16_t y1, const int16_t x2, const int16_t y2) { - int16_t dx = std::abs(x2 - x1); int16_t dy = std::abs(y2 - y1); @@ -795,7 +759,7 @@ void Chart::drawBoldLine(int16_t x1, int16_t y1, int16_t x2, int16_t y2) } // Convert and format current axis label to user defined format; helper function for easier handling of OBP60Formatter -String Chart::convNformatLabel(double label) +String Chart::convNformatLabel(const double& label) { GwApi::BoatValue tmpBVal(dbName); // temporary boat value for string formatter String sVal; @@ -803,9 +767,9 @@ String Chart::convNformatLabel(double label) tmpBVal.setFormat(dbFormat); tmpBVal.valid = true; tmpBVal.value = label; - sVal = formatValue(&tmpBVal, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places + sVal = formatValue(&tmpBVal, *commonData, NO_SIMUDATA).svalue; // Formatted value as string including unit conversion and switching decimal places if (sVal.length() > 0 && sVal[0] == '!') { - sVal = sVal.substring(1); // cut leading "!" created at OBPFormatter for use with other font than 7SEG + sVal = sVal.substring(1); // cut leading "!" created at OBPFormatter; doesn't work for other fonts than 7SEG } return sVal; diff --git a/lib/obp60task/OBPcharts.h b/lib/obp60task/OBPcharts.h index c6d24b2..fbdcddd 100644 --- a/lib/obp60task/OBPcharts.h +++ b/lib/obp60task/OBPcharts.h @@ -22,6 +22,27 @@ protected: CommonData* commonData; GwLog* logger; + enum ChrtDataFormat { + WIND, + ROTATION, + SPEED, + DEPTH, + TEMPERATURE, + OTHER + }; + + static constexpr char HORIZONTAL = 'H'; + static constexpr char VERTICAL = 'V'; + static constexpr int8_t FULL_SIZE = 0; + static constexpr int8_t HALF_SIZE_LEFT = 1; + static constexpr int8_t HALF_SIZE_RIGHT = 2; + + static constexpr int8_t MIN_FREE_VALUES = 60; // free 60 values when chart line reaches chart end + static constexpr int8_t THRESHOLD_NO_DATA = 3; // max. seconds of invalid values in a row + static constexpr int8_t VALAXIS_STEP = 60; // pixels between two chart value axis labels + + static constexpr bool NO_SIMUDATA = true; // switch off simulation feature of function + RingBuffer& dataBuf; // Buffer to display //char chrtDir; // Chart timeline direction: 'H' = horizontal, 'V' = vertical //int8_t chrtSz; // Chart size: [0] = full size, [1] = half size left/top, [2] half size right/bottom @@ -45,10 +66,10 @@ protected: double chrtMax; // Range high end value double chrtMid; // Range mid value double rngStep; // Defines the step of adjustment (e.g. 10 m/s) for value axis range - bool recalcRngCntr = false; // Flag for re-calculation of mid value of chart for wind data types + bool recalcRngMid = false; // Flag for re-calculation of mid value of chart for wind data types String dbName, dbFormat; // Name and format of data buffer - char chrtDataFmt; // Data format of chart: 'S' = size values; 'D' = depth value, 'W' = degree of course or wind; 'R' rotational degrees + ChrtDataFormat chrtDataFmt; // Data format of chart boat data type double dbMIN_VAL; // Lowest possible value of buffer of type double dbMAX_VAL; // Highest possible value of buffer of type ; indicates invalid value in buffer size_t bufSize; // History buffer size: 1.920 values for 32 min. history chart @@ -67,32 +88,29 @@ protected: int x, y; // x and y coordinates for drawing int prevX, prevY; // Last x and y coordinates for drawing - static constexpr int8_t MIN_FREE_VALUES = 60; - static constexpr int8_t THRESHOLD_NO_DATA = 3; - static constexpr int8_t VALAXIS_STEP = 60; - bool setChartDimensions(const char direction, const int8_t size); //define dimensions and start points for chart - void drawChrt(const char chrtDir, int8_t& chrtIntv, GwApi::BoatValue& currValue); // Draw chart line - void getBufStartNSize(int8_t& chrtIntv); // Identify buffer size and buffer start position for chart + void drawChrt(const char chrtDir, const int8_t chrtIntv, GwApi::BoatValue& currValue); // Draw chart line + void getBufferStartNSize(const int8_t chrtIntv); // Identify buffer size and buffer start position for chart void calcChrtBorders(double& rngMin, double& rngMid, double& rngMax, double& rng); // Calculate chart points for value axis and return range between and - void drawChrtTimeAxis(const char chrtDir, const int8_t chrtSz, int8_t& chrtIntv); // Draw time axis of chart, value and lines - void drawChrtValAxis(const char chrtDir, const int8_t chrtSz); // Draw value axis of chart, value and lines + void drawChartLines(const char direction, const int8_t chrtIntv, const double chrtScale); // Draw chart graph + Pos setCurrentChartPoint(const int i, const char direction, const double chrtVal, const double chrtScale); // Set current chart point to draw + void drawChrtTimeAxis(const char chrtDir, const int8_t chrtSz, const int8_t chrtIntv); // Draw time axis of chart, value and lines + void drawChrtValAxis(const char chrtDir, const int8_t chrtSz, bool prntLabel); // Draw value axis of chart, value and lines void prntCurrValue(const char chrtDir, GwApi::BoatValue& currValue); // Add current boat data value to chart void prntNoValidData(const char chrtDir); // print message for no valid data available - double getAngleRng(double center, size_t amount); // Calculate range between chart center and edges - void prntHorizThreeValueAxisLabel(const GFXfont* font); // print horizontal axis label with only three values: top, mid, and bottom - void prntHorizMultiValueAxisLabel(const GFXfont* font); // print horizontal axis label with multiple axis lines - void drawBoldLine(int16_t x1, int16_t y1, int16_t x2, int16_t y2); // Draw chart line with thickness of 2px - String convNformatLabel(double label); // Convert and format current axis label to user defined format; helper function for easier handling of OBP60Formatter + double getAngleRng(const double center, size_t amount); // Calculate range between chart center and edges + void prntVerticChartThreeValueAxisLabel(const GFXfont* font); // print value axis label with only three values: top, mid, and bottom for vertical chart + void prntHorizChartThreeValueAxisLabel(const GFXfont* font); // print value axis label with only three values: top, mid, and bottom for horizontal chart + void prntHorizChartMultiValueAxisLabel(const GFXfont* font); // print value axis label with multiple axis lines for horizontal chart + void drawBoldLine(const int16_t x1, const int16_t y1, const int16_t x2, const int16_t y2); // Draw chart line with thickness of 2px + String convNformatLabel(const double& label); // Convert and format current axis label to user defined format; helper function for easier handling of OBP60Formatter String formatLabel(const double& label); // Format current axis label for printing w/o data format conversion (has been done earlier) public: // Define default chart range and range step for each boat data type static std::map dfltChrtDta; - // Chart(RingBuffer& dataBuf, char chrtDir, int8_t chrtSz, double dfltRng, CommonData& common, bool useSimuData); // Chart object of data chart Chart(RingBuffer& dataBuf, double dfltRng, CommonData& common, bool useSimuData); // Chart object of data chart ~Chart(); - // void showChrt(GwApi::BoatValue currValue, int8_t& chrtIntv, bool showCurrValue); // Perform all actions to draw chart - void showChrt(char chrtDir, int8_t chrtSz, int8_t& chrtIntv, bool showCurrValue, GwApi::BoatValue currValue); // Perform all actions to draw chart + void showChrt(char chrtDir, int8_t chrtSz, const int8_t chrtIntv, bool prntName, bool showCurrValue, GwApi::BoatValue currValue); // Perform all actions to draw chart }; diff --git a/lib/obp60task/PageOneValue.cpp b/lib/obp60task/PageOneValue.cpp index 59a02bf..2a0075b 100644 --- a/lib/obp60task/PageOneValue.cpp +++ b/lib/obp60task/PageOneValue.cpp @@ -10,11 +10,32 @@ class PageOneValue : public Page { private: GwLog* logger; + enum PageMode { + VALUE, + CHART, + BOTH + }; + enum DisplayMode { + FULL, + HALF + }; + + static constexpr char HORIZONTAL = 'H'; + static constexpr char VERTICAL = 'V'; + static constexpr int8_t FULL_SIZE = 0; + static constexpr int8_t HALF_SIZE_TOP = 1; + static constexpr int8_t HALF_SIZE_BOTTOM = 2; + + static constexpr bool PRNT_NAME = true; + static constexpr bool NO_PRNT_NAME = false; + static constexpr bool PRNT_VALUE = true; + static constexpr bool NO_PRNT_VALUE = false; + int width; // Screen width int height; // Screen height bool keylock = false; // Keylock - char pageMode = 'V'; // Page mode: 'V' for value, 'C' for chart, 'B' for both + PageMode pageMode = VALUE; // Page display mode int8_t dataIntv = 1; // Update interval for wind history chart: // (1)|(2)|(3)|(4)|(8) x 240 seconds for 4, 8, 12, 16, 32 min. history chart @@ -31,14 +52,14 @@ private: // Data buffer pointer (owned by HstryBuffers) RingBuffer* dataHstryBuf = nullptr; - std::unique_ptr dataChart; // Chart object, full and half size + std::unique_ptr dataChart; // Chart object - void showData(GwApi::BoatValue* bValue1, char size) + void showData(GwApi::BoatValue* bValue1, DisplayMode mode) { int nameXoff, nameYoff, unitXoff, unitYoff, value1Xoff, value1Yoff; const GFXfont *nameFnt, *unitFnt, *valueFnt1, *valueFnt2, *valueFnt3; - if (size == 'F') { // full size data display + if (mode == FULL) { // full size data display nameXoff = 0; nameYoff = 0; nameFnt = &Ubuntu_Bold32pt8b; @@ -51,17 +72,17 @@ private: valueFnt2 = &Ubuntu_Bold32pt8b; valueFnt3 = &DSEG7Classic_BoldItalic60pt7b; } else { // half size data and chart display - nameXoff = 105; - nameYoff = -35; + nameXoff = -10; + nameYoff = -34; nameFnt = &Ubuntu_Bold20pt8b; - unitXoff = -35; - unitYoff = -102; + unitXoff = 63; + unitYoff = -119; unitFnt = &Ubuntu_Bold12pt8b; valueFnt1 = &Ubuntu_Bold12pt8b; - value1Xoff = 105; - value1Yoff = -102; + value1Xoff = 153; + value1Yoff = -119; valueFnt2 = &Ubuntu_Bold20pt8b; - valueFnt3 = &DSEG7Classic_BoldItalic30pt7b; + valueFnt3 = &DSEG7Classic_BoldItalic42pt7b; } String name1 = xdrDelete(bValue1->getName()); // Value name @@ -156,14 +177,18 @@ public: { if (dataHstryBuf) { // if boat data type supports charts - // Set page mode value | full chart | value/half chart + // Set page mode value | value/half chart | full chart if (key == 1) { - if (pageMode == 'V') { - pageMode = 'C'; - } else if (pageMode == 'C') { - pageMode = 'B'; - } else { - pageMode = 'V'; + switch (pageMode) { + case VALUE: + pageMode = BOTH; + break; + case BOTH: + pageMode = CHART; + break; + case CHART: + pageMode = VALUE; + break; } return 0; // Commit the key } @@ -208,6 +233,7 @@ public: #endif // buffer initialization will fail, if page is default page, because is not executed at system start for default page if (!dataChart) { // Create chart objects if they don't exist + GwApi::BoatValue* bValue1 = pageData.values[0]; // Page boat data element String bValName1 = bValue1->getName(); // Value name String bValFormat = bValue1->getFormat(); // Value format @@ -216,7 +242,6 @@ public: if (dataHstryBuf) { dataChart.reset(new Chart(*dataHstryBuf, Chart::dfltChrtDta[bValFormat].range, *commonData, useSimuData)); - //dataHfChart.reset(new Chart(*dataHstryBuf, 'H', 2, Chart::dfltChrtDta[bValFormat].range, *commonData, useSimuData)); LOG_DEBUG(GwLog::DEBUG, "PageOneValue: Created chart objects for %s", bValName1); } else { LOG_DEBUG(GwLog::DEBUG, "PageOneValue: No chart objects available for %s", bValName1); @@ -228,7 +253,6 @@ public: int displayPage(PageData& pageData) { - LOG_DEBUG(GwLog::LOG, "Display PageOneValue"); // Get boat value for page @@ -250,19 +274,19 @@ public: getdisplay().setPartialWindow(0, 0, width, height); // Set partial update - if (pageMode == 'V' || dataHstryBuf == nullptr) { + if (pageMode == VALUE || dataHstryBuf == nullptr) { // show only data value; ignore other pageMode options if no chart supported boat data history buffer is available - showData(bValue1, 'F'); + showData(bValue1, FULL); - } else if (pageMode == 'C') { // show only data chart + } else if (pageMode == CHART) { // show only data chart if (dataChart) { - dataChart->showChrt('H', 0, dataIntv, true, *bValue1); + dataChart->showChrt(HORIZONTAL, FULL_SIZE, dataIntv, PRNT_NAME, PRNT_VALUE, *bValue1); } - } else if (pageMode == 'B') { // show data value and chart - showData(bValue1, 'H'); + } else if (pageMode == BOTH) { // show data value and chart + showData(bValue1, HALF); if (dataChart) { - dataChart->showChrt('H', 2, dataIntv, false, *bValue1); + dataChart->showChrt(HORIZONTAL, HALF_SIZE_BOTTOM, dataIntv, NO_PRNT_NAME, NO_PRNT_VALUE, *bValue1); } } diff --git a/lib/obp60task/PageWindPlot.cpp b/lib/obp60task/PageWindPlot.cpp index d90bab9..924687c 100644 --- a/lib/obp60task/PageWindPlot.cpp +++ b/lib/obp60task/PageWindPlot.cpp @@ -11,20 +11,28 @@ class PageWindPlot : public Page { private: GwLog* logger; - static constexpr char SHOW_WIND_DIR = 'D'; - static constexpr char SHOW_WIND_SPEED = 'S'; - static constexpr char SHOW_BOTH = 'B'; + enum ChartMode { + DIRECTION, + SPEED, + BOTH + }; + static constexpr char HORIZONTAL = 'H'; static constexpr char VERTICAL = 'V'; static constexpr int8_t FULL_SIZE = 0; - static constexpr int8_t HALF_SIZE_TOP = 1; - static constexpr int8_t HALF_SIZE_BOTTOM = 2; + static constexpr int8_t HALF_SIZE_LEFT = 1; + static constexpr int8_t HALF_SIZE_RIGHT = 2; + + static constexpr bool PRNT_NAME = true; + static constexpr bool NO_PRNT_NAME = false; + static constexpr bool PRNT_VALUE = true; + static constexpr bool NO_PRNT_VALUE = false; int width; // Screen width int height; // Screen height bool keylock = false; // Keylock - char chrtMode = 'D'; // Chart mode: 'D' for TWD, 'S' for TWS, 'B' for both + ChartMode chrtMode = DIRECTION; bool showTruW = true; // Show true wind or apparent wind in chart area bool oldShowTruW = false; // remember recent user selection of wind data type @@ -46,8 +54,8 @@ private: RingBuffer* awsHstry = nullptr; // Chart objects - std::unique_ptr twdChart, awdChart; // Chart object for wind direction, full size - std::unique_ptr twsChart, awsChart; // Chart object for wind speed, full size + std::unique_ptr twdChart, awdChart; // Chart object for wind direction + std::unique_ptr twsChart, awsChart; // Chart object for wind speed // Active charts and values Chart* wdChart = nullptr; @@ -89,14 +97,14 @@ public: // Key functions virtual int handleKey(int key) { - // Set chart mode TWD | TWS + // Set chart mode if (key == 1) { - if (chrtMode == SHOW_WIND_DIR) { - chrtMode = SHOW_WIND_SPEED; - } else if (chrtMode == SHOW_WIND_SPEED) { - chrtMode = SHOW_BOTH; + if (chrtMode == DIRECTION) { + chrtMode = SPEED; + } else if (chrtMode == SPEED) { + chrtMode = BOTH; } else { - chrtMode = SHOW_WIND_DIR; + chrtMode = DIRECTION; } return 0; // Commit the key } @@ -192,35 +200,6 @@ public: LOG_DEBUG(GwLog::LOG, "Display PageWindPlot"); ulong pageTime = millis(); - /* if (!twdChart) { // Create true wind charts if they don't exist - twdHstry = pageData.hstryBuffers->getBuffer("TWD"); - twsHstry = pageData.hstryBuffers->getBuffer("TWS"); - - if (twdHstry) { - twdChart.reset(new Chart(*twdHstry, Chart::dfltChrtDta["formatCourse"].range, *commonData, useSimuData)); - } - if (twsHstry) { - twsChart.reset(new Chart(*twsHstry, Chart::dfltChrtDta["formatKnots"].range, *commonData, useSimuData)); - } - } - - if (!awdChart) { // Create apparent wind charts if they don't exist - awdHstry = pageData.hstryBuffers->getBuffer("AWD"); - awsHstry = pageData.hstryBuffers->getBuffer("AWS"); - - if (awdHstry) { - awdChart.reset(new Chart(*awdHstry, Chart::dfltChrtDta["formatCourse"].range, *commonData, useSimuData)); - } - if (awsHstry) { - awsChart.reset(new Chart(*awsHstry, Chart::dfltChrtDta["formatKnots"].range, *commonData, useSimuData)); - } - if (twdHstry && twsHstry && awdHstry && awsHstry) { - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Created wind charts"); - } else { - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Some/all chart objects for wind data missing"); - } - } */ - if (showTruW != oldShowTruW) { // Switch active charts based on showTruW @@ -247,22 +226,22 @@ public: getdisplay().setPartialWindow(0, 0, width, height); // Set partial update getdisplay().setTextColor(commonData->fgcolor); - if (chrtMode == SHOW_WIND_DIR) { + if (chrtMode == DIRECTION) { if (wdChart) { - wdChart->showChrt(VERTICAL, FULL_SIZE, dataIntv, true, *wdBVal); + wdChart->showChrt(VERTICAL, FULL_SIZE, dataIntv, PRNT_NAME, PRNT_VALUE, *wdBVal); } - } else if (chrtMode == SHOW_WIND_SPEED) { + } else if (chrtMode == SPEED) { if (wsChart) { - wsChart->showChrt(HORIZONTAL, FULL_SIZE, dataIntv, true, *wsBVal); + wsChart->showChrt(HORIZONTAL, FULL_SIZE, dataIntv, PRNT_NAME, PRNT_VALUE, *wsBVal); } - } else if (chrtMode == SHOW_BOTH) { + } else if (chrtMode == BOTH) { if (wdChart) { - wdChart->showChrt(VERTICAL, HALF_SIZE_TOP, dataIntv, true, *wdBVal); + wdChart->showChrt(VERTICAL, HALF_SIZE_LEFT, dataIntv, PRNT_NAME, PRNT_VALUE, *wdBVal); } if (wsChart) { - wsChart->showChrt(VERTICAL, HALF_SIZE_BOTTOM, dataIntv, true, *wsBVal); + wsChart->showChrt(VERTICAL, HALF_SIZE_RIGHT, dataIntv, PRNT_NAME, PRNT_VALUE, *wsBVal); } } From 86a078690a864d09996004afacdbe27c4a0540de Mon Sep 17 00:00:00 2001 From: Ulrich Meine Date: Sun, 11 Jan 2026 15:30:13 +0100 Subject: [PATCH 089/121] PageTwoValues: added chart display option --- lib/obp60task/PageOneValue.cpp | 15 +- lib/obp60task/PageTwoValues.cpp | 445 +++++++++++++++++++++----------- lib/obp60task/PageWindPlot.cpp | 2 - 3 files changed, 306 insertions(+), 156 deletions(-) diff --git a/lib/obp60task/PageOneValue.cpp b/lib/obp60task/PageOneValue.cpp index 2a0075b..c0ddc7e 100644 --- a/lib/obp60task/PageOneValue.cpp +++ b/lib/obp60task/PageOneValue.cpp @@ -12,8 +12,8 @@ private: enum PageMode { VALUE, - CHART, - BOTH + BOTH, + CHART }; enum DisplayMode { FULL, @@ -54,6 +54,7 @@ private: RingBuffer* dataHstryBuf = nullptr; std::unique_ptr dataChart; // Chart object + // display data value in display [FULL|HALF] void showData(GwApi::BoatValue* bValue1, DisplayMode mode) { int nameXoff, nameYoff, unitXoff, unitYoff, value1Xoff, value1Yoff; @@ -158,17 +159,17 @@ public: Page::setupKeys(); #if defined BOARD_OBP60S3 - constexpr int ZOOM_IDX = 4; + constexpr int ZOOM_KEY = 4; #elif defined BOARD_OBP40S3 - constexpr int ZOOM_IDX = 1; + constexpr int ZOOM_KEY = 1; #endif if (dataHstryBuf) { // show "Mode" key only if chart supported boat data type is available commonData->keydata[0].label = "MODE"; - commonData->keydata[ZOOM_IDX].label = "ZOOM"; + commonData->keydata[ZOOM_KEY].label = "ZOOM"; } else { commonData->keydata[0].label = ""; - commonData->keydata[ZOOM_IDX].label = ""; + commonData->keydata[ZOOM_KEY].label = ""; } } @@ -177,7 +178,7 @@ public: { if (dataHstryBuf) { // if boat data type supports charts - // Set page mode value | value/half chart | full chart + // Set page mode: value | value/half chart | full chart if (key == 1) { switch (pageMode) { case VALUE: diff --git a/lib/obp60task/PageTwoValues.cpp b/lib/obp60task/PageTwoValues.cpp index eaa59d4..d4c0d98 100644 --- a/lib/obp60task/PageTwoValues.cpp +++ b/lib/obp60task/PageTwoValues.cpp @@ -3,176 +3,327 @@ #include "Pagedata.h" #include "OBP60Extensions.h" #include "BoatDataCalibration.h" +#include "OBPDataOperations.h" +#include "OBPcharts.h" -class PageTwoValues : public Page -{ - public: - PageTwoValues(CommonData &common){ - commonData = &common; - common.logger->logDebug(GwLog::LOG,"Instantiate PageTwoValue"); +class PageTwoValues : public Page { +private: + GwLog* logger; + + enum PageMode { + VALUES, + VAL1_CHART, + VAL2_CHART, + CHARTS + }; + enum DisplayMode { + FULL, + HALF + }; + + static constexpr char HORIZONTAL = 'H'; + static constexpr char VERTICAL = 'V'; + static constexpr int8_t FULL_SIZE = 0; + static constexpr int8_t HALF_SIZE_TOP = 1; + static constexpr int8_t HALF_SIZE_BOTTOM = 2; + + static constexpr bool PRNT_NAME = true; + static constexpr bool NO_PRNT_NAME = false; + static constexpr bool PRNT_VALUE = true; + static constexpr bool NO_PRNT_VALUE = false; + + static constexpr int YOFFSET = 130; // y offset for display of 2nd boat value + + int width; // Screen width + int height; // Screen height + + bool keylock = false; // Keylock + PageMode pageMode = VALUES; // Page display mode + int8_t dataIntv = 1; // Update interval for wind history chart: + // (1)|(2)|(3)|(4)|(8) x 240 seconds for 4, 8, 12, 16, 32 min. history chart + + // String lengthformat; + bool useSimuData; + bool holdValues; + String flashLED; + String backlightMode; + String tempFormat; + + // Data buffer pointer (owned by HstryBuffers) + static constexpr int NUMVALUES = 2; // two data values in this page + RingBuffer* dataHstryBuf[NUMVALUES] = { nullptr }; + std::unique_ptr dataChart[NUMVALUES]; // Chart object + + // Old values for hold function + String sValueOld[NUMVALUES] = { "", "" }; + String unitOld[NUMVALUES] = { "", "" }; + + // display data values in display [FULL|HALF] + void showData(const std::vector& bValue, DisplayMode mode) + { + getdisplay().setTextColor(commonData->fgcolor); + + int numValues = bValue.size(); // do we have to handle 1 or 2 values? + + for (int i = 0; i < numValues; i++) { + int yOffset = YOFFSET * i; + String name = xdrDelete(bValue[i]->getName()); // Value name + name = name.substring(0, 6); // String length limit for value name + calibrationData.calibrateInstance(bValue[i], logger); // Check if boat data value is to be calibrated + double value = bValue[i]->value; // Value as double in SI unit + bool valid = bValue[i]->valid; // Valid information + String sValue = formatValue(bValue[i], *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places + String unit = formatValue(bValue[i], *commonData).unit; // Unit of value + + // Show name + getdisplay().setFont(&Ubuntu_Bold20pt8b); + getdisplay().setCursor(20, 75 + yOffset); + getdisplay().print(name); // name + + // Show unit + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().setCursor(20, 125 + yOffset); + + if (holdValues) { + getdisplay().print(unitOld[i]); // name + } else { + getdisplay().print(unit); // name + } + + // Switch font depending on value format and adjust position + if (bValue[i]->getFormat() == "formatLatitude" || bValue[i]->getFormat() == "formatLongitude") { + getdisplay().setFont(&Ubuntu_Bold20pt8b); + getdisplay().setCursor(50, 125 + yOffset); + } else if (bValue[i]->getFormat() == "formatTime" || bValue[i]->getFormat() == "formatDate") { + getdisplay().setFont(&Ubuntu_Bold20pt8b); + getdisplay().setCursor(170, 105 + yOffset); + } else { // Default font for other formats + getdisplay().setFont(&DSEG7Classic_BoldItalic42pt7b); + getdisplay().setCursor(180, 125 + yOffset); + } + + // Show bus data + if (!holdValues || useSimuData) { + getdisplay().print(sValue); // Real value as formated string + } else { + getdisplay().print(sValueOld[i]); // Old value as formated string + } + + if (valid == true) { + sValueOld[i] = sValue; // Save the old value + unitOld[i] = unit; // Save the old unit + } + } + + if (numValues == 2 && mode == FULL) { // print line only, if we want to show 2 data values + getdisplay().fillRect(0, 145, width, 3, commonData->fgcolor); // Horizontal line 3 pix + } } - virtual int handleKey(int key){ - // Code for keylock - if(key == 11){ +public: + PageTwoValues(CommonData& common) + { + commonData = &common; + logger = commonData->logger; + LOG_DEBUG(GwLog::LOG, "Instantiate PageTwoValues"); + + width = getdisplay().width(); // Screen width + height = getdisplay().height(); // Screen height + + // Get config data + // lengthformat = commonData->config->getString(commonData->config->lengthFormat); + useSimuData = commonData->config->getBool(commonData->config->useSimuData); + holdValues = commonData->config->getBool(commonData->config->holdvalues); + flashLED = commonData->config->getString(commonData->config->flashLED); + backlightMode = commonData->config->getString(commonData->config->backlight); + tempFormat = commonData->config->getString(commonData->config->tempFormat); // [K|°C|°F] + } + + virtual void setupKeys() + { + Page::setupKeys(); + +#if defined BOARD_OBP60S3 + constexpr int ZOOM_KEY = 4; +#elif defined BOARD_OBP40S3 + constexpr int ZOOM_KEY = 1; +#endif + + if (dataHstryBuf[0] || dataHstryBuf[1]) { // show "Mode" key only if at least 1 chart supported boat data type is available + commonData->keydata[0].label = "MODE"; + commonData->keydata[ZOOM_KEY].label = "ZOOM"; + } else { + commonData->keydata[0].label = ""; + commonData->keydata[ZOOM_KEY].label = ""; + } + } + + // Key functions + virtual int handleKey(int key) + { + if (dataHstryBuf[0] || dataHstryBuf[1]) { // if at least 1 boat data type supports charts + + // Set page mode: value | value/half chart | full charts + if (key == 1) { + switch (pageMode) { + + case VALUES: + + if (dataHstryBuf[0]) { + pageMode = VAL1_CHART; + } else if (dataHstryBuf[1]) { + pageMode = VAL2_CHART; + } + break; + + case VAL1_CHART: + + if (dataHstryBuf[1]) { + pageMode = VAL2_CHART; + } else { + pageMode = CHARTS; + } + break; + + case VAL2_CHART: + pageMode = CHARTS; + break; + + case CHARTS: + pageMode = VALUES; + break; + } + return 0; // Commit the key + } + + // Set time frame to show for history chart +#if defined BOARD_OBP60S3 + if (key == 5) { +#elif defined BOARD_OBP40S3 + if (key == 2) { +#endif + if (dataIntv == 1) { + dataIntv = 2; + } else if (dataIntv == 2) { + dataIntv = 3; + } else if (dataIntv == 3) { + dataIntv = 4; + } else if (dataIntv == 4) { + dataIntv = 8; + } else { + dataIntv = 1; + } + return 0; // Commit the key + } + } + + // Keylock function + if (key == 11) { // Code for keylock commonData->keylock = !commonData->keylock; - return 0; // Commit the key + return 0; // Commit the key } return key; } - int displayPage(PageData &pageData){ - GwConfigHandler *config = commonData->config; - GwLog *logger = commonData->logger; - - // Old values for hold function - static String svalue1old = ""; - static String unit1old = ""; - static String svalue2old = ""; - static String unit2old = ""; - - // Get config data - String lengthformat = config->getString(config->lengthFormat); - // bool simulation = config->getBool(config->useSimuData); - bool holdvalues = config->getBool(config->holdvalues); - String flashLED = config->getString(config->flashLED); - String backlightMode = config->getString(config->backlight); - - // Get boat values #1 - GwApi::BoatValue *bvalue1 = pageData.values[0]; // First element in list (only one value by PageOneValue) - String name1 = xdrDelete(bvalue1->getName()); // Value name - name1 = name1.substring(0, 6); // String length limit for value name - calibrationData.calibrateInstance(bvalue1, logger); // Check if boat data value is to be calibrated - double value1 = bvalue1->value; // Value as double in SI unit - bool valid1 = bvalue1->valid; // Valid information - String svalue1 = formatValue(bvalue1, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places - String unit1 = formatValue(bvalue1, *commonData).unit; // Unit of value - - // Get boat values #2 - GwApi::BoatValue *bvalue2 = pageData.values[1]; // Second element in list - String name2 = xdrDelete(bvalue2->getName()); // Value name - name2 = name2.substring(0, 6); // String length limit for value name - calibrationData.calibrateInstance(bvalue2, logger); // Check if boat data value is to be calibrated - double value2 = bvalue2->value; // Value as double in SI unit - bool valid2 = bvalue2->valid; // Valid information - String svalue2 = formatValue(bvalue2, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places - String unit2 = formatValue(bvalue2, *commonData).unit; // Unit of value - - // Optical warning by limit violation (unused) - if(String(flashLED) == "Limit Violation"){ + virtual void displayNew(PageData& pageData) + { +#ifdef BOARD_OBP60S3 + // Clear optical warning + if (flashLED == "Limit Violation") { setBlinkingLED(false); - setFlashLED(false); + setFlashLED(false); + } +#endif + // buffer initialization will fail, if page is default page, because is not executed at system start for default page + for (int i = 0; i < NUMVALUES; i++) { + if (!dataChart[i]) { // Create chart objects if they don't exist + + GwApi::BoatValue* bValue = pageData.values[i]; // Page boat data element + String bValName = bValue->getName(); // Value name + String bValFormat = bValue->getFormat(); // Value format + + dataHstryBuf[i] = pageData.hstryBuffers->getBuffer(bValName); + + if (dataHstryBuf[i]) { + dataChart[i].reset(new Chart(*dataHstryBuf[i], Chart::dfltChrtDta[bValFormat].range, *commonData, useSimuData)); + LOG_DEBUG(GwLog::DEBUG, "PageTwoValues: Created chart object%d for %s", i, bValName.c_str()); + } else { + LOG_DEBUG(GwLog::DEBUG, "PageTwoValues: No chart object available for %s", bValName.c_str()); + } + } } - // Logging boat values - if (bvalue1 == NULL) return PAGE_OK; // WTF why this statement? - LOG_DEBUG(GwLog::LOG,"Drawing at PageTwoValues, %s: %f, %s: %f", name1.c_str(), value1, name2.c_str(), value2); + setupKeys(); // adjust key depending on chart supported boat data type + } + + int displayPage(PageData& pageData) + { + LOG_DEBUG(GwLog::LOG, "Display PageTwoValues"); + + // Get boat values for page + std::vector bValue; + bValue.push_back(pageData.values[0]); // Page boat data element 1 + bValue.push_back(pageData.values[1]); // Page boat data element 2 + + // Optical warning by limit violation (unused) + if (String(flashLED) == "Limit Violation") { + setBlinkingLED(false); + setFlashLED(false); + } + + if (bValue[0] == NULL && bValue[1] == NULL) + return PAGE_OK; // no data, no page to display + + LOG_DEBUG(GwLog::DEBUG, "PageTwoValues: printing #1: %s, %.3f, #2: %s, %.3f", + bValue[0]->getName().c_str(), bValue[0]->value, bValue[1]->getName().c_str(), bValue[1]->value); // Draw page //*********************************************************** - // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + getdisplay().setPartialWindow(0, 0, width, height); // Set partial update - // ############### Value 1 ################ + if (pageMode == VALUES || (dataHstryBuf[0] == nullptr && dataHstryBuf[1] == nullptr)) { + // show only data value; ignore other pageMode options if no chart supported boat data history buffer is available + showData(bValue, FULL); - // Show name - getdisplay().setTextColor(commonData->fgcolor); - getdisplay().setFont(&Ubuntu_Bold20pt8b); - getdisplay().setCursor(20, 80); - getdisplay().print(name1); // Page name + } else if (pageMode == VAL1_CHART) { // show data value 1 and chart + showData({bValue[0]}, HALF); + if (dataChart[0]) { + dataChart[0]->showChrt(HORIZONTAL, HALF_SIZE_BOTTOM, dataIntv, NO_PRNT_NAME, NO_PRNT_VALUE, *bValue[0]); + } - // Show unit - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(20, 130); - if(holdvalues == false){ - getdisplay().print(unit1); // Unit - } - else{ - getdisplay().print(unit1old); - } + } else if (pageMode == VAL2_CHART) { // show data value 2 and chart + showData({bValue[1]}, HALF); + if (dataChart[1]) { + dataChart[1]->showChrt(HORIZONTAL, HALF_SIZE_BOTTOM, dataIntv, NO_PRNT_NAME, NO_PRNT_VALUE, *bValue[1]); + } - // Switch font if format for any values - if(bvalue1->getFormat() == "formatLatitude" || bvalue1->getFormat() == "formatLongitude"){ - getdisplay().setFont(&Ubuntu_Bold20pt8b); - getdisplay().setCursor(50, 130); - } - else if(bvalue1->getFormat() == "formatTime" || bvalue1->getFormat() == "formatDate"){ - getdisplay().setFont(&Ubuntu_Bold20pt8b); - getdisplay().setCursor(170, 105); - } - else{ - getdisplay().setFont(&DSEG7Classic_BoldItalic42pt7b); - getdisplay().setCursor(180, 130); - } - - // Show bus data - if(holdvalues == false){ - getdisplay().print(svalue1); // Real value as formated string - } - else{ - getdisplay().print(svalue1old); // Old value as formated string - } - if(valid1 == true){ - svalue1old = svalue1; // Save the old value - unit1old = unit1; // Save the old unit - } - - // ############### Horizontal Line ################ - - // Horizontal line 3 pix - getdisplay().fillRect(0, 145, 400, 3, commonData->fgcolor); - - // ############### Value 2 ################ - - // Show name - getdisplay().setFont(&Ubuntu_Bold20pt8b); - getdisplay().setCursor(20, 190); - getdisplay().print(name2); // Page name - - // Show unit - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(20, 240); - if(holdvalues == false){ - getdisplay().print(unit2); // Unit - } - else{ - getdisplay().print(unit2old); - } - - // Switch font if format for any values - if(bvalue2->getFormat() == "formatLatitude" || bvalue2->getFormat() == "formatLongitude"){ - getdisplay().setFont(&Ubuntu_Bold20pt8b); - getdisplay().setCursor(50, 240); - } - else if(bvalue2->getFormat() == "formatTime" || bvalue2->getFormat() == "formatDate"){ - getdisplay().setFont(&Ubuntu_Bold20pt8b); - getdisplay().setCursor(170, 215); - } - else{ - getdisplay().setFont(&DSEG7Classic_BoldItalic42pt7b); - getdisplay().setCursor(180, 240); - } - - // Show bus data - if(holdvalues == false){ - getdisplay().print(svalue2); // Real value as formated string - } - else{ - getdisplay().print(svalue2old); // Old value as formated string - } - if(valid2 == true){ - svalue2old = svalue2; // Save the old value - unit2old = unit2; // Save the old unit + } else if (pageMode == CHARTS) { // show both data charts + if (dataChart[0]) { + if (dataChart[1]) { + dataChart[0]->showChrt(HORIZONTAL, HALF_SIZE_TOP, dataIntv, PRNT_NAME, PRNT_VALUE, *bValue[0]); + } else { + dataChart[0]->showChrt(HORIZONTAL, FULL_SIZE, dataIntv, PRNT_NAME, PRNT_VALUE, *bValue[0]); + } + } + if (dataChart[1]) { + if (dataChart[0]) { + dataChart[1]->showChrt(HORIZONTAL, HALF_SIZE_BOTTOM, dataIntv, PRNT_NAME, PRNT_VALUE, *bValue[1]); + } else { + dataChart[1]->showChrt(HORIZONTAL, FULL_SIZE, dataIntv, PRNT_NAME, PRNT_VALUE, *bValue[1]); + } + } } return PAGE_UPDATE; }; }; -static Page *createPage(CommonData &common){ +static Page* createPage(CommonData& common) +{ return new PageTwoValues(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 @@ -181,10 +332,10 @@ static Page *createPage(CommonData &common){ * this will be number of BoatValue pointers in pageData.values */ PageDescription registerPageTwoValues( - "TwoValues", // Page name - createPage, // Action - 2, // Number of bus values depends on selection in Web configuration - true // Show display header on/off + "TwoValues", // Page name + createPage, // Action + 2, // Number of bus values depends on selection in Web configuration + true // Show display header on/off ); #endif diff --git a/lib/obp60task/PageWindPlot.cpp b/lib/obp60task/PageWindPlot.cpp index 924687c..f5745f7 100644 --- a/lib/obp60task/PageWindPlot.cpp +++ b/lib/obp60task/PageWindPlot.cpp @@ -163,8 +163,6 @@ public: oldShowTruW = !showTruW; // Force chart update in displayPage #endif - // With chart object initialization being performed here, PageWindPlot won't properly work as default page, - // because is not executed at system start for default page if (!twdChart) { // Create true wind charts if they don't exist twdHstry = pageData.hstryBuffers->getBuffer("TWD"); twsHstry = pageData.hstryBuffers->getBuffer("TWS"); From cd3c99d509ff0eb272b13b8045de7591c45bed1b Mon Sep 17 00:00:00 2001 From: Ulrich Meine Date: Sun, 11 Jan 2026 17:07:27 +0100 Subject: [PATCH 090/121] PageOneValue: fix unit position --- lib/obp60task/PageOneValue.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/obp60task/PageOneValue.cpp b/lib/obp60task/PageOneValue.cpp index c0ddc7e..6349f29 100644 --- a/lib/obp60task/PageOneValue.cpp +++ b/lib/obp60task/PageOneValue.cpp @@ -76,7 +76,7 @@ private: nameXoff = -10; nameYoff = -34; nameFnt = &Ubuntu_Bold20pt8b; - unitXoff = 63; + unitXoff = -295; unitYoff = -119; unitFnt = &Ubuntu_Bold12pt8b; valueFnt1 = &Ubuntu_Bold12pt8b; From 0b79b7e2ef173e856320b9401a11b71e1048d471 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Thu, 15 Jan 2026 15:08:42 +0100 Subject: [PATCH 091/121] Modify PageDigitalOut --- lib/obp60task/OBP60Extensions.cpp | 47 +++++++++++++++++++++++++++---- lib/obp60task/OBP60Extensions.h | 3 +- lib/obp60task/PageDigitalOut.cpp | 24 ++++++++++------ lib/obp60task/PageNavigation.cpp | 19 +++++++------ 4 files changed, 70 insertions(+), 23 deletions(-) diff --git a/lib/obp60task/OBP60Extensions.cpp b/lib/obp60task/OBP60Extensions.cpp index c8729f9..99dedce 100644 --- a/lib/obp60task/OBP60Extensions.cpp +++ b/lib/obp60task/OBP60Extensions.cpp @@ -57,7 +57,7 @@ GxEPD2_BW & getdisplay(){r #endif // Horter I2C moduls -PCF8574 pcf8574_Out(PCF8574_I2C_ADDR1); // First digital output modul PCF8574 from Horter +PCF8574 pcf8574_Modul1(PCF8574_I2C_ADDR1); // First digital IO modul PCF8574 from Horter // FRAM Adafruit_FRAM_I2C fram; @@ -89,8 +89,8 @@ void hardwareInit(GwApi *api) Wire.begin(); // Init PCF8574 digital outputs Wire.setClock(I2C_SPEED_LOW); // Set I2C clock on 10 kHz - if(pcf8574_Out.begin()){ // Initialize PCF8574 - pcf8574_Out.write8(255); // Clear all outputs + if(pcf8574_Modul1.begin()){ // Initialize PCF8574 + pcf8574_Modul1.write8(255); // Clear all outputs } Wire.setClock(I2C_SPEED); // Set I2C clock on 100 kHz fram = Adafruit_FRAM_I2C(); @@ -193,13 +193,29 @@ void powerInit(String powermode) { } } -void setPCF8574PortPin(uint pin, uint8_t value){ +/* Old function +void setPCF8574PortPin(uint8_t pin, uint8_t value){ Wire.setClock(I2C_SPEED_LOW); // Set I2C clock on 10 kHz - if(pcf8574_Out.begin()){ // Check available and initialize PCF8574 - pcf8574_Out.write(pin, value); // Toggle pin + if(pcf8574_Modul1.begin()){ // Check available and initialize PCF8574 + pcf8574_Modul1.write(pin, value); // Set pin } Wire.setClock(I2C_SPEED); // Set I2C clock on 100 kHz } +*/ + +void setPCF8574PortPin(uint8_t pin, uint8_t value) +{ + if (pin > 7) return; + Wire.setClock(I2C_SPEED_LOW); + if (pcf8574_Modul1.begin()) + { + uint8_t port = pcf8574_Modul1.read8(); // Read all 8 bits + if (value == LOW) port &= ~(1 << pin); // Set bit + else port |= (1 << pin); + pcf8574_Modul1.write8(port); // Write byte + } + Wire.setClock(I2C_SPEED); // Set I2C clock on 100 kHz +} void setPortPin(uint pin, bool value){ @@ -437,6 +453,25 @@ void drawTextCenter(int16_t cx, int16_t cy, String text) { getdisplay().print(text); } +// Draw centered botton with centered text +void drawButtonCenter(int16_t cx, int16_t cy, int8_t sx, int8_t sy, String text, uint16_t fg, uint16_t bg, bool inverted) { + int16_t x1, y1; + uint16_t w, h; + uint16_t color; + getdisplay().getTextBounds(text, 0, 150, &x1, &y1, &w, &h); // Find text center + getdisplay().setCursor(cx - w / 2, cy + h / 2); // Set cursor to center + if (inverted) { + getdisplay().setTextColor(bg); + getdisplay().print(text); // Draw text + getdisplay().fillRoundRect(cx - sx / 2, cy + sy / 2, sx, sy, 5, fg); // Draw button + } + else{ + getdisplay().setTextColor(fg); + getdisplay().print(text); // Draw text + getdisplay().drawRoundRect(cx - sx / 2, cy + sy / 2, sx, sy, 5, fg); // Draw button + } +} + // Draw right aligned text void drawTextRalign(int16_t x, int16_t y, String text) { int16_t x1, y1; diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index 010bd35..33c2a14 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -89,7 +89,7 @@ uint8_t getLastPage(); void hardwareInit(GwApi *api); void powerInit(String powermode); -void setPCF8574PortPin(uint pin, uint8_t value);// Set PCF8574 port pin +void setPCF8574PortPin(uint8_t pin, uint8_t value);// Set PCF8574 port pin void setPortPin(uint pin, bool value); // Set port pin for extension port void togglePortPin(uint pin); // Toggle extension port pin @@ -108,6 +108,7 @@ void setBuzzerPower(uint power); // Set buzzer power String xdrDelete(String input); // Delete xdr prefix from string void drawTextCenter(int16_t cx, int16_t cy, String text); +void drawButtonCenter(int16_t cx, int16_t cy, int8_t sx, int8_t sy, String text, uint16_t fg, uint16_t bg, bool inverted); void drawTextRalign(int16_t x, int16_t y, String text); void drawTextBoxed(Rect box, String text, uint16_t fg, uint16_t bg, bool inverted, bool border); diff --git a/lib/obp60task/PageDigitalOut.cpp b/lib/obp60task/PageDigitalOut.cpp index dfe0a76..9988aed 100644 --- a/lib/obp60task/PageDigitalOut.cpp +++ b/lib/obp60task/PageDigitalOut.cpp @@ -22,12 +22,22 @@ bool button3 = false; bool button4 = false; bool button5 = false; -public: + public: PageDigitalOut(CommonData &common){ commonData = &common; common.logger->logDebug(GwLog::LOG,"Instantiate PageDigitalOut"); } + // Set botton labels + virtual void setupKeys(){ + Page::setupKeys(); + commonData->keydata[0].label = "BTN 1"; + commonData->keydata[1].label = "BTN 2"; + commonData->keydata[2].label = "BTN 3"; + commonData->keydata[3].label = "BTN 4"; + commonData->keydata[4].label = "BTN 5"; + } + virtual int handleKey(int key){ // Code for keylock if(key == 11){ @@ -94,18 +104,16 @@ public: getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update getdisplay().setTextColor(commonData->fgcolor); getdisplay().setFont(&Ubuntu_Bold12pt8b); + drawButtonCenter(50, 30, 40, 20, "1", commonData->fgcolor, commonData->bgcolor, button1); + drawButtonCenter(50, 30, 40, 20, "2", commonData->fgcolor, commonData->bgcolor, button2); + drawButtonCenter(50, 30, 40, 20, "3", commonData->fgcolor, commonData->bgcolor, button3); + drawButtonCenter(50, 30, 40, 20, "4", commonData->fgcolor, commonData->bgcolor, button4); + drawButtonCenter(50, 30, 40, 20, "5", commonData->fgcolor, commonData->bgcolor, button5); getdisplay().fillRoundRect(200, 250 , 200, 25, 5, commonData->fgcolor); // Black rect getdisplay().fillRoundRect(202, 252 , 196, 21, 5, commonData->bgcolor); // White rect getdisplay().setCursor(210, 270); getdisplay().print("Map server lost"); - // Set botton labels - commonData->keydata[0].label = "BTN 1"; - commonData->keydata[1].label = "BTN 2"; - commonData->keydata[2].label = "BTN 3"; - commonData->keydata[3].label = "BTN 4"; - commonData->keydata[4].label = "BTN 5"; - return PAGE_UPDATE; }; }; diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp index 7531685..8a3da06 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -19,20 +19,28 @@ bool firstRun = true; // Detect the first page run int zoom = 15; // Default zoom level bool showValues = false; // Show values HDT, SOG, DBT in navigation map -private: + private: uint8_t* imageBackupData = nullptr; int imageBackupWidth = 0; int imageBackupHeight = 0; size_t imageBackupSize = 0; bool hasImageBackup = false; -public: + public: PageNavigation(CommonData &common){ commonData = &common; common.logger->logDebug(GwLog::LOG,"Instantiate PageNavigation"); imageBackupData = (uint8_t*)heap_caps_malloc((GxEPD_WIDTH * GxEPD_HEIGHT), MALLOC_CAP_SPIRAM); } + // Set botton labels + virtual void setupKeys(){ + Page::setupKeys(); + commonData->keydata[0].label = "ZOOM -"; + commonData->keydata[1].label = "ZOOM +"; + commonData->keydata[4].label = "VALUES"; + } + virtual int handleKey(int key){ // Code for keylock if(key == 11){ @@ -475,12 +483,7 @@ public: getdisplay().setCursor(70, 85); getdisplay().print(svalue6); } - - // Set botton labels - commonData->keydata[0].label = "ZOOM -"; - commonData->keydata[1].label = "ZOOM +"; - commonData->keydata[4].label = "VALUES"; - + return PAGE_UPDATE; }; }; From 02c611ead0c312d68cc7ba53fc2b55b3b4ca5b02 Mon Sep 17 00:00:00 2001 From: TobiasE-github Date: Thu, 15 Jan 2026 17:56:52 +0100 Subject: [PATCH 092/121] use 9.95 as the limit for formatting numbers below and above 10 --- lib/obp60task/OBP60Formatter.cpp | 37 ++++++++++++++++---------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/lib/obp60task/OBP60Formatter.cpp b/lib/obp60task/OBP60Formatter.cpp index ebb2142..3424863 100644 --- a/lib/obp60task/OBP60Formatter.cpp +++ b/lib/obp60task/OBP60Formatter.cpp @@ -243,7 +243,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool speed = speed; // Unit conversion form m/s to m/s result.unit = "m/s"; } - if(speed < 10) { + if(speed < 9.95) { snprintf(buffer, bsize, fmt_dec_1, speed); } else if (speed < 100) { @@ -323,8 +323,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool snprintf(buffer, bsize, "%2.0f", speed); } else{ - speed = std::round(speed * 100) / 100; // in rare cases, speed could be 9.9999 kn instead of 10.0 kn - if (speed < 10.0){ + if (speed < 9.95){ snprintf(buffer, bsize, fmt_dec_1, speed); } else if (speed < 100.0){ @@ -378,7 +377,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool if (dop > 99.9){ dop = 99.9; } - if (dop < 10){ + if (dop < 9.95){ snprintf(buffer, bsize, fmt_dec_1, dop); } else if(dop < 100){ @@ -457,7 +456,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool else{ result.unit = "m"; } - if (depth < 10) { + if (depth < 9.95) { snprintf(buffer, bsize, fmt_dec_1, depth); } else if (depth < 100){ @@ -523,7 +522,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool else{ result.unit = "K"; } - if(temp < 10) { + if(temp < 9.95) { snprintf(buffer, bsize, fmt_dec_1, temp); } else if (temp < 100) { @@ -556,7 +555,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool else { result.unit = "m"; } - if (distance < 10){ + if (distance < 9.95){ snprintf(buffer, bsize, fmt_dec_1, distance); } else if (distance < 100){ @@ -613,7 +612,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool rawvalue = 12 + float(random(0, 30)) / 10.0; voltage = rawvalue; } - if (voltage < 10) { + if (voltage < 9.95) { snprintf(buffer, bsize, fmt_dec_1, voltage); } else { @@ -633,7 +632,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool rawvalue = 8.2 + float(random(0, 50)) / 10.0; current = rawvalue; } - if (current < 10) { + if (current < 9.95) { snprintf(buffer, bsize, fmt_dec_1, current); } else if(current < 100) { @@ -656,7 +655,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool rawvalue = 21.8 + float(random(0, 50)) / 10.0; temperature = rawvalue; } - if (temperature < 10) { + if (temperature < 9.95) { snprintf(buffer, bsize, fmt_dec_1, temperature); } else if (temperature < 100) { @@ -679,7 +678,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool rawvalue = 21.8 + float(random(0, 50)) / 10.0; temperature = rawvalue; } - if (temperature < 10) { + if (temperature < 9.95) { snprintf(buffer, bsize, fmt_dec_1, temperature); } else if(temperature < 100) { @@ -702,7 +701,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool rawvalue = 41.3 + float(random(0, 50)) / 10.0; humidity = rawvalue; } - if (humidity < 10) { + if (humidity < 9.95) { snprintf(buffer, bsize, fmt_dec_1, humidity); } else if(humidity < 100) { @@ -725,7 +724,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool rawvalue = 85.8 + float(random(0, 50)) / 10.0; volume = rawvalue; } - if (volume < 10) { + if (volume < 9.95) { snprintf(buffer, bsize, fmt_dec_1, volume); } else if (volume < 100) { @@ -748,7 +747,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool rawvalue = 75.2 + float(random(0, 50)) / 10.0; volume = rawvalue; } - if (volume < 10) { + if (volume < 9.95) { snprintf(buffer, bsize, fmt_dec_1, volume); } else if (volume < 100) { @@ -771,7 +770,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool rawvalue = 7.5 + float(random(0, 20)) / 10.0; flow = rawvalue; } - if (flow < 10) { + if (flow < 9.95) { snprintf(buffer, bsize, fmt_dec_1, flow); } else if (flow < 100) { @@ -794,7 +793,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool rawvalue = 18.5 + float(random(0, 20)) / 10.0; generic = rawvalue; } - if (generic < 10) { + if (generic < 9.95) { snprintf(buffer, bsize, fmt_dec_1, generic); } else if (generic < 100) { @@ -817,7 +816,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool rawvalue = 55.3 + float(random(0, 20)) / 10.0; dplace = rawvalue; } - if (dplace < 10) { + if (dplace < 9.95) { snprintf(buffer, bsize, fmt_dec_1, dplace); } else if (dplace < 100) { @@ -861,7 +860,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool rawvalue = 2505 + random(0, 20); rpm = rawvalue; } - if (rpm < 10) { + if (rpm < 9.95) { snprintf(buffer, bsize, fmt_dec_1, rpm); } else if (rpm < 100) { @@ -877,7 +876,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool // Default format //######################################################## else { - if (value->value < 10) { + if (value->value < 9.95) { snprintf(buffer, bsize, fmt_dec_1, value->value); } else if (value->value < 100) { From e4214beefe3f949e863024df265354b598e08b6a Mon Sep 17 00:00:00 2001 From: TobiasE-github Date: Sat, 17 Jan 2026 10:57:07 +0100 Subject: [PATCH 093/121] general but flexible limits for formatting numbers --- lib/obp60task/OBP60Formatter.cpp | 78 +++++++++++++++++--------------- 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/lib/obp60task/OBP60Formatter.cpp b/lib/obp60task/OBP60Formatter.cpp index 3424863..9b42102 100644 --- a/lib/obp60task/OBP60Formatter.cpp +++ b/lib/obp60task/OBP60Formatter.cpp @@ -92,6 +92,8 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool const char* fmt_dec_1; const char* fmt_dec_10; const char* fmt_dec_100; + double limit_dec_10; + double limit_dec_100; if (precision == "1") { // //All values are displayed using a DSEG7* font. In this font, ' ' is a very short space, and '.' takes up no space at all. @@ -100,10 +102,14 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool fmt_dec_1 = "!%1.1f"; //insert a blank digit and then display a two-digit number fmt_dec_10 = "!%2.0f"; //insert a blank digit and then display a two-digit number fmt_dec_100 = "%3.0f"; //dispay a three digit number + limit_dec_10=9.95; // use fmt_dec_1 below this number to avoid formatting 9.96 as 10.0 instead of 10 + limit_dec_100=99.5; } else { fmt_dec_1 = "%3.2f"; fmt_dec_10 = "%3.1f"; fmt_dec_100 = "%3.0f"; + limit_dec_10=9.995; + limit_dec_100=99.95; } // LOG_DEBUG(GwLog::DEBUG,"formatValue init: getFormat: %s date->value: %f time->value: %f", value->getFormat(), commondata.date->value, commondata.time->value); @@ -243,10 +249,10 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool speed = speed; // Unit conversion form m/s to m/s result.unit = "m/s"; } - if(speed < 9.95) { + if(speed < limit_dec_10) { snprintf(buffer, bsize, fmt_dec_1, speed); } - else if (speed < 100) { + else if (speed < limit_dec_100) { snprintf(buffer, bsize, fmt_dec_10, speed); } else { @@ -323,10 +329,10 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool snprintf(buffer, bsize, "%2.0f", speed); } else{ - if (speed < 9.95){ + if (speed < limit_dec_10){ snprintf(buffer, bsize, fmt_dec_1, speed); } - else if (speed < 100.0){ + else if (speed < limit_dec_100){ snprintf(buffer, bsize, fmt_dec_10, speed); } else { @@ -377,10 +383,10 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool if (dop > 99.9){ dop = 99.9; } - if (dop < 9.95){ + if (dop < limit_dec_10){ snprintf(buffer, bsize, fmt_dec_1, dop); } - else if(dop < 100){ + else if(dop < limit_dec_100){ snprintf(buffer, bsize, fmt_dec_10, dop); } else { @@ -456,10 +462,10 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool else{ result.unit = "m"; } - if (depth < 9.95) { + if (depth < limit_dec_10) { snprintf(buffer, bsize, fmt_dec_1, depth); } - else if (depth < 100){ + else if (depth < limit_dec_100){ snprintf(buffer, bsize, fmt_dec_10, depth); } else { @@ -522,10 +528,10 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool else{ result.unit = "K"; } - if(temp < 9.95) { + if(temp < limit_dec_10) { snprintf(buffer, bsize, fmt_dec_1, temp); } - else if (temp < 100) { + else if (temp < limit_dec_100) { snprintf(buffer, bsize, fmt_dec_10, temp); } else { @@ -555,10 +561,10 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool else { result.unit = "m"; } - if (distance < 9.95){ + if (distance < limit_dec_10){ snprintf(buffer, bsize, fmt_dec_1, distance); } - else if (distance < 100){ + else if (distance < limit_dec_100){ snprintf(buffer, bsize, fmt_dec_10, distance); } else { @@ -612,7 +618,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool rawvalue = 12 + float(random(0, 30)) / 10.0; voltage = rawvalue; } - if (voltage < 9.95) { + if (voltage < limit_dec_10) { snprintf(buffer, bsize, fmt_dec_1, voltage); } else { @@ -632,10 +638,10 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool rawvalue = 8.2 + float(random(0, 50)) / 10.0; current = rawvalue; } - if (current < 9.95) { + if (current < limit_dec_10) { snprintf(buffer, bsize, fmt_dec_1, current); } - else if(current < 100) { + else if(current < limit_dec_100) { snprintf(buffer, bsize, fmt_dec_10, current); } else { @@ -655,10 +661,10 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool rawvalue = 21.8 + float(random(0, 50)) / 10.0; temperature = rawvalue; } - if (temperature < 9.95) { + if (temperature < limit_dec_10) { snprintf(buffer, bsize, fmt_dec_1, temperature); } - else if (temperature < 100) { + else if (temperature < limit_dec_100) { snprintf(buffer, bsize, fmt_dec_10, temperature); } else { @@ -678,10 +684,10 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool rawvalue = 21.8 + float(random(0, 50)) / 10.0; temperature = rawvalue; } - if (temperature < 9.95) { + if (temperature < limit_dec_10) { snprintf(buffer, bsize, fmt_dec_1, temperature); } - else if(temperature < 100) { + else if(temperature < limit_dec_100) { snprintf(buffer, bsize, fmt_dec_10, temperature); } else { @@ -701,10 +707,10 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool rawvalue = 41.3 + float(random(0, 50)) / 10.0; humidity = rawvalue; } - if (humidity < 9.95) { + if (humidity < limit_dec_10) { snprintf(buffer, bsize, fmt_dec_1, humidity); } - else if(humidity < 100) { + else if(humidity < limit_dec_100) { snprintf(buffer, bsize, fmt_dec_10, humidity); } else { @@ -724,13 +730,13 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool rawvalue = 85.8 + float(random(0, 50)) / 10.0; volume = rawvalue; } - if (volume < 9.95) { + if (volume < limit_dec_10) { snprintf(buffer, bsize, fmt_dec_1, volume); } - else if (volume < 100) { + else if (volume < limit_dec_100) { snprintf(buffer, bsize, fmt_dec_10, volume); } - else if (volume >= 100) { + else if (volume >= limit_dec_100) { snprintf(buffer, bsize, fmt_dec_100, volume); } result.unit = "%"; @@ -747,10 +753,10 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool rawvalue = 75.2 + float(random(0, 50)) / 10.0; volume = rawvalue; } - if (volume < 9.95) { + if (volume < limit_dec_10) { snprintf(buffer, bsize, fmt_dec_1, volume); } - else if (volume < 100) { + else if (volume < limit_dec_100) { snprintf(buffer, bsize, fmt_dec_10, volume); } else { @@ -770,10 +776,10 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool rawvalue = 7.5 + float(random(0, 20)) / 10.0; flow = rawvalue; } - if (flow < 9.95) { + if (flow < limit_dec_10) { snprintf(buffer, bsize, fmt_dec_1, flow); } - else if (flow < 100) { + else if (flow < limit_dec_100) { snprintf(buffer, bsize, fmt_dec_10, flow); } else { @@ -793,10 +799,10 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool rawvalue = 18.5 + float(random(0, 20)) / 10.0; generic = rawvalue; } - if (generic < 9.95) { + if (generic < limit_dec_10) { snprintf(buffer, bsize, fmt_dec_1, generic); } - else if (generic < 100) { + else if (generic < limit_dec_100) { snprintf(buffer, bsize, fmt_dec_10, generic); } else { @@ -816,10 +822,10 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool rawvalue = 55.3 + float(random(0, 20)) / 10.0; dplace = rawvalue; } - if (dplace < 9.95) { + if (dplace < limit_dec_10) { snprintf(buffer, bsize, fmt_dec_1, dplace); } - else if (dplace < 100) { + else if (dplace < limit_dec_100) { snprintf(buffer, bsize, fmt_dec_10, dplace); } else { @@ -860,10 +866,10 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool rawvalue = 2505 + random(0, 20); rpm = rawvalue; } - if (rpm < 9.95) { + if (rpm < limit_dec_10) { snprintf(buffer, bsize, fmt_dec_1, rpm); } - else if (rpm < 100) { + else if (rpm < limit_dec_100) { snprintf(buffer, bsize, fmt_dec_10, rpm); } else { @@ -876,10 +882,10 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool // Default format //######################################################## else { - if (value->value < 9.95) { + if (value->value < limit_dec_10) { snprintf(buffer, bsize, fmt_dec_1, value->value); } - else if (value->value < 100) { + else if (value->value < limit_dec_100) { snprintf(buffer, bsize, fmt_dec_10, value->value); } else { From b8e64ff64c6aa24760c5f1429efb7c46da7f24b0 Mon Sep 17 00:00:00 2001 From: Ulrich Meine Date: Sat, 17 Jan 2026 12:25:57 +0100 Subject: [PATCH 094/121] Update data calibration: calibration in main loop on boat data values: - modified code to calibrate affected boat data each second in main obp60task loop; - removed individual calibration call in all pages - moved code from BoatDataCalibration to OBPDataOperations - added DBS and HDT to list of supported data calibration types - changed output format for wind angles from [-180..180] to [0..360], because negative value are not displayed properly on pages --- lib/obp60task/BoatDataCalibration.cpp | 190 --------------------- lib/obp60task/BoatDataCalibration.h | 34 ---- lib/obp60task/OBPDataOperations.cpp | 229 +++++++++++++++++++++++++- lib/obp60task/OBPDataOperations.h | 29 +++- lib/obp60task/PageFourValues.cpp | 5 - lib/obp60task/PageFourValues2.cpp | 5 - lib/obp60task/PageOneValue.cpp | 2 - lib/obp60task/PageRudderPosition.cpp | 2 - lib/obp60task/PageSixValues.cpp | 2 - lib/obp60task/PageThreeValues.cpp | 4 - lib/obp60task/PageTwoValues.cpp | 2 - lib/obp60task/PageWind.cpp | 3 - lib/obp60task/PageWindRose.cpp | 7 - lib/obp60task/PageWindRoseFlex.cpp | 7 - lib/obp60task/config.json | 30 ++-- lib/obp60task/config_obp40.json | 30 ++-- lib/obp60task/obp60task.cpp | 28 ++-- 17 files changed, 300 insertions(+), 309 deletions(-) delete mode 100644 lib/obp60task/BoatDataCalibration.cpp delete mode 100644 lib/obp60task/BoatDataCalibration.h diff --git a/lib/obp60task/BoatDataCalibration.cpp b/lib/obp60task/BoatDataCalibration.cpp deleted file mode 100644 index 9543926..0000000 --- a/lib/obp60task/BoatDataCalibration.cpp +++ /dev/null @@ -1,190 +0,0 @@ -#if defined BOARD_OBP60S3 || defined BOARD_OBP40S3 - -#include "BoatDataCalibration.h" -#include -#include -#include - -CalibrationDataList calibrationData; -std::unordered_map CalibrationDataList::calibMap; // list of calibration data instances - -void CalibrationDataList::readConfig(GwConfigHandler* config, GwLog* logger) -// Initial load of calibration data into internal list -// This method is called once at init phase of to read the configuration values -{ - std::string instance; - double offset; - double slope; - double smooth; - - String calInstance = ""; - String calOffset = ""; - String calSlope = ""; - String calSmooth = ""; - - // Load user format configuration values - String lengthFormat = config->getString(config->lengthFormat); // [m|ft] - String distanceFormat = config->getString(config->distanceFormat); // [m|km|nm] - String speedFormat = config->getString(config->speedFormat); // [m/s|km/h|kn] - String windspeedFormat = config->getString(config->windspeedFormat); // [m/s|km/h|kn|bft] - String tempFormat = config->getString(config->tempFormat); // [K|C|F] - - // Read calibration settings for data instances - for (int i = 0; i < MAX_CALIBRATION_DATA; i++) { - calInstance = "calInstance" + String(i + 1); - calOffset = "calOffset" + String(i + 1); - calSlope = "calSlope" + String(i + 1); - calSmooth = "calSmooth" + String(i + 1); - - instance = std::string(config->getString(calInstance, "---").c_str()); - if (instance == "---") { - LOG_DEBUG(GwLog::LOG, "no calibration data for instance no. %d", i + 1); - continue; - } - calibMap[instance] = { 0.0f, 1.0f, 1.0f, 0.0f, false }; - offset = (config->getString(calOffset, "")).toFloat(); - slope = (config->getString(calSlope, "")).toFloat(); - smooth = (config->getString(calSmooth, "")).toInt(); // user input is int; further math is done with double - - // Convert calibration values to internal standard formats - if (instance == "AWS" || instance == "TWS") { - if (windspeedFormat == "m/s") { - // No conversion needed - } else if (windspeedFormat == "km/h") { - offset /= 3.6; // Convert km/h to m/s - } else if (windspeedFormat == "kn") { - offset /= 1.94384; // Convert kn to m/s - } else if (windspeedFormat == "bft") { - offset *= 2 + (offset / 2); // Convert Bft to m/s (approx) -> to be improved - } - - } else if (instance == "AWA" || instance == "COG" || instance == "TWA" || instance == "TWD" || instance == "HDM" || instance == "PRPOS" || instance == "RPOS") { - offset *= M_PI / 180; // Convert deg to rad - - } else if (instance == "DBT") { - if (lengthFormat == "m") { - // No conversion needed - } else if (lengthFormat == "ft") { - offset /= 3.28084; // Convert ft to m - } - - } else if (instance == "SOG" || instance == "STW") { - if (speedFormat == "m/s") { - // No conversion needed - } else if (speedFormat == "km/h") { - offset /= 3.6; // Convert km/h to m/s - } else if (speedFormat == "kn") { - offset /= 1.94384; // Convert kn to m/s - } - - } else if (instance == "WTemp") { - if (tempFormat == "K" || tempFormat == "C") { - // No conversion needed - } else if (tempFormat == "F") { - offset *= 9.0 / 5.0; // Convert °F to K - slope *= 9.0 / 5.0; // Convert °F to K - } - } - - // transform smoothing factor from {0.01..10} to {0.3..0.95} and invert for exponential smoothing formula - if (smooth <= 0) { - smooth = 0; - } else { - if (smooth > 10) { - smooth = 10; - } - smooth = 0.3 + ((smooth - 0.01) * (0.95 - 0.3) / (10 - 0.01)); - } - smooth = 1 - smooth; - - calibMap[instance].offset = offset; - calibMap[instance].slope = slope; - calibMap[instance].smooth = smooth; - calibMap[instance].isCalibrated = false; - LOG_DEBUG(GwLog::LOG, "calibration data: %s, offset: %f, slope: %f, smoothing: %f", instance.c_str(), - calibMap[instance].offset, calibMap[instance].slope, calibMap[instance].smooth); - } - LOG_DEBUG(GwLog::LOG, "all calibration data read"); -} - -void CalibrationDataList::calibrateInstance(GwApi::BoatValue* boatDataValue, GwLog* logger) -// Method to calibrate the boat data value -{ - std::string instance = boatDataValue->getName().c_str(); - double offset = 0; - double slope = 1.0; - double dataValue = 0; - std::string format = ""; - - if (calibMap.find(instance) == calibMap.end()) { - LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s not in calibration list", instance.c_str()); - return; - } else if (!boatDataValue->valid) { // no valid boat data value, so we don't want to apply calibration data - calibMap[instance].isCalibrated = false; - return; - } else { - offset = calibMap[instance].offset; - slope = calibMap[instance].slope; - dataValue = boatDataValue->value; - format = boatDataValue->getFormat().c_str(); - LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s: value: %f, format: %s", instance.c_str(), dataValue, format.c_str()); - - if (format == "formatWind") { // instance is of type angle - dataValue = (dataValue * slope) + offset; - dataValue = fmod(dataValue, 2 * M_PI); - if (dataValue > (M_PI)) { - dataValue -= (2 * M_PI); - } else if (dataValue < (M_PI * -1)) { - dataValue += (2 * M_PI); - } - } else if (format == "formatCourse") { // instance is of type direction - dataValue = (dataValue * slope) + offset; - dataValue = fmod(dataValue, 2 * M_PI); - if (dataValue < 0) { - dataValue += (2 * M_PI); - } - } else if (format == "kelvinToC") { // instance is of type temperature - dataValue = ((dataValue - 273.15) * slope) + offset + 273.15; - } else { - - dataValue = (dataValue * slope) + offset; - } - - calibMap[instance].isCalibrated = true; - boatDataValue->value = dataValue; - - calibrationData.smoothInstance(boatDataValue, logger); // smooth the boat data value - calibMap[instance].value = boatDataValue->value; // store the calibrated + smoothed value in the list - - LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s: Offset: %f, Slope: %f, Result: %f", instance.c_str(), offset, slope, calibMap[instance].value); - } -} - -void CalibrationDataList::smoothInstance(GwApi::BoatValue* boatDataValue, GwLog* logger) -// Method to smoothen the boat data value -{ - static std::unordered_map lastValue; // array for last values of smoothed boat data values - - std::string instance = boatDataValue->getName().c_str(); - double oldValue = 0; - double dataValue = boatDataValue->value; - double smoothFactor = 0; - - if (!boatDataValue->valid) { // no valid boat data value, so we don't want to smoothen value - return; - } else if (calibMap.find(instance) == calibMap.end()) { - LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: smooth factor for %s not found in calibration list", instance.c_str()); - return; - } else { - smoothFactor = calibMap[instance].smooth; - - if (lastValue.find(instance) != lastValue.end()) { - oldValue = lastValue[instance]; - dataValue = oldValue + (smoothFactor * (dataValue - oldValue)); // exponential smoothing algorithm - } - lastValue[instance] = dataValue; // store the new value for next cycle; first time, store only the current value and return - boatDataValue->value = dataValue; // set the smoothed value to the boat data value - } -} - -#endif \ No newline at end of file diff --git a/lib/obp60task/BoatDataCalibration.h b/lib/obp60task/BoatDataCalibration.h deleted file mode 100644 index d906fa9..0000000 --- a/lib/obp60task/BoatDataCalibration.h +++ /dev/null @@ -1,34 +0,0 @@ -// Functions lib for data instance calibration - -#ifndef _BOATDATACALIBRATION_H -#define _BOATDATACALIBRATION_H - -// #include "Pagedata.h" -#include "GwApi.h" -#include -#include - -#define MAX_CALIBRATION_DATA 3 // maximum number of calibration data instances - -typedef struct { - double offset; // calibration offset - double slope; // calibration slope - double smooth; // smoothing factor - double value; // calibrated data value - bool isCalibrated; // is data instance value calibrated? -} TypeCalibData; - -class CalibrationDataList { -public: - static std::unordered_map calibMap; // list of calibration data instances - - void readConfig(GwConfigHandler* config, GwLog* logger); - void calibrateInstance(GwApi::BoatValue* boatDataValue, GwLog* logger); - void smoothInstance(GwApi::BoatValue* boatDataValue, GwLog* logger); - -private: -}; - -extern CalibrationDataList calibrationData; // this list holds all calibration data - -#endif \ No newline at end of file diff --git a/lib/obp60task/OBPDataOperations.cpp b/lib/obp60task/OBPDataOperations.cpp index aae0040..1adb7f4 100644 --- a/lib/obp60task/OBPDataOperations.cpp +++ b/lib/obp60task/OBPDataOperations.cpp @@ -1,5 +1,221 @@ #include "OBPDataOperations.h" -#include "BoatDataCalibration.h" // Functions lib for data instance calibration +//#include "BoatDataCalibration.h" // Functions lib for data instance calibration + +// --- Class CalibrationData --------------- +CalibrationData::CalibrationData(GwLog* log) +{ + logger = log; +} + +void CalibrationData::readConfig(GwConfigHandler* config) +// Initial load of calibration data into internal list +// This method is called once at init phase of to read the configuration values +{ + std::string instance; + double offset; + double slope; + double smooth; + + String calInstance = ""; + String calOffset = ""; + String calSlope = ""; + String calSmooth = ""; + + // Load user format configuration values + String lengthFormat = config->getString(config->lengthFormat); // [m|ft] + String distanceFormat = config->getString(config->distanceFormat); // [m|km|nm] + String speedFormat = config->getString(config->speedFormat); // [m/s|km/h|kn] + String windspeedFormat = config->getString(config->windspeedFormat); // [m/s|km/h|kn|bft] + String tempFormat = config->getString(config->tempFormat); // [K|C|F] + + // Read calibration settings for data instances + for (int i = 0; i < MAX_CALIBRATION_DATA; i++) { + calInstance = "calInstance" + String(i + 1); + calOffset = "calOffset" + String(i + 1); + calSlope = "calSlope" + String(i + 1); + calSmooth = "calSmooth" + String(i + 1); + + instance = std::string(config->getString(calInstance, "---").c_str()); + if (instance == "---") { + LOG_DEBUG(GwLog::LOG, "No calibration data for instance no. %d", i + 1); + continue; + } + + calibrationMap[instance] = { 0.0f, 1.0f, 1.0f, 0.0f, false }; + offset = (config->getString(calOffset, "")).toDouble(); + slope = (config->getString(calSlope, "")).toDouble(); + smooth = (config->getString(calSmooth, "")).toInt(); // user input is int; further math is done with double + + if (slope == 0.0) { + slope = 1.0; // eliminate adjustment if user selected "0" -> that would set the calibrated value to "0" + } + + // Convert calibration values from user input format to internal standard SI format + if (instance == "AWS" || instance == "TWS") { + if (windspeedFormat == "m/s") { + // No conversion needed + } else if (windspeedFormat == "km/h") { + offset /= 3.6; // Convert km/h to m/s + } else if (windspeedFormat == "kn") { + offset /= 1.94384; // Convert kn to m/s + } else if (windspeedFormat == "bft") { + offset *= 2 + (offset / 2); // Convert Bft to m/s (approx) -> to be improved + } + + } else if (instance == "AWA" || instance == "COG" || instance == "HDM" || instance == "HDT" || instance == "PRPOS" || instance == "RPOS" || instance == "TWA" || instance == "TWD") { + offset *= DEG_TO_RAD; // Convert deg to rad + + } else if (instance == "DBS" || instance == "DBT") { + if (lengthFormat == "m") { + // No conversion needed + } else if (lengthFormat == "ft") { + offset /= 3.28084; // Convert ft to m + } + + } else if (instance == "SOG" || instance == "STW") { + if (speedFormat == "m/s") { + // No conversion needed + } else if (speedFormat == "km/h") { + offset /= 3.6; // Convert km/h to m/s + } else if (speedFormat == "kn") { + offset /= 1.94384; // Convert kn to m/s + } + + } else if (instance == "WTemp") { + if (tempFormat == "K" || tempFormat == "C") { + // No conversion needed + } else if (tempFormat == "F") { + offset *= 9.0 / 5.0; // Convert °F to K + slope *= 9.0 / 5.0; // Convert °F to K + } + } + + // transform smoothing factor from [0.01..10] to [0.3..0.95] and invert for exponential smoothing formula + if (smooth <= 0) { + smooth = 0; + } else { + if (smooth > 10) { + smooth = 10; + } + smooth = 0.3 + ((smooth - 0.01) * (0.95 - 0.3) / (10 - 0.01)); + } + smooth = 1 - smooth; + + calibrationMap[instance].offset = offset; + calibrationMap[instance].slope = slope; + calibrationMap[instance].smooth = smooth; + calibrationMap[instance].isCalibrated = false; + LOG_DEBUG(GwLog::LOG, "Calibration data type added: %s, offset: %f, slope: %f, smoothing: %f", instance.c_str(), + calibrationMap[instance].offset, calibrationMap[instance].slope, calibrationMap[instance].smooth); + } + // LOG_DEBUG(GwLog::LOG, "All calibration data read"); +} + +// Handle calibrationMap and calibrate all boat data values +void CalibrationData::handleCalibration(BoatValueList* boatValueList) +{ + GwApi::BoatValue* bValue; + + for (const auto& cMap : calibrationMap) { + std::string instance = cMap.first.c_str(); + bValue = boatValueList->findValueOrCreate(String(instance.c_str())); + + calibrateInstance(bValue); + smoothInstance(bValue); + } +} + +// Calibrate single boat data value +// Return calibrated boat value or DBL_MAX, if no calibration was performed +bool CalibrationData::calibrateInstance(GwApi::BoatValue* boatDataValue) +{ + std::string instance = boatDataValue->getName().c_str(); + double offset = 0; + double slope = 1.0; + double dataValue = 0; + std::string format = ""; + + // we test this earlier, but for safety reason ... + if (calibrationMap.find(instance) == calibrationMap.end()) { + LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s not in calibration list", instance.c_str()); + return false; + } + + calibrationMap[instance].isCalibrated = false; // reset calibration flag until properly calibrated + + if (!boatDataValue->valid) { // no valid boat data value, so we don't want to apply calibration data + return false; + } + + offset = calibrationMap[instance].offset; + slope = calibrationMap[instance].slope; + dataValue = boatDataValue->value; + format = boatDataValue->getFormat().c_str(); + LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s: value: %f, format: %s", instance.c_str(), dataValue, format.c_str()); + + if (format == "formatWind") { // instance is of type angle + dataValue = (dataValue * slope) + offset; + // dataValue = WindUtils::toPI(dataValue); + dataValue = WindUtils::to2PI(dataValue); // we should call for format of [-180..180], but pages cannot handle negative values yet + + } else if (format == "formatCourse") { // instance is of type direction + dataValue = (dataValue * slope) + offset; + dataValue = WindUtils::to2PI(dataValue); + + } else if (format == "kelvinToC") { // instance is of type temperature + dataValue = ((dataValue - 273.15) * slope) + offset + 273.15; + + } else { + dataValue = (dataValue * slope) + offset; + } + + + boatDataValue->value = dataValue; // update boat data value with calibrated value + calibrationMap[instance].value = dataValue; // store the calibrated value in the list + calibrationMap[instance].isCalibrated = true; + + LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s: Offset: %f, Slope: %f, Result: %f", instance.c_str(), offset, slope, calibrationMap[instance].value); + return true; +} + +// Smooth single boat data value +// Return smoothed boat value or DBL_MAX, if no smoothing was performed +bool CalibrationData::smoothInstance(GwApi::BoatValue* boatDataValue) +{ + std::string instance = boatDataValue->getName().c_str(); + double oldValue = 0; + double dataValue = boatDataValue->value; + double smoothFactor = 0; + + // we test this earlier, but for safety reason ... + if (calibrationMap.find(instance) == calibrationMap.end()) { + LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s not in calibration list", instance.c_str()); + return false; + } + + calibrationMap[instance].isCalibrated = false; // reset calibration flag until properly calibrated + + if (!boatDataValue->valid) { // no valid boat data value, so we don't need to do anything + return false; + } + + smoothFactor = calibrationMap[instance].smooth; + + if (lastValue.find(instance) != lastValue.end()) { + oldValue = lastValue[instance]; + dataValue = oldValue + (smoothFactor * (dataValue - oldValue)); // exponential smoothing algorithm + } + lastValue[instance] = dataValue; // store the new value for next cycle; first time, store only the current value and return + + boatDataValue->value = dataValue; // update boat data value with smoothed value + calibrationMap[instance].value = dataValue; // store the smoothed value in the list + calibrationMap[instance].isCalibrated = true; + + LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s: smooth: %f, oldValue: %f, result: %f", instance.c_str(), smoothFactor, oldValue, calibrationMap[instance].value); + + return true; +} +// --- End Class CalibrationData --------------- // --- Class HstryBuf --------------- HstryBuf::HstryBuf(const String& name, int size, BoatValueList* boatValues, GwLog* log) @@ -43,12 +259,15 @@ void HstryBuf::handle(bool useSimuData, CommonData& common) if (boatValue->valid) { // Calibrate boat value before adding it to history buffer - calibrationData.calibrateInstance(tmpBVal.get(), logger); - add(tmpBVal->value); + //calibrationData.calibrateInstance(tmpBVal.get(), logger); + //add(tmpBVal->value); + add(boatValue->value); } else if (useSimuData) { // add simulated value to history buffer - double simValue = formatValue(tmpBVal.get(), common).value; // simulated value is generated at - add(simValue); + double simSIValue = formatValue(tmpBVal.get(), common).value; // simulated value is generated at ; here: retreive SI value + add(simSIValue); + } else { + // here we will add invalid (DBL_MAX) value; this will mark periods of missing data in buffer together with a timestamp } } // --- End Class HstryBuf --------------- diff --git a/lib/obp60task/OBPDataOperations.h b/lib/obp60task/OBPDataOperations.h index 0fb8647..e42e293 100644 --- a/lib/obp60task/OBPDataOperations.h +++ b/lib/obp60task/OBPDataOperations.h @@ -1,9 +1,36 @@ -// Function lib for history buffer handling, true wind calculation, and other operations on boat data +// Function lib for boat data calibration, history buffer handling, true wind calculation, and other operations on boat data #pragma once #include "OBPRingBuffer.h" #include "Pagedata.h" #include "obp60task.h" #include +#include + +// Calibration of boat data values, when user setting available +// supported boat data types are: AWA, AWS, COG, DBS, DBT, HDM, HDT, PRPOS, RPOS, SOG, STW, TWA, TWS, TWD, WTemp +class CalibrationData { +private: + typedef struct { + double offset; // calibration offset + double slope; // calibration slope + double smooth; // smoothing factor + double value; // calibrated data value (for future use) + bool isCalibrated; // is data instance value calibrated? (for future use) + } tCalibrationData; + + std::unordered_map calibrationMap; // list of calibration data instances + std::unordered_map lastValue; // array for last smoothed values of boat data values + GwLog* logger; + + static constexpr int8_t MAX_CALIBRATION_DATA = 3; // maximum number of calibration data instances + +public: + CalibrationData(GwLog* log); + void readConfig(GwConfigHandler* config); + void handleCalibration(BoatValueList* boatValues); // Handle calibrationMap and calibrate all boat data values + bool calibrateInstance(GwApi::BoatValue* boatDataValue); // Calibrate single boat data value + bool smoothInstance(GwApi::BoatValue* boatDataValue); // Smooth single boat data value +}; class HstryBuf { private: diff --git a/lib/obp60task/PageFourValues.cpp b/lib/obp60task/PageFourValues.cpp index b7526c0..cb9de68 100644 --- a/lib/obp60task/PageFourValues.cpp +++ b/lib/obp60task/PageFourValues.cpp @@ -2,7 +2,6 @@ #include "Pagedata.h" #include "OBP60Extensions.h" -#include "BoatDataCalibration.h" class PageFourValues : public Page { @@ -46,7 +45,6 @@ class PageFourValues : public Page GwApi::BoatValue *bvalue1 = pageData.values[0]; // First element in list (only one value by PageOneValue) String name1 = xdrDelete(bvalue1->getName()); // Value name name1 = name1.substring(0, 6); // String length limit for value name - calibrationData.calibrateInstance(bvalue1, logger); // Check if boat data value is to be calibrated double value1 = bvalue1->value; // Value as double in SI unit bool valid1 = bvalue1->valid; // Valid information String svalue1 = formatValue(bvalue1, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places @@ -56,7 +54,6 @@ class PageFourValues : public Page GwApi::BoatValue *bvalue2 = pageData.values[1]; // Second element in list String name2 = xdrDelete(bvalue2->getName()); // Value name name2 = name2.substring(0, 6); // String length limit for value name - calibrationData.calibrateInstance(bvalue2, logger); // Check if boat data value is to be calibrated double value2 = bvalue2->value; // Value as double in SI unit bool valid2 = bvalue2->valid; // Valid information String svalue2 = formatValue(bvalue2, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places @@ -66,7 +63,6 @@ class PageFourValues : public Page GwApi::BoatValue *bvalue3 = pageData.values[2]; // Third element in list String name3 = xdrDelete(bvalue3->getName()); // Value name name3 = name3.substring(0, 6); // String length limit for value name - calibrationData.calibrateInstance(bvalue3, logger); // Check if boat data value is to be calibrated double value3 = bvalue3->value; // Value as double in SI unit bool valid3 = bvalue3->valid; // Valid information String svalue3 = formatValue(bvalue3, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places @@ -76,7 +72,6 @@ class PageFourValues : public Page GwApi::BoatValue *bvalue4 = pageData.values[3]; // Fourth element in list String name4 = xdrDelete(bvalue4->getName()); // Value name name4 = name4.substring(0, 6); // String length limit for value name - calibrationData.calibrateInstance(bvalue4, logger); // Check if boat data value is to be calibrated double value4 = bvalue4->value; // Value as double in SI unit bool valid4 = bvalue4->valid; // Valid information String svalue4 = formatValue(bvalue4, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places diff --git a/lib/obp60task/PageFourValues2.cpp b/lib/obp60task/PageFourValues2.cpp index e608409..730e14b 100644 --- a/lib/obp60task/PageFourValues2.cpp +++ b/lib/obp60task/PageFourValues2.cpp @@ -2,7 +2,6 @@ #include "Pagedata.h" #include "OBP60Extensions.h" -#include "BoatDataCalibration.h" class PageFourValues2 : public Page { @@ -46,7 +45,6 @@ class PageFourValues2 : public Page GwApi::BoatValue *bvalue1 = pageData.values[0]; // First element in list (only one value by PageOneValue) String name1 = xdrDelete(bvalue1->getName()); // Value name name1 = name1.substring(0, 6); // String length limit for value name - calibrationData.calibrateInstance(bvalue1, logger); // Check if boat data value is to be calibrated double value1 = bvalue1->value; // Value as double in SI unit bool valid1 = bvalue1->valid; // Valid information String svalue1 = formatValue(bvalue1, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places @@ -56,7 +54,6 @@ class PageFourValues2 : public Page GwApi::BoatValue *bvalue2 = pageData.values[1]; // Second element in list (only one value by PageOneValue) String name2 = xdrDelete(bvalue2->getName()); // Value name name2 = name2.substring(0, 6); // String length limit for value name - calibrationData.calibrateInstance(bvalue2, logger); // Check if boat data value is to be calibrated double value2 = bvalue2->value; // Value as double in SI unit bool valid2 = bvalue2->valid; // Valid information String svalue2 = formatValue(bvalue2, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places @@ -66,7 +63,6 @@ class PageFourValues2 : public Page GwApi::BoatValue *bvalue3 = pageData.values[2]; // Second element in list (only one value by PageOneValue) String name3 = xdrDelete(bvalue3->getName()); // Value name name3 = name3.substring(0, 6); // String length limit for value name - calibrationData.calibrateInstance(bvalue3, logger); // Check if boat data value is to be calibrated double value3 = bvalue3->value; // Value as double in SI unit bool valid3 = bvalue3->valid; // Valid information String svalue3 = formatValue(bvalue3, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places @@ -76,7 +72,6 @@ class PageFourValues2 : public Page GwApi::BoatValue *bvalue4 = pageData.values[3]; // Second element in list (only one value by PageOneValue) String name4 = xdrDelete(bvalue4->getName()); // Value name name4 = name4.substring(0, 6); // String length limit for value name - calibrationData.calibrateInstance(bvalue4, logger); // Check if boat data value is to be calibrated double value4 = bvalue4->value; // Value as double in SI unit bool valid4 = bvalue4->valid; // Valid information String svalue4 = formatValue(bvalue4, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places diff --git a/lib/obp60task/PageOneValue.cpp b/lib/obp60task/PageOneValue.cpp index 6349f29..a5c0f9c 100644 --- a/lib/obp60task/PageOneValue.cpp +++ b/lib/obp60task/PageOneValue.cpp @@ -2,7 +2,6 @@ #include "Pagedata.h" #include "OBP60Extensions.h" -#include "BoatDataCalibration.h" #include "OBPDataOperations.h" #include "OBPcharts.h" @@ -88,7 +87,6 @@ private: String name1 = xdrDelete(bValue1->getName()); // Value name name1 = name1.substring(0, 6); // String length limit for value name - calibrationData.calibrateInstance(bValue1, logger); // Check if boat data value is to be calibrated double value1 = bValue1->value; // Value as double in SI unit bool valid1 = bValue1->valid; // Valid information String sValue1 = formatValue(bValue1, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places diff --git a/lib/obp60task/PageRudderPosition.cpp b/lib/obp60task/PageRudderPosition.cpp index 22b5f91..6a8695f 100644 --- a/lib/obp60task/PageRudderPosition.cpp +++ b/lib/obp60task/PageRudderPosition.cpp @@ -2,7 +2,6 @@ #include "Pagedata.h" #include "OBP60Extensions.h" -#include "BoatDataCalibration.h" class PageRudderPosition : public Page { @@ -41,7 +40,6 @@ public: GwApi::BoatValue *bvalue1 = pageData.values[0]; // First element in list String name1 = bvalue1->getName().c_str(); // Value name name1 = name1.substring(0, 6); // String length limit for value name - calibrationData.calibrateInstance(bvalue1, logger); // Check if boat data value is to be calibrated value1 = bvalue1->value; // Raw value without unit convertion bool valid1 = bvalue1->valid; // Valid information String svalue1 = formatValue(bvalue1, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places diff --git a/lib/obp60task/PageSixValues.cpp b/lib/obp60task/PageSixValues.cpp index af735f2..b75f307 100644 --- a/lib/obp60task/PageSixValues.cpp +++ b/lib/obp60task/PageSixValues.cpp @@ -2,7 +2,6 @@ #include "Pagedata.h" #include "OBP60Extensions.h" -#include "BoatDataCalibration.h" const int SixValues_x1 = 5; const int SixValues_DeltaX = 200; @@ -57,7 +56,6 @@ class PageSixValues : public Page bvalue = pageData.values[i]; DataName[i] = xdrDelete(bvalue->getName()); DataName[i] = DataName[i].substring(0, 6); // String length limit for value name - calibrationData.calibrateInstance(bvalue, logger); // Check if boat data value is to be calibrated DataValue[i] = bvalue->value; // Value as double in SI unit DataValid[i] = bvalue->valid; DataText[i] = formatValue(bvalue, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places diff --git a/lib/obp60task/PageThreeValues.cpp b/lib/obp60task/PageThreeValues.cpp index 996e2bc..7c5324f 100644 --- a/lib/obp60task/PageThreeValues.cpp +++ b/lib/obp60task/PageThreeValues.cpp @@ -2,7 +2,6 @@ #include "Pagedata.h" #include "OBP60Extensions.h" -#include "BoatDataCalibration.h" class PageThreeValues : public Page { @@ -44,7 +43,6 @@ class PageThreeValues : public Page GwApi::BoatValue *bvalue1 = pageData.values[0]; // First element in list (only one value by PageOneValue) String name1 = xdrDelete(bvalue1->getName()); // Value name name1 = name1.substring(0, 6); // String length limit for value name - calibrationData.calibrateInstance(bvalue1, logger); // Check if boat data value is to be calibrated double value1 = bvalue1->value; // Value as double in SI unit bool valid1 = bvalue1->valid; // Valid information String svalue1 = formatValue(bvalue1, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places @@ -54,7 +52,6 @@ class PageThreeValues : public Page GwApi::BoatValue *bvalue2 = pageData.values[1]; // Second element in list String name2 = xdrDelete(bvalue2->getName()); // Value name name2 = name2.substring(0, 6); // String length limit for value name - calibrationData.calibrateInstance(bvalue2, logger); // Check if boat data value is to be calibrated double value2 = bvalue2->value; // Value as double in SI unit bool valid2 = bvalue2->valid; // Valid information String svalue2 = formatValue(bvalue2, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places @@ -64,7 +61,6 @@ class PageThreeValues : public Page GwApi::BoatValue *bvalue3 = pageData.values[2]; // Third element in list String name3 = xdrDelete(bvalue3->getName()); // Value name name3 = name3.substring(0, 6); // String length limit for value name - calibrationData.calibrateInstance(bvalue3, logger); // Check if boat data value is to be calibrated double value3 = bvalue3->value; // Value as double in SI unit bool valid3 = bvalue3->valid; // Valid information String svalue3 = formatValue(bvalue3, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places diff --git a/lib/obp60task/PageTwoValues.cpp b/lib/obp60task/PageTwoValues.cpp index d4c0d98..323cbcb 100644 --- a/lib/obp60task/PageTwoValues.cpp +++ b/lib/obp60task/PageTwoValues.cpp @@ -2,7 +2,6 @@ #include "Pagedata.h" #include "OBP60Extensions.h" -#include "BoatDataCalibration.h" #include "OBPDataOperations.h" #include "OBPcharts.h" @@ -69,7 +68,6 @@ private: int yOffset = YOFFSET * i; String name = xdrDelete(bValue[i]->getName()); // Value name name = name.substring(0, 6); // String length limit for value name - calibrationData.calibrateInstance(bValue[i], logger); // Check if boat data value is to be calibrated double value = bValue[i]->value; // Value as double in SI unit bool valid = bValue[i]->valid; // Valid information String sValue = formatValue(bValue[i], *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places diff --git a/lib/obp60task/PageWind.cpp b/lib/obp60task/PageWind.cpp index 226b430..242a365 100644 --- a/lib/obp60task/PageWind.cpp +++ b/lib/obp60task/PageWind.cpp @@ -3,7 +3,6 @@ #include "Pagedata.h" #include "OBP60Extensions.h" #include "N2kMessages.h" -#include "BoatDataCalibration.h" #define front_width 120 #define front_height 162 @@ -324,7 +323,6 @@ public: } String name1 = bvalue1->getName().c_str(); // Value name name1 = name1.substring(0, 6); // String length limit for value name - calibrationData.calibrateInstance(bvalue1, logger); // Check if boat data value is to be calibrated double value1 = bvalue1->value; // Value as double in SI unit // bool valid1 = bvalue1->valid; // Valid information String svalue1 = formatValue(bvalue1, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places @@ -338,7 +336,6 @@ public: } String name2 = bvalue2->getName().c_str(); // Value name name2 = name2.substring(0, 6); // String length limit for value name - calibrationData.calibrateInstance(bvalue2, logger); // Check if boat data value is to be calibrated double value2 = bvalue2->value; // Value as double in SI unit // bool valid2 = bvalue2->valid; // Valid information if (simulation) { diff --git a/lib/obp60task/PageWindRose.cpp b/lib/obp60task/PageWindRose.cpp index c4ab0e0..427e64b 100644 --- a/lib/obp60task/PageWindRose.cpp +++ b/lib/obp60task/PageWindRose.cpp @@ -2,7 +2,6 @@ #include "Pagedata.h" #include "OBP60Extensions.h" -#include "BoatDataCalibration.h" class PageWindRose : public Page { @@ -52,7 +51,6 @@ public: GwApi::BoatValue *bvalue1 = pageData.values[0]; // First element in list (only one value by PageOneValue) String name1 = xdrDelete(bvalue1->getName()); // Value name name1 = name1.substring(0, 6); // String length limit for value name - calibrationData.calibrateInstance(bvalue1, logger); // Check if boat data value is to be calibrated double value1 = bvalue1->value; // Value as double in SI unit bool valid1 = bvalue1->valid; // Valid information value1 = formatValue(bvalue1, *commonData).value;// Format only nesaccery for simulation data for pointer @@ -67,7 +65,6 @@ public: GwApi::BoatValue *bvalue2 = pageData.values[1]; // Second element in list String name2 = xdrDelete(bvalue2->getName()); // Value name name2 = name2.substring(0, 6); // String length limit for value name - calibrationData.calibrateInstance(bvalue2, logger); // Check if boat data value is to be calibrated double value2 = bvalue2->value; // Value as double in SI unit bool valid2 = bvalue2->valid; // Valid information String svalue2 = formatValue(bvalue2, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places @@ -81,7 +78,6 @@ public: GwApi::BoatValue *bvalue3 = pageData.values[2]; // Third element in list String name3 = xdrDelete(bvalue3->getName()); // Value name name3 = name3.substring(0, 6); // String length limit for value name - calibrationData.calibrateInstance(bvalue3, logger); // Check if boat data value is to be calibrated double value3 = bvalue3->value; // Value as double in SI unit bool valid3 = bvalue3->valid; // Valid information String svalue3 = formatValue(bvalue3, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places @@ -95,7 +91,6 @@ public: GwApi::BoatValue *bvalue4 = pageData.values[3]; // Fourth element in list String name4 = xdrDelete(bvalue4->getName()); // Value name name4 = name4.substring(0, 6); // String length limit for value name - calibrationData.calibrateInstance(bvalue4, logger); // Check if boat data value is to be calibrated double value4 = bvalue4->value; // Value as double in SI unit bool valid4 = bvalue4->valid; // Valid information String svalue4 = formatValue(bvalue4, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places @@ -109,7 +104,6 @@ public: GwApi::BoatValue *bvalue5 = pageData.values[4]; // Fifth element in list String name5 = xdrDelete(bvalue5->getName()); // Value name name5 = name5.substring(0, 6); // String length limit for value name - calibrationData.calibrateInstance(bvalue5, logger); // Check if boat data value is to be calibrated double value5 = bvalue5->value; // Value as double in SI unit bool valid5 = bvalue5->valid; // Valid information String svalue5 = formatValue(bvalue5, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places @@ -123,7 +117,6 @@ public: GwApi::BoatValue *bvalue6 = pageData.values[5]; // Sixth element in list String name6 = xdrDelete(bvalue6->getName()); // Value name name6 = name6.substring(0, 6); // String length limit for value name - calibrationData.calibrateInstance(bvalue6, logger); // Check if boat data value is to be calibrated double value6 = bvalue6->value; // Value as double in SI unit bool valid6 = bvalue6->valid; // Valid information String svalue6 = formatValue(bvalue6, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places diff --git a/lib/obp60task/PageWindRoseFlex.cpp b/lib/obp60task/PageWindRoseFlex.cpp index 26efa28..d3526e0 100644 --- a/lib/obp60task/PageWindRoseFlex.cpp +++ b/lib/obp60task/PageWindRoseFlex.cpp @@ -2,7 +2,6 @@ #include "Pagedata.h" #include "OBP60Extensions.h" -#include "BoatDataCalibration.h" class PageWindRoseFlex : public Page { @@ -79,7 +78,6 @@ public: } String name1 = bvalue1->getName().c_str(); // Value name name1 = name1.substring(0, 6); // String length limit for value name - calibrationData.calibrateInstance(bvalue1, logger); // Check if boat data value is to be calibrated double value1 = bvalue1->value; // Value as double in SI unit bool valid1 = bvalue1->valid; // Valid information String svalue1 = formatValue(bvalue1, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places @@ -97,7 +95,6 @@ public: } String name2 = bvalue2->getName().c_str(); // Value name name2 = name2.substring(0, 6); // String length limit for value name - calibrationData.calibrateInstance(bvalue2, logger); // Check if boat data value is to be calibrated double value2 = bvalue2->value; // Value as double in SI unit bool valid2 = bvalue2->valid; // Valid information if (simulation) { @@ -122,7 +119,6 @@ public: else{ name3font=Ubuntu_Bold12pt8b; } - calibrationData.calibrateInstance(bvalue3, logger); // Check if boat data value is to be calibrated double value3 = bvalue3->value; // Value as double in SI unit bool valid3 = bvalue3->valid; // Valid information String svalue3 = formatValue(bvalue3, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places @@ -142,7 +138,6 @@ public: else{ name4font=Ubuntu_Bold12pt8b; } - calibrationData.calibrateInstance(bvalue4, logger); // Check if boat data value is to be calibrated double value4 = bvalue4->value; // Value as double in SI unit bool valid4 = bvalue4->valid; // Valid information String svalue4 = formatValue(bvalue4, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places @@ -162,7 +157,6 @@ public: else{ name5font=Ubuntu_Bold12pt8b; } - calibrationData.calibrateInstance(bvalue5, logger); // Check if boat data value is to be calibrated double value5 = bvalue5->value; // Value as double in SI unit bool valid5 = bvalue5->valid; // Valid information String svalue5 = formatValue(bvalue5, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places @@ -182,7 +176,6 @@ public: else{ name6font=Ubuntu_Bold8pt8b; } - calibrationData.calibrateInstance(bvalue6, logger); // Check if boat data value is to be calibrated double value6 = bvalue6->value; // Value as double in SI unit bool valid6 = bvalue6->valid; // Valid information String svalue6 = formatValue(bvalue6, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places diff --git a/lib/obp60task/config.json b/lib/obp60task/config.json index 3c7d3fe..0616a39 100644 --- a/lib/obp60task/config.json +++ b/lib/obp60task/config.json @@ -708,8 +708,10 @@ "AWA", "AWS", "COG", + "DBS", "DBT", "HDM", + "HDT", "PRPOS", "RPOS", "SOG", @@ -735,7 +737,7 @@ "obp60":"true" }, "condition": [ - { "calInstance1": ["AWA", "AWS", "COG", "DBT", "HDM", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + { "calInstance1": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } ] }, { @@ -743,13 +745,13 @@ "label": "Data Instance 1 Calibration Slope", "type": "number", "default": "1.00", - "description": "Slope for data instance 1", + "description": "Slope for data instance 1; Default: 1(!)", "category": "OBP60 Calibrations", "capabilities": { "obp60":"true" }, "condition": [ - { "calInstance1": ["AWA", "AWS", "COG", "DBT", "HDM", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + { "calInstance1": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } ] }, { @@ -766,7 +768,7 @@ "obp60":"true" }, "condition": [ - { "calInstance1": ["AWA", "AWS", "COG", "DBT", "HDM", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + { "calInstance1": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } ] }, { @@ -780,8 +782,10 @@ "AWA", "AWS", "COG", + "DBS", "DBT", "HDM", + "HDT", "PRPOS", "RPOS", "SOG", @@ -807,7 +811,7 @@ "obp60":"true" }, "condition": [ - { "calInstance2": ["AWA", "AWS", "COG", "DBT", "HDM", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + { "calInstance2": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } ] }, { @@ -815,13 +819,13 @@ "label": "Data Instance 2 Calibration Slope", "type": "number", "default": "1.00", - "description": "Slope for data instance 2", + "description": "Slope for data instance 2; Default: 1(!)", "category": "OBP60 Calibrations", "capabilities": { "obp60":"true" }, "condition": [ - { "calInstance2": ["AWA", "AWS", "COG", "DBT", "HDM", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + { "calInstance2": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } ] }, { @@ -838,7 +842,7 @@ "obp60":"true" }, "condition": [ - { "calInstance2": ["AWA", "AWS", "COG", "DBT", "HDM", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + { "calInstance2": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } ] }, { @@ -852,8 +856,10 @@ "AWA", "AWS", "COG", + "DBS", "DBT", "HDM", + "HDT", "PRPOS", "RPOS", "SOG", @@ -879,7 +885,7 @@ "obp60":"true" }, "condition": [ - { "calInstance3": ["AWA", "AWS", "COG", "DBT", "HDM", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + { "calInstance3": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } ] }, { @@ -887,13 +893,13 @@ "label": "Data Instance 3 Calibration Slope", "type": "number", "default": "1.00", - "description": "Slope for data instance 3", + "description": "Slope for data instance 3; Default: 1(!)", "category": "OBP60 Calibrations", "capabilities": { "obp60":"true" }, "condition": [ - { "calInstance3": ["AWA", "AWS", "COG", "DBT", "HDM", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + { "calInstance3": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } ] }, { @@ -910,7 +916,7 @@ "obp60":"true" }, "condition": [ - { "calInstance3": ["AWA", "AWS", "COG", "DBT", "HDM", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + { "calInstance3": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } ] }, { diff --git a/lib/obp60task/config_obp40.json b/lib/obp60task/config_obp40.json index c8553c0..bb0478a 100644 --- a/lib/obp60task/config_obp40.json +++ b/lib/obp60task/config_obp40.json @@ -719,8 +719,10 @@ "AWA", "AWS", "COG", + "DBS", "DBT", "HDM", + "HDT", "PRPOS", "RPOS", "SOG", @@ -746,7 +748,7 @@ "obp40":"true" }, "condition": [ - { "calInstance1": ["AWA", "AWS", "COG", "DBT", "HDM", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + { "calInstance1": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } ] }, { @@ -754,13 +756,13 @@ "label": "Data Instance 1 Calibration Slope", "type": "number", "default": "1.00", - "description": "Slope for data instance 1", + "description": "Slope for data instance 1, Default: 1(!)", "category": "OBP40 Calibrations", "capabilities": { "obp40":"true" }, "condition": [ - { "calInstance1": ["AWA", "AWS", "COG", "DBT", "HDM", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + { "calInstance1": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } ] }, { @@ -777,7 +779,7 @@ "obp40":"true" }, "condition": [ - { "calInstance1": ["AWA", "AWS", "COG", "DBT", "HDM", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + { "calInstance1": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } ] }, { @@ -791,8 +793,10 @@ "AWA", "AWS", "COG", + "DBS", "DBT", "HDM", + "HDT", "PRPOS", "RPOS", "SOG", @@ -818,7 +822,7 @@ "obp40":"true" }, "condition": [ - { "calInstance2": ["AWA", "AWS", "COG", "DBT", "HDM", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + { "calInstance2": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } ] }, { @@ -826,13 +830,13 @@ "label": "Data Instance 2 Calibration Slope", "type": "number", "default": "1.00", - "description": "Slope for data instance 2", + "description": "Slope for data instance 2; Default: 1(!)", "category": "OBP40 Calibrations", "capabilities": { "obp40":"true" }, "condition": [ - { "calInstance2": ["AWA", "AWS", "COG", "DBT", "HDM", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + { "calInstance2": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } ] }, { @@ -849,7 +853,7 @@ "obp40":"true" }, "condition": [ - { "calInstance2": ["AWA", "AWS", "COG", "DBT", "HDM", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + { "calInstance2": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } ] }, { @@ -863,8 +867,10 @@ "AWA", "AWS", "COG", + "DBS", "DBT", "HDM", + "HDT", "PRPOS", "RPOS", "SOG", @@ -890,7 +896,7 @@ "obp40":"true" }, "condition": [ - { "calInstance3": ["AWA", "AWS", "COG", "DBT", "HDM", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + { "calInstance3": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } ] }, { @@ -898,13 +904,13 @@ "label": "Data Instance 3 Calibration Slope", "type": "number", "default": "1.00", - "description": "Slope for data instance 3", + "description": "Slope for data instance 3, Default: 1(!)", "category": "OBP40 Calibrations", "capabilities": { "obp40":"true" }, "condition": [ - { "calInstance3": ["AWA", "AWS", "COG", "DBT", "HDM", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + { "calInstance3": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } ] }, { @@ -921,7 +927,7 @@ "obp40":"true" }, "condition": [ - { "calInstance3": ["AWA", "AWS", "COG", "DBT", "HDM", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + { "calInstance3": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } ] }, { diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index 1058ae0..5ac6575 100644 --- a/lib/obp60task/obp60task.cpp +++ b/lib/obp60task/obp60task.cpp @@ -12,7 +12,6 @@ #include // GxEPD2 lib for b/w E-Ink displays #include "OBP60Extensions.h" // Functions lib for extension board #include "OBP60Keypad.h" // Functions for keypad -#include "BoatDataCalibration.h" // Functions lib for data instance calibration #include "OBPDataOperations.h" // Functions lib for data operations such as true wind calculation #ifdef BOARD_OBP40S3 @@ -147,7 +146,6 @@ void keyboardTask(void *param){ vTaskDelete(NULL); } -// Scorgan: moved class declaration to header file to make class available to other functions // --- Class BoatValueList -------------- bool BoatValueList::addValueToList(GwApi::BoatValue *v){ for (int i=0;igetString(xxx); - //add all necessary data to common data + CalibrationData calibrationDataList(logger); // all boat data types which are supposed to be calibrated //fill the page data from config numPages=config->getInt(config->visiblePages,1); @@ -480,26 +477,25 @@ void OBP60Task(GwApi *api){ pages[i].parameters.values.push_back(value); } - // Read the specified boat data type of relevant pages and create a history buffer for each type + // Read the specified boat data types of relevant pages and create a history buffer for each type if (pages[i].parameters.pageName == "OneValue" || pages[i].parameters.pageName == "TwoValues" || pages[i].parameters.pageName == "WindPlot") { for (auto pVal : pages[i].parameters.values) { - hstryBufList.addBuffer(pVal->getName()); + hstryBufferList.addBuffer(pVal->getName()); } } // Add list of history buffers to page parameters - pages[i].parameters.hstryBuffers = &hstryBufList; + pages[i].parameters.hstryBuffers = &hstryBufferList; } // add out of band system page (always available) Page *syspage = allPages.pages[0]->creator(commonData); - // Check user settings for true wind calculation + // Read user settings from config file bool calcTrueWnds = api->getConfig()->getBool(api->getConfig()->calcTrueWnds, false); bool useSimuData = api->getConfig()->getBool(api->getConfig()->useSimuData, false); - - // Read all calibration data settings from config - calibrationData.readConfig(config, logger); + // Read user calibration data settings from config file + calibrationDataList.readConfig(config); // Display screenshot handler for HTTP request // http://192.168.15.1/api/user/OBP60Task/screenshot @@ -814,10 +810,10 @@ void OBP60Task(GwApi *api){ api->getStatus(commonData.status); if (calcTrueWnds) { - trueWind.addWinds(); + trueWind.addWinds(); // calculate true wind data from apparent wind values } - // Handle history buffers for certain boat data for windplot page and other usage - hstryBufList.handleHstryBufs(useSimuData, commonData); + calibrationDataList.handleCalibration(&boatValues); // Process calibration for all boat data in + hstryBufferList.handleHstryBufs(useSimuData, commonData); // Handle history buffers for certain boat data for windplot page and other usage // Clear display // getdisplay().fillRect(0, 0, getdisplay().width(), getdisplay().height(), commonData.bgcolor); From cc1d07fac049483aa40b7a52ab5e2f9dff335cd6 Mon Sep 17 00:00:00 2001 From: Ulrich Meine Date: Sat, 17 Jan 2026 13:07:48 +0100 Subject: [PATCH 095/121] True Wind Calculation: change wind angle range to [0..360] temporarily for proper display on pages --- lib/obp60task/OBPDataOperations.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/obp60task/OBPDataOperations.cpp b/lib/obp60task/OBPDataOperations.cpp index 1adb7f4..562b1f6 100644 --- a/lib/obp60task/OBPDataOperations.cpp +++ b/lib/obp60task/OBPDataOperations.cpp @@ -156,7 +156,7 @@ bool CalibrationData::calibrateInstance(GwApi::BoatValue* boatDataValue) if (format == "formatWind") { // instance is of type angle dataValue = (dataValue * slope) + offset; // dataValue = WindUtils::toPI(dataValue); - dataValue = WindUtils::to2PI(dataValue); // we should call for format of [-180..180], but pages cannot handle negative values yet + dataValue = WindUtils::to2PI(dataValue); // we should call for format of [-180..180], but pages cannot display negative values properly yet } else if (format == "formatCourse") { // instance is of type direction dataValue = (dataValue * slope) + offset; @@ -518,7 +518,8 @@ bool WindUtils::addWinds() twsBVal->valid = true; } if (!twaBVal->valid) { - twaBVal->value = twa; + //twaBVal->value = twa; + twaBVal->value = to2PI(twa); // convert to [0..360], because pages cannot display negative values properly yet twaBVal->valid = true; } if (!awdBVal->valid) { From cb2b85d50599a74af06c2d80745b068af1e91481 Mon Sep 17 00:00:00 2001 From: Ulrich Meine Date: Sat, 17 Jan 2026 13:45:41 +0100 Subject: [PATCH 096/121] Data Calibration: Extend no. of calibration instances from 3 to 4 --- lib/obp60task/OBPDataOperations.h | 2 +- lib/obp60task/config.json | 74 +++++++++++++++++++++++++++++++ lib/obp60task/config_obp40.json | 74 +++++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+), 1 deletion(-) diff --git a/lib/obp60task/OBPDataOperations.h b/lib/obp60task/OBPDataOperations.h index e42e293..9c5b783 100644 --- a/lib/obp60task/OBPDataOperations.h +++ b/lib/obp60task/OBPDataOperations.h @@ -22,7 +22,7 @@ private: std::unordered_map lastValue; // array for last smoothed values of boat data values GwLog* logger; - static constexpr int8_t MAX_CALIBRATION_DATA = 3; // maximum number of calibration data instances + static constexpr int8_t MAX_CALIBRATION_DATA = 4; // maximum number of calibration data instances public: CalibrationData(GwLog* log); diff --git a/lib/obp60task/config.json b/lib/obp60task/config.json index 0616a39..e897c7f 100644 --- a/lib/obp60task/config.json +++ b/lib/obp60task/config.json @@ -919,6 +919,80 @@ { "calInstance3": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } ] }, + { + "name": "calInstance4", + "label": "Calibration Data Instance 4", + "type": "list", + "default": "---", + "description": "Data instance for calibration", + "list": [ + "---", + "AWA", + "AWS", + "COG", + "DBS", + "DBT", + "HDM", + "HDT", + "PRPOS", + "RPOS", + "SOG", + "STW", + "TWA", + "TWS", + "TWD", + "WTemp" + ], + "category": "OBP60 Calibrations", + "capabilities": { + "obp60":"true" + } + }, + { + "name": "calOffset4", + "label": "Data Instance 4 Calibration Offset", + "type": "number", + "default": "0.00", + "description": "Offset for data instance 4", + "category": "OBP60 Calibrations", + "capabilities": { + "obp60":"true" + }, + "condition": [ + { "calInstance4": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + ] + }, + { + "name": "calSlope4", + "label": "Data Instance 4 Calibration Slope", + "type": "number", + "default": "1.00", + "description": "Slope for data instance 3; Default: 1(!)", + "category": "OBP60 Calibrations", + "capabilities": { + "obp60":"true" + }, + "condition": [ + { "calInstance4": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + ] + }, + { + "name": "calSmooth4", + "label": "Data Instance 4 Smoothing", + "type": "number", + "default": "0", + "check": "checkMinMax", + "min": 0, + "max": 10, + "description": "Smoothing factor [0..10]; 0 = no smoothing", + "category": "OBP60 Calibrations", + "capabilities": { + "obp60":"true" + }, + "condition": [ + { "calInstance4": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + ] + }, { "name": "mapsource", "label": "Map Source", diff --git a/lib/obp60task/config_obp40.json b/lib/obp60task/config_obp40.json index bb0478a..1fbc987 100644 --- a/lib/obp60task/config_obp40.json +++ b/lib/obp60task/config_obp40.json @@ -930,6 +930,80 @@ { "calInstance3": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } ] }, + { + "name": "calInstance4", + "label": "Calibration Data Instance 4", + "type": "list", + "default": "---", + "description": "Data instance for calibration", + "list": [ + "---", + "AWA", + "AWS", + "COG", + "DBS", + "DBT", + "HDM", + "HDT", + "PRPOS", + "RPOS", + "SOG", + "STW", + "TWA", + "TWS", + "TWD", + "WTemp" + ], + "category": "OBP40 Calibrations", + "capabilities": { + "obp40": "true" + } + }, + { + "name": "calOffset4", + "label": "Data Instance 4 Calibration Offset", + "type": "number", + "default": "0.00", + "description": "Offset for data instance 4", + "category": "OBP40 Calibrations", + "capabilities": { + "obp40":"true" + }, + "condition": [ + { "calInstance4": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + ] + }, + { + "name": "calSlope4", + "label": "Data Instance 4 Calibration Slope", + "type": "number", + "default": "1.00", + "description": "Slope for data instance 4, Default: 1(!)", + "category": "OBP40 Calibrations", + "capabilities": { + "obp40":"true" + }, + "condition": [ + { "calInstance4": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + ] + }, + { + "name": "calSmooth4", + "label": "Data Instance 4 Smoothing", + "type": "number", + "default": "0", + "check": "checkMinMax", + "min": 0, + "max": 10, + "description": "Smoothing factor [0..10]; 0 = no smoothing", + "category": "OBP40 Calibrations", + "capabilities": { + "obp40":"true" + }, + "condition": [ + { "calInstance4": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + ] + }, { "name": "mapsource", "label": "Map Source", From 1da26a90ec13a091d66e048583617da14ae17d24 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 30 Jan 2026 10:14:11 +0100 Subject: [PATCH 097/121] Auto stash before merge of "master" and "origin/master" --- lib/obp60task/PageDigitalOut.cpp | 7 +++++++ lib/obp60task/PageNavigation.cpp | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/lib/obp60task/PageDigitalOut.cpp b/lib/obp60task/PageDigitalOut.cpp index 9988aed..10db020 100644 --- a/lib/obp60task/PageDigitalOut.cpp +++ b/lib/obp60task/PageDigitalOut.cpp @@ -28,7 +28,10 @@ bool button5 = false; common.logger->logDebug(GwLog::LOG,"Instantiate PageDigitalOut"); } +<<<<<<< Updated upstream // Set botton labels +======= +>>>>>>> Stashed changes virtual void setupKeys(){ Page::setupKeys(); commonData->keydata[0].label = "BTN 1"; @@ -36,7 +39,11 @@ bool button5 = false; commonData->keydata[2].label = "BTN 3"; commonData->keydata[3].label = "BTN 4"; commonData->keydata[4].label = "BTN 5"; +<<<<<<< Updated upstream } +======= + } +>>>>>>> Stashed changes virtual int handleKey(int key){ // Code for keylock diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp index 8a3da06..1e0d16f 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -33,14 +33,22 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map imageBackupData = (uint8_t*)heap_caps_malloc((GxEPD_WIDTH * GxEPD_HEIGHT), MALLOC_CAP_SPIRAM); } +<<<<<<< Updated upstream // Set botton labels +======= +>>>>>>> Stashed changes virtual void setupKeys(){ Page::setupKeys(); commonData->keydata[0].label = "ZOOM -"; commonData->keydata[1].label = "ZOOM +"; commonData->keydata[4].label = "VALUES"; +<<<<<<< Updated upstream } +======= + } + +>>>>>>> Stashed changes virtual int handleKey(int key){ // Code for keylock if(key == 11){ @@ -483,7 +491,11 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map getdisplay().setCursor(70, 85); getdisplay().print(svalue6); } +<<<<<<< Updated upstream +======= + +>>>>>>> Stashed changes return PAGE_UPDATE; }; }; From d19da640ae7b7ca804bf1659983615b804eb8035 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 30 Jan 2026 17:26:10 +0100 Subject: [PATCH 098/121] Add PageDigitalOut --- lib/obp60task/OBP60Extensions.cpp | 43 +++++++++++------------ lib/obp60task/OBP60Extensions.h | 2 +- lib/obp60task/PageDigitalOut.cpp | 58 +++++++++++++++++-------------- lib/obp60task/PageNavigation.cpp | 12 ------- lib/obp60task/config.json | 55 +++++++++++++++++++++++++++++ lib/obp60task/config_obp40.json | 55 +++++++++++++++++++++++++++++ 6 files changed, 164 insertions(+), 61 deletions(-) diff --git a/lib/obp60task/OBP60Extensions.cpp b/lib/obp60task/OBP60Extensions.cpp index 99dedce..29911dd 100644 --- a/lib/obp60task/OBP60Extensions.cpp +++ b/lib/obp60task/OBP60Extensions.cpp @@ -90,7 +90,7 @@ void hardwareInit(GwApi *api) // Init PCF8574 digital outputs Wire.setClock(I2C_SPEED_LOW); // Set I2C clock on 10 kHz if(pcf8574_Modul1.begin()){ // Initialize PCF8574 - pcf8574_Modul1.write8(255); // Clear all outputs + pcf8574_Modul1.write8(255); // Clear all outputs (low activ) } Wire.setClock(I2C_SPEED); // Set I2C clock on 100 kHz fram = Adafruit_FRAM_I2C(); @@ -193,26 +193,23 @@ void powerInit(String powermode) { } } -/* Old function -void setPCF8574PortPin(uint8_t pin, uint8_t value){ - Wire.setClock(I2C_SPEED_LOW); // Set I2C clock on 10 kHz - if(pcf8574_Modul1.begin()){ // Check available and initialize PCF8574 - pcf8574_Modul1.write(pin, value); // Set pin - } - Wire.setClock(I2C_SPEED); // Set I2C clock on 100 kHz -} -*/ - -void setPCF8574PortPin(uint8_t pin, uint8_t value) +void setPCF8574PortPinModul1(uint8_t pin, uint8_t value) { + static bool firstRunFinished; + static uint8_t port1; // Retained data for port bits + // If fisrt run then set all outputs to low + if(firstRunFinished == false){ + port1 = 255; // Low active + firstRunFinished = true; + } if (pin > 7) return; - Wire.setClock(I2C_SPEED_LOW); - if (pcf8574_Modul1.begin()) + Wire.setClock(I2C_SPEED_LOW); // Set I2C clock on 10 kHz for longer wires + // Set bit + if (pcf8574_Modul1.begin(port1)) // Check module availability { - uint8_t port = pcf8574_Modul1.read8(); // Read all 8 bits - if (value == LOW) port &= ~(1 << pin); // Set bit - else port |= (1 << pin); - pcf8574_Modul1.write8(port); // Write byte + if (value == LOW) port1 &= ~(1 << pin); // Set bit + else port1 |= (1 << pin); + pcf8574_Modul1.write8(port1); // Write byte } Wire.setClock(I2C_SPEED); // Set I2C clock on 100 kHz } @@ -458,17 +455,19 @@ void drawButtonCenter(int16_t cx, int16_t cy, int8_t sx, int8_t sy, String text, int16_t x1, y1; uint16_t w, h; uint16_t color; - getdisplay().getTextBounds(text, 0, 150, &x1, &y1, &w, &h); // Find text center - getdisplay().setCursor(cx - w / 2, cy + h / 2); // Set cursor to center + + getdisplay().getTextBounds(text, cx, cy, &x1, &y1, &w, &h); // Find text center + getdisplay().setCursor(cx - w/2, cy + h/2); // Set cursor to center + //getdisplay().drawPixel(cx, cy, fg); // Debug pixel for center position if (inverted) { + getdisplay().fillRoundRect(cx - sx / 2, cy - sy / 2, sx, sy, 5, fg); // Draw button getdisplay().setTextColor(bg); getdisplay().print(text); // Draw text - getdisplay().fillRoundRect(cx - sx / 2, cy + sy / 2, sx, sy, 5, fg); // Draw button } else{ + getdisplay().drawRoundRect(cx - sx / 2, cy - sy / 2, sx, sy, 5, fg); // Draw button getdisplay().setTextColor(fg); getdisplay().print(text); // Draw text - getdisplay().drawRoundRect(cx - sx / 2, cy + sy / 2, sx, sy, 5, fg); // Draw button } } diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index 33c2a14..5975456 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -89,7 +89,7 @@ uint8_t getLastPage(); void hardwareInit(GwApi *api); void powerInit(String powermode); -void setPCF8574PortPin(uint8_t pin, uint8_t value);// Set PCF8574 port pin +void setPCF8574PortPinModul1(uint8_t pin, uint8_t value);// Set PCF8574 port pin void setPortPin(uint pin, bool value); // Set port pin for extension port void togglePortPin(uint pin); // Toggle extension port pin diff --git a/lib/obp60task/PageDigitalOut.cpp b/lib/obp60task/PageDigitalOut.cpp index 10db020..464a069 100644 --- a/lib/obp60task/PageDigitalOut.cpp +++ b/lib/obp60task/PageDigitalOut.cpp @@ -28,22 +28,15 @@ bool button5 = false; common.logger->logDebug(GwLog::LOG,"Instantiate PageDigitalOut"); } -<<<<<<< Updated upstream // Set botton labels -======= ->>>>>>> Stashed changes virtual void setupKeys(){ Page::setupKeys(); - commonData->keydata[0].label = "BTN 1"; - commonData->keydata[1].label = "BTN 2"; - commonData->keydata[2].label = "BTN 3"; - commonData->keydata[3].label = "BTN 4"; - commonData->keydata[4].label = "BTN 5"; -<<<<<<< Updated upstream + commonData->keydata[0].label = "1"; + commonData->keydata[1].label = "2"; + commonData->keydata[2].label = "3"; + commonData->keydata[3].label = "4"; + commonData->keydata[4].label = "5"; } -======= - } ->>>>>>> Stashed changes virtual int handleKey(int key){ // Code for keylock @@ -54,31 +47,31 @@ bool button5 = false; // Code for button 1 if(key == 1){ button1 = !button1; - setPCF8574PortPin(0, button1 ? 0 : 1); // Attention! Inverse logic for PCF8574 + setPCF8574PortPinModul1(0, button1 ? 0 : 1); // Attention! Inverse logic for PCF8574 return 0; // Commit the key } // Code for button 2 if(key == 2){ button2 = !button2; - setPCF8574PortPin(1, button2 ? 0 : 1); // Attention! Inverse logic for PCF8574 + setPCF8574PortPinModul1(1, button2 ? 0 : 1); // Attention! Inverse logic for PCF8574 return 0; // Commit the key } // Code for button 3 if(key == 3){ button3 = !button3; - setPCF8574PortPin(2, button3 ? 0 : 1); // Attention! Inverse logic for PCF8574 + setPCF8574PortPinModul1(2, button3 ? 0 : 1); // Attention! Inverse logic for PCF8574 return 0; // Commit the key } // Code for button 4 if(key == 4){ button4 = !button4; - setPCF8574PortPin(3, button4 ? 0 : 1); // Attention! Inverse logic for PCF8574 + setPCF8574PortPinModul1(3, button4 ? 0 : 1); // Attention! Inverse logic for PCF8574 return 0; // Commit the key } // Code for button 5 if(key == 5){ button5 = !button5; - setPCF8574PortPin(4, button5 ? 0 : 1); // Attention! Inverse logic for PCF8574 + setPCF8574PortPinModul1(4, button5 ? 0 : 1); // Attention! Inverse logic for PCF8574 return 0; // Commit the key } return key; @@ -94,6 +87,11 @@ bool button5 = false; bool holdvalues = config->getBool(config->holdvalues); String flashLED = config->getString(config->flashLED); String backlightMode = config->getString(config->backlight); + String name1 = config->getString(config->mod1Out1); + String name2 = config->getString(config->mod1Out2); + String name3 = config->getString(config->mod1Out3); + String name4 = config->getString(config->mod1Out4); + String name5 = config->getString(config->mod1Out5); // Optical warning by limit violation (unused) if(String(flashLED) == "Limit Violation"){ @@ -111,15 +109,23 @@ bool button5 = false; getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update getdisplay().setTextColor(commonData->fgcolor); getdisplay().setFont(&Ubuntu_Bold12pt8b); - drawButtonCenter(50, 30, 40, 20, "1", commonData->fgcolor, commonData->bgcolor, button1); - drawButtonCenter(50, 30, 40, 20, "2", commonData->fgcolor, commonData->bgcolor, button2); - drawButtonCenter(50, 30, 40, 20, "3", commonData->fgcolor, commonData->bgcolor, button3); - drawButtonCenter(50, 30, 40, 20, "4", commonData->fgcolor, commonData->bgcolor, button4); - drawButtonCenter(50, 30, 40, 20, "5", commonData->fgcolor, commonData->bgcolor, button5); - getdisplay().fillRoundRect(200, 250 , 200, 25, 5, commonData->fgcolor); // Black rect - getdisplay().fillRoundRect(202, 252 , 196, 21, 5, commonData->bgcolor); // White rect - getdisplay().setCursor(210, 270); - getdisplay().print("Map server lost"); + // Write text + getdisplay().setCursor(100, 50 + 8); + getdisplay().print(name1); + getdisplay().setCursor(100, 100 + 8); + getdisplay().print(name2); + getdisplay().setCursor(100, 150 + 8); + getdisplay().print(name3); + getdisplay().setCursor(100,200 + 8); + getdisplay().print(name4); + getdisplay().setCursor(100, 250 + 8); + getdisplay().print(name5); + // Draw bottons + drawButtonCenter(50, 50, 40, 27, "1", commonData->fgcolor, commonData->bgcolor, button1); + drawButtonCenter(50, 100, 40, 27, "2", commonData->fgcolor, commonData->bgcolor, button2); + drawButtonCenter(50, 150, 40, 27, "3", commonData->fgcolor, commonData->bgcolor, button3); + drawButtonCenter(50, 200, 40, 27, "4", commonData->fgcolor, commonData->bgcolor, button4); + drawButtonCenter(50, 250, 40, 27, "5", commonData->fgcolor, commonData->bgcolor, button5); return PAGE_UPDATE; }; diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp index 1e0d16f..bd4adf1 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -33,22 +33,14 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map imageBackupData = (uint8_t*)heap_caps_malloc((GxEPD_WIDTH * GxEPD_HEIGHT), MALLOC_CAP_SPIRAM); } -<<<<<<< Updated upstream // Set botton labels -======= ->>>>>>> Stashed changes virtual void setupKeys(){ Page::setupKeys(); commonData->keydata[0].label = "ZOOM -"; commonData->keydata[1].label = "ZOOM +"; commonData->keydata[4].label = "VALUES"; -<<<<<<< Updated upstream } - -======= - } ->>>>>>> Stashed changes virtual int handleKey(int key){ // Code for keylock if(key == 11){ @@ -491,11 +483,7 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map getdisplay().setCursor(70, 85); getdisplay().print(svalue6); } -<<<<<<< Updated upstream -======= - ->>>>>>> Stashed changes return PAGE_UPDATE; }; }; diff --git a/lib/obp60task/config.json b/lib/obp60task/config.json index 3c7d3fe..70e842a 100644 --- a/lib/obp60task/config.json +++ b/lib/obp60task/config.json @@ -661,6 +661,61 @@ "obp60":"true" } }, + { + "name": "mod1Out1", + "label": "Name1", + "type": "string", + "default": "text1", + "description": "Button name", + "category": "OBP60 IO-Modul1", + "capabilities": { + "obp60":"true" + } + }, + { + "name": "mod1Out2", + "label": "Name2", + "type": "string", + "default": "text2", + "description": "Button name", + "category": "OBP60 IO-Modul1", + "capabilities": { + "obp60":"true" + } + }, + { + "name": "mod1Out3", + "label": "Name3", + "type": "string", + "default": "text3", + "description": "Button name", + "category": "OBP60 IO-Modul1", + "capabilities": { + "obp60":"true" + } + }, + { + "name": "mod1Out4", + "label": "Name4", + "type": "string", + "default": "text4", + "description": "Button name", + "category": "OBP60 IO-Modul1", + "capabilities": { + "obp60":"true" + } + }, + { + "name": "mod1Out5", + "label": "Name5", + "type": "string", + "default": "text5", + "description": "Button name", + "category": "OBP60 IO-Modul1", + "capabilities": { + "obp60":"true" + } + }, { "name": "tSensitivity", "label": "Touch Sensitivity [%]", diff --git a/lib/obp60task/config_obp40.json b/lib/obp60task/config_obp40.json index c8553c0..ad7d880 100644 --- a/lib/obp60task/config_obp40.json +++ b/lib/obp60task/config_obp40.json @@ -672,6 +672,61 @@ "obp40": "true" } }, + { + "name": "mod1Out1", + "label": "Name1", + "type": "string", + "default": "text1", + "description": "Button name", + "category": "OBP60 IO-Modul1", + "capabilities": { + "obp40":"true" + } + }, + { + "name": "mod1Out2", + "label": "Name2", + "type": "string", + "default": "text2", + "description": "Button name", + "category": "OBP60 IO-Modul1", + "capabilities": { + "obp40":"true" + } + }, + { + "name": "mod1Out3", + "label": "Name3", + "type": "string", + "default": "text3", + "description": "Button name", + "category": "OBP60 IO-Modul1", + "capabilities": { + "obp40":"true" + } + }, + { + "name": "mod1Out4", + "label": "Name4", + "type": "string", + "default": "text4", + "description": "Button name", + "category": "OBP60 IO-Modul1", + "capabilities": { + "obp40":"true" + } + }, + { + "name": "mod1Out5", + "label": "Name5", + "type": "string", + "default": "text5", + "description": "Button name", + "category": "OBP60 IO-Modul1", + "capabilities": { + "obp40":"true" + } + }, { "name": "tSensitivity", "label": "Touch Sensitivity [%]", From 1de936fd47711d9a573ce7e199331b916c52027d Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Sun, 1 Feb 2026 18:09:44 +0100 Subject: [PATCH 099/121] Typo --- lib/obp60task/OBP60Extensions.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/obp60task/OBP60Extensions.cpp b/lib/obp60task/OBP60Extensions.cpp index 29911dd..57c3701 100644 --- a/lib/obp60task/OBP60Extensions.cpp +++ b/lib/obp60task/OBP60Extensions.cpp @@ -205,7 +205,7 @@ void setPCF8574PortPinModul1(uint8_t pin, uint8_t value) if (pin > 7) return; Wire.setClock(I2C_SPEED_LOW); // Set I2C clock on 10 kHz for longer wires // Set bit - if (pcf8574_Modul1.begin(port1)) // Check module availability + if (pcf8574_Modul1.begin(port1)) // Check module availability and start it { if (value == LOW) port1 &= ~(1 << pin); // Set bit else port1 |= (1 << pin); From 576f0a0d4fc54db1b26d63eeb65ac18af19b0ffb Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Mon, 2 Feb 2026 16:29:18 +0100 Subject: [PATCH 100/121] Fix for LED brightness and add Page Autopilot --- lib/obp60task/LedSpiTask.cpp | 4 +- lib/obp60task/PageAutopilot.cpp | 263 ++++++++++++++++++++++++++++++++ lib/obp60task/PageCompass.cpp | 8 +- lib/obp60task/obp60task.cpp | 4 +- 4 files changed, 273 insertions(+), 6 deletions(-) create mode 100644 lib/obp60task/PageAutopilot.cpp diff --git a/lib/obp60task/LedSpiTask.cpp b/lib/obp60task/LedSpiTask.cpp index fbbd640..74497e7 100644 --- a/lib/obp60task/LedSpiTask.cpp +++ b/lib/obp60task/LedSpiTask.cpp @@ -22,9 +22,11 @@ static uint8_t mulcolor(uint8_t f1, uint8_t f2){ } Color setBrightness(const Color &color,uint8_t brightness){ + if (brightness > 100) brightness = 100; + uint16_t br255=brightness*255; br255=br255/100; - //very simple for now + //Very simple for now Color rt=color; rt.g=mulcolor(rt.g,br255); rt.b=mulcolor(rt.b,br255); diff --git a/lib/obp60task/PageAutopilot.cpp b/lib/obp60task/PageAutopilot.cpp new file mode 100644 index 0000000..6cbf299 --- /dev/null +++ b/lib/obp60task/PageAutopilot.cpp @@ -0,0 +1,263 @@ +#if defined BOARD_OBP60S3 || defined BOARD_OBP40S3 + +#include "Pagedata.h" +#include "OBP60Extensions.h" + +// These constants have to match the declaration below in : +// PageDescription registerPageAutopilot( +// {"HDM","HDT", "COG", "STW", "SOG", "DBT","XTE", "DTW", "BTW"}, // Bus values we need in the page +const int HowManyValues = 9; + +const int AverageValues = 4; + +const int ShowHDM = 0; +const int ShowHDT = 1; +const int ShowCOG = 2; +const int ShowSTW = 3; +const int ShowSOG = 4; +const int ShowDBT = 5; +const int ShowXTE = 6; +const int ShowDTW = 7; +const int ShowBTW = 8; + +const int Compass_X0 = 200; // X center point of compass band +const int Compass_Y0 = 220; // Y position of compass lines +const int Compass_LineLength = 22; // Length of compass lines +const float Compass_LineDelta = 8.0;// Compass band: 1deg = 5 Pixels, 10deg = 50 Pixels + +class PageAutopilot : public Page +{ + int WhichDataCompass = ShowHDM; // Start value + int WhichDataDisplay = ShowHDM; // Start value + + public: + PageAutopilot(CommonData &common){ + commonData = &common; + common.logger->logDebug(GwLog::LOG,"Instantiate PageAutopilot"); + } + + virtual void setupKeys(){ + Page::setupKeys(); + commonData->keydata[0].label = "CMP"; + commonData->keydata[1].label = "SRC"; + } + + virtual int handleKey(int key){ + // Code for keylock + + if ( key == 1 ) { + WhichDataCompass += 1; + if ( WhichDataCompass > ShowCOG) + WhichDataCompass = ShowHDM; + return 0; + } + if ( key == 2 ) { + WhichDataDisplay += 1; + if ( WhichDataDisplay > ShowDBT) + WhichDataDisplay = ShowHDM; + } + + if(key == 11){ + commonData->keylock = !commonData->keylock; + return 0; // Commit the key + } + return key; + } + + int displayPage(PageData &pageData){ + GwConfigHandler *config = commonData->config; + GwLog *logger = commonData->logger; + + // Old values for hold function + static String OldDataText[HowManyValues] = {"", "", "","", "", "","", "", ""}; + static String OldDataUnits[HowManyValues] = {"", "", "","", "", "","", "", ""}; + + // Get config data + String lengthformat = config->getString(config->lengthFormat); + // bool simulation = config->getBool(config->useSimuData); + bool holdvalues = config->getBool(config->holdvalues); + String flashLED = config->getString(config->flashLED); + String backlightMode = config->getString(config->backlight); + + GwApi::BoatValue *bvalue; + String DataName[HowManyValues]; + double DataValue[HowManyValues]; + bool DataValid[HowManyValues]; + String DataText[HowManyValues]; + String DataUnits[HowManyValues]; + String DataFormat[HowManyValues]; + FormattedData TheFormattedData; + + for (int i = 0; i < HowManyValues; i++){ + bvalue = pageData.values[i]; + TheFormattedData = formatValue(bvalue, *commonData); + DataName[i] = xdrDelete(bvalue->getName()); + DataName[i] = DataName[i].substring(0, 6); // String length limit for value name + DataUnits[i] = formatValue(bvalue, *commonData).unit; + DataText[i] = TheFormattedData.svalue; // Formatted value as string including unit conversion and switching decimal places + DataValue[i] = TheFormattedData.value; // Value as double in SI unit + DataValid[i] = bvalue->valid; + DataFormat[i] = bvalue->getFormat(); // Unit of value + LOG_DEBUG(GwLog::LOG,"Drawing at PageAutopilot: %d %s %f %s %s", i, DataName[i], DataValue[i], DataFormat[i], DataText[i] ); + } + + // Optical warning by limit violation (unused) + if(String(flashLED) == "Limit Violation"){ + setBlinkingLED(false); + setFlashLED(false); + } + + if (bvalue == NULL) return PAGE_OK; // WTF why this statement? + + //*********************************************************** + + // Set display in partial refresh mode + getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + getdisplay().setTextColor(commonData->fgcolor); + + // Horizontal line 2 pix top & bottom + // Print data on top half + getdisplay().fillRect(0, 130, 400, 2, commonData->fgcolor); + getdisplay().setFont(&Ubuntu_Bold20pt8b); + getdisplay().setCursor(10, 70); + getdisplay().print(DataName[WhichDataDisplay]); // Page name + // Show unit + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().setCursor(10, 120); + getdisplay().print(DataUnits[WhichDataDisplay]); + getdisplay().setCursor(190, 120); + getdisplay().setFont(&DSEG7Classic_BoldItalic42pt7b); + + if(holdvalues == false){ + getdisplay().print(DataText[WhichDataDisplay]); // Real value as formated string + } + else{ + getdisplay().print(OldDataText[WhichDataDisplay]); // Old value as formated string + } + if(DataValid[WhichDataDisplay] == true){ + OldDataText[WhichDataDisplay] = DataText[WhichDataDisplay]; // Save the old value + OldDataUnits[WhichDataDisplay] = DataUnits[WhichDataDisplay]; // Save the old unit + } + + // Now draw compass band + // Get the data + double TheAngle = DataValue[WhichDataCompass]; + static double AvgAngle = 0; + AvgAngle = ( AvgAngle * AverageValues + TheAngle ) / (AverageValues + 1 ); + + int TheTrend = round( ( TheAngle - AvgAngle) * 180.0 / M_PI ); + + static const int bsize = 30; + char buffer[bsize+1]; + buffer[0]=0; + + getdisplay().setFont(&Ubuntu_Bold16pt8b); + getdisplay().setCursor(10, Compass_Y0-60); + getdisplay().print(DataName[WhichDataCompass]); // Page name + + + // Draw compass base line and pointer + getdisplay().fillRect(0, Compass_Y0, 400, 3, commonData->fgcolor); + getdisplay().fillTriangle(Compass_X0,Compass_Y0-40,Compass_X0-10,Compass_Y0-80,Compass_X0+10,Compass_Y0-80,commonData->fgcolor); + // Draw trendlines + for ( int i = 1; i < abs(TheTrend) / 2; i++){ + int x1; + if ( TheTrend < 0 ) + x1 = Compass_X0 + 20 * i; + else + x1 = Compass_X0 - 20 * ( i + 1 ); + + getdisplay().fillRect(x1, Compass_Y0 -55, 10, 6, commonData->fgcolor); + } + // Central line + satellite lines + double NextSector = round(TheAngle / ( M_PI / 9 )) * ( M_PI / 9 ); // Get the next 20degree value + double Offset = - ( NextSector - TheAngle); // Offest of the center line compared to TheAngle in Radian + + int Delta_X = int ( Offset * 180.0 / M_PI * Compass_LineDelta ); + for ( int i = 0; i <=4; i++ ){ + int x0; + x0 = Compass_X0 + Delta_X + 2 * i * 5 * Compass_LineDelta; + getdisplay().fillRect(x0-2, Compass_Y0 - 2 * Compass_LineLength, 5, 2 * Compass_LineLength, commonData->fgcolor); + x0 = Compass_X0 + Delta_X + ( 2 * i + 1 ) * 5 * Compass_LineDelta; + getdisplay().fillRect(x0-1, Compass_Y0 - Compass_LineLength, 3, Compass_LineLength, commonData->fgcolor); + + x0 = Compass_X0 + Delta_X - 2 * i * 5 * Compass_LineDelta; + getdisplay().fillRect(x0-2, Compass_Y0 - 2 * Compass_LineLength, 5, 2 * Compass_LineLength, commonData->fgcolor); + x0 = Compass_X0 + Delta_X - ( 2 * i + 1 ) * 5 * Compass_LineDelta; + getdisplay().fillRect(x0-1, Compass_Y0 - Compass_LineLength, 3, Compass_LineLength, commonData->fgcolor); + } + + getdisplay().fillRect(0, Compass_Y0, 400, 3, commonData->fgcolor); + // Add the numbers to the compass band + int x0; + float AngleToDisplay = NextSector * 180.0 / M_PI; + + x0 = Compass_X0 + Delta_X; + getdisplay().setFont(&DSEG7Classic_BoldItalic16pt7b); + + do { + getdisplay().setCursor(x0 - 40, Compass_Y0 + 40); + snprintf(buffer,bsize,"%03.0f", AngleToDisplay); + getdisplay().print(buffer); + AngleToDisplay += 20; + if ( AngleToDisplay >= 360.0 ) + AngleToDisplay -= 360.0; + x0 -= 4 * 5 * Compass_LineDelta; + } while ( x0 >= 0 - 60 ); + + AngleToDisplay = NextSector * 180.0 / M_PI - 20; + if ( AngleToDisplay < 0 ) + AngleToDisplay += 360.0; + + x0 = Compass_X0 + Delta_X + 4 * 5 * Compass_LineDelta; + do { + getdisplay().setCursor(x0 - 40, Compass_Y0 + 40); + snprintf(buffer,bsize,"%03.0f", AngleToDisplay); + // Quick and dirty way to prevent wrapping text in next line + if ( ( x0 - 40 ) > 380 ) + buffer[0] = 0; + else if ( ( x0 - 40 ) > 355 ) + buffer[1] = 0; + else if ( ( x0 - 40 ) > 325 ) + buffer[2] = 0; + + getdisplay().print(buffer); + + AngleToDisplay -= 20; + if ( AngleToDisplay < 0 ) + AngleToDisplay += 360.0; + x0 += 4 * 5 * Compass_LineDelta; + } while (x0 < ( 400 - 20 -40 ) ); + + // static int x_test = 320; + // x_test += 2; + + // snprintf(buffer,bsize,"%03d", x_test); + // getdisplay().setCursor(x_test, Compass_Y0 - 60); + // getdisplay().print(buffer); + // if ( x_test > 390) + // x_test = 320; + + return PAGE_UPDATE; + }; + +}; + +static Page *createPage(CommonData &common){ + return new PageAutopilot(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 registerPageAutopilot( + "Autopilot", // Page name + createPage, // Action + 0, // Number of bus values depends on selection in Web configuration + {"HDM","HDT", "COG", "STW", "SOG", "DBT","XTE", "DTW", "BTW"}, // Bus values we need in the page + true // Show display header on/off +); + +#endif diff --git a/lib/obp60task/PageCompass.cpp b/lib/obp60task/PageCompass.cpp index db98c87..12281e9 100644 --- a/lib/obp60task/PageCompass.cpp +++ b/lib/obp60task/PageCompass.cpp @@ -17,10 +17,10 @@ const int ShowSTW = 3; const int ShowSOG = 4; const int ShowDBS = 5; -const int Compass_X0 = 200; // center point of compass band -const int Compass_Y0 = 220; // position of compass lines -const int Compass_LineLength = 22; // length of compass lines -const float Compass_LineDelta = 8.0;// compass band: 1deg = 5 Pixels, 10deg = 50 Pixels +const int Compass_X0 = 200; // X center point of compass band +const int Compass_Y0 = 220; // Y position of compass lines +const int Compass_LineLength = 22; // Length of compass lines +const float Compass_LineDelta = 8.0;// Compass band: 1deg = 5 Pixels, 10deg = 50 Pixels class PageCompass : public Page { diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index 1058ae0..c213ba4 100644 --- a/lib/obp60task/obp60task.cpp +++ b/lib/obp60task/obp60task.cpp @@ -264,6 +264,8 @@ void registerAllPages(PageList &list){ list.add(®isterPageNavigation); extern PageDescription registerPageDigitalOut; list.add(®isterPageDigitalOut); + extern PageDescription registerPageAutopilot; + list.add(®isterPageAutopilot); } // Undervoltage detection for shutdown display @@ -531,7 +533,7 @@ void OBP60Task(GwApi *api){ commonData.backlight.mode = backlightMapping(config->getConfigItem(config->backlight,true)->asString()); commonData.backlight.color = colorMapping(config->getConfigItem(config->blColor,true)->asString()); - commonData.backlight.brightness = 2.55 * uint(config->getConfigItem(config->blBrightness,true)->asInt()); + commonData.backlight.brightness = uint(config->getConfigItem(config->blBrightness,true)->asInt()); commonData.powermode = api->getConfig()->getConfigItem(api->getConfig()->powerMode,true)->asString(); bool uvoltage = config->getConfigItem(config->underVoltage, true)->asBoolean(); From 6a56a8fb569ebb77eed40a092d30511cd2c7aa98 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Mon, 2 Feb 2026 17:00:27 +0100 Subject: [PATCH 101/121] Fix I2C adresses for INA219 --- lib/obp60task/OBP60Hardware.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/obp60task/OBP60Hardware.h b/lib/obp60task/OBP60Hardware.h index fa25e2d..8768e0f 100644 --- a/lib/obp60task/OBP60Hardware.h +++ b/lib/obp60task/OBP60Hardware.h @@ -23,8 +23,8 @@ #define AS5600_I2C_ADDR 0x36 // Addr. 0x36 (fix) // INA219 #define SHUNT_VOLTAGE 0.075 // Shunt voltage in V by max. current (75mV) - #define INA219_I2C_ADDR1 0x40 // Addr. 0x41 (fix A0 = 5V, A1 = GND) for battery - #define INA219_I2C_ADDR2 0x41 // Addr. 0x44 (fix A0 = GND, A1 = 5V) for solar panels + #define INA219_I2C_ADDR1 0x41 // Addr. 0x41 (fix A0 = 5V, A1 = GND) for battery + #define INA219_I2C_ADDR2 0x44 // Addr. 0x44 (fix A0 = GND, A1 = 5V) for solar panels #define INA219_I2C_ADDR3 0x45 // Addr. 0x45 (fix A0 = 5V, A1 = 5V) for generator // INA226 #define INA226_I2C_ADDR1 0x41 // Addr. 0x41 (fix A0 = 5V, A1 = GND) for battery @@ -103,8 +103,8 @@ #define AS5600_I2C_ADDR 0x36 // Addr. 0x36 (fix) // INA219 #define SHUNT_VOLTAGE 0.075 // Shunt voltage in V by max. current (75mV) - #define INA219_I2C_ADDR1 0x40 // Addr. 0x41 (fix A0 = 5V, A1 = GND) for battery - #define INA219_I2C_ADDR2 0x41 // Addr. 0x44 (fix A0 = GND, A1 = 5V) for solar panels + #define INA219_I2C_ADDR1 0x41 // Addr. 0x41 (fix A0 = 5V, A1 = GND) for battery + #define INA219_I2C_ADDR2 0x44 // Addr. 0x44 (fix A0 = GND, A1 = 5V) for solar panels #define INA219_I2C_ADDR3 0x45 // Addr. 0x45 (fix A0 = 5V, A1 = 5V) for generator // INA226 #define INA226_I2C_ADDR1 0x41 // Addr. 0x41 (fix A0 = 5V, A1 = GND) for battery From 753e87068fb586ab773c14a226ac03ecb800ccc0 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Mon, 2 Feb 2026 21:26:43 +0100 Subject: [PATCH 102/121] New function for backlight - 3 brigjhtness steps --- lib/obp60task/OBP60Extensions.cpp | 34 +++++++++++++++++++++++++++++++ lib/obp60task/OBP60Extensions.h | 1 + lib/obp60task/config.json | 4 ++-- lib/obp60task/config_obp40.json | 4 ++-- lib/obp60task/obp60task.cpp | 2 +- 5 files changed, 40 insertions(+), 5 deletions(-) diff --git a/lib/obp60task/OBP60Extensions.cpp b/lib/obp60task/OBP60Extensions.cpp index 57c3701..76c6334 100644 --- a/lib/obp60task/OBP60Extensions.cpp +++ b/lib/obp60task/OBP60Extensions.cpp @@ -331,6 +331,40 @@ void toggleBacklightLED(uint brightness, const Color &color){ ledTaskData->setLedData(current); } +void stepsBacklightLED(uint brightness, const Color &color){ + static uint step = 0; + uint actBrightness = 0; + // Different brightness steps + if(step == 0){ + actBrightness = brightness; // 100% from brightess + statusBacklightLED = true; + } + if(step == 1){ + actBrightness = brightness * 0.5; // 50% from brightess + statusBacklightLED = true; + } + if(step == 2){ + actBrightness = brightness * 0.2; // 20% from brightess + statusBacklightLED = true; + } + if(step == 3){ + actBrightness = 0; // 0% + statusBacklightLED = false; + } + if(actBrightness < 5){ // Limiter if values too low + actBrightness = 5; + } + step = step + 1; // Increment step counter + if(step == 4){ // Reset counter + step = 0; + } + if (ledTaskData == nullptr) return; + Color nv=setBrightness(statusBacklightLED?color:COLOR_BLACK,actBrightness); + LedInterface current=ledTaskData->getLedData(); + current.setBacklight(nv); + ledTaskData->setLedData(current); +} + void setFlashLED(bool status){ if (ledTaskData == nullptr) return; Color c=status?COLOR_RED:COLOR_BLACK; diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index 5975456..604c356 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -96,6 +96,7 @@ void togglePortPin(uint pin); // Toggle extension port pin Color colorMapping(const String &colorString); // Color mapping string to CHSV colors void setBacklightLED(uint brightness, const Color &color);// Set backlight LEDs void toggleBacklightLED(uint brightness,const Color &color);// Toggle backlight LEDs +void stepsBacklightLED(uint brightness, const Color &color);// Set backlight LEDs in 4 steps (100%, 50%, 10%, 0%) BacklightMode backlightMapping(const String &backlightString);// Configuration string to value void setFlashLED(bool status); // Set flash LED diff --git a/lib/obp60task/config.json b/lib/obp60task/config.json index 70e842a..307f9f4 100644 --- a/lib/obp60task/config.json +++ b/lib/obp60task/config.json @@ -1273,9 +1273,9 @@ "type": "number", "default": "50", "check": "checkMinMax", - "min": 20, + "min": 5, "max": 100, - "description": "Backlight brightness [20...100%]", + "description": "Backlight brightness [5...100%]", "category": "OBP60 Display", "capabilities": { "obp60":"true" diff --git a/lib/obp60task/config_obp40.json b/lib/obp60task/config_obp40.json index ad7d880..9889b04 100644 --- a/lib/obp60task/config_obp40.json +++ b/lib/obp60task/config_obp40.json @@ -1285,9 +1285,9 @@ "type": "number", "default": "50", "check": "checkMinMax", - "min": 20, + "min": 5, "max": 100, - "description": "Backlight brightness [20...100%]", + "description": "Backlight brightness [5...100%]", "category": "OBP40 Display", "capabilities": { "obp40": "false" diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index c213ba4..e206204 100644 --- a/lib/obp60task/obp60task.cpp +++ b/lib/obp60task/obp60task.cpp @@ -660,7 +660,7 @@ void OBP60Task(GwApi *api){ // if(String(backlight) == "Control by Key"){ if(keyboardMessage == 6){ LOG_DEBUG(GwLog::LOG,"Toggle Backlight LED"); - toggleBacklightLED(commonData.backlight.brightness, commonData.backlight.color); + stepsBacklightLED(commonData.backlight.brightness, commonData.backlight.color); } } #ifdef BOARD_OBP40S3 From 352009073e29bd6eae19c19d7d2290f559978be1 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Mon, 2 Feb 2026 21:38:14 +0100 Subject: [PATCH 103/121] Setup to remote project --- extra_script.py | 30 +++--------------------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/extra_script.py b/extra_script.py index dd9a90b..ed62c64 100644 --- a/extra_script.py +++ b/extra_script.py @@ -10,7 +10,7 @@ from datetime import datetime import re import pprint from platformio.project.config import ProjectConfig -from platformio.project.exception import InvalidProjectConfError + Import("env") #print(env.Dump()) @@ -104,17 +104,7 @@ def writeFileIfChanged(fileName,data): return True def mergeConfig(base,other): - try: - customconfig = env.GetProjectOption("custom_config") - except InvalidProjectConfError: - customconfig = None for bdir in other: - if customconfig and os.path.exists(os.path.join(bdir,customconfig)): - cname=os.path.join(bdir,customconfig) - print("merge custom config {}".format(cname)) - with open(cname,'rb') as ah: - base += json.load(ah) - continue cname=os.path.join(bdir,"config.json") if os.path.exists(cname): print("merge config %s"%cname) @@ -284,9 +274,9 @@ class Grove: def _ss(self,z=False): if z: return self.name - return self.name if self.name != 'Z' else '' + return self.name if self.name is not 'Z' else '' def _suffix(self): - return '_'+self.name if self.name != 'Z' else '' + return '_'+self.name if self.name is not 'Z' else '' def replace(self,line): if line is None: return line @@ -526,17 +516,3 @@ env.Append( ) #script does not run on clean yet - maybe in the future env.AddPostAction("clean",cleangenerated) - -#look for extra task scripts and include them here -for taskdir in userTaskDirs: - script = os.path.join(taskdir, "extra_task.py") - if os.path.isfile(script): - taskname = os.path.basename(os.path.normpath(taskdir)) - print("#extra task script for '{}'".format(taskname)) - with open(script) as fh: - try: - code = compile(fh.read(), taskname, 'exec') - except SyntaxError: - print("#ERROR: script does not compile") - continue - exec(code) From 6bc1b60f605aa37019bdf2e60242e50eb26dd381 Mon Sep 17 00:00:00 2001 From: Thomas Hooge Date: Wed, 4 Feb 2026 08:18:39 +0100 Subject: [PATCH 104/121] Fix build for new gateway extra_script.py --- extra_script.py | 61 ++++++++++++++----- .../{config.json => config_obp60.json} | 0 lib/obp60task/platformio.ini | 5 +- 3 files changed, 50 insertions(+), 16 deletions(-) rename lib/obp60task/{config.json => config_obp60.json} (100%) diff --git a/extra_script.py b/extra_script.py index 2b7b787..3f41463 100644 --- a/extra_script.py +++ b/extra_script.py @@ -104,8 +104,7 @@ def writeFileIfChanged(fileName,data): return True def mergeConfig(base,other): - for bdir in other: - cname=os.path.join(bdir,"config.json") + for cname in other: if os.path.exists(cname): print("merge config %s"%cname) with open(cname,'rb') as ah: @@ -151,13 +150,25 @@ def expandConfig(config): rt.append(replaceTexts(c,replace)) return rt -def generateMergedConfig(inFile,outFile,addDirs=[]): +def createUserItemList(dirs,itemName,files): + rt=[] + for d in dirs: + iname=os.path.join(d,itemName) + if os.path.exists(iname): + rt.append(iname) + for f in files: + if not os.path.exists(f): + raise Exception("user item %s not found"%f) + rt.append(f) + return rt + +def generateMergedConfig(inFile,outFile,addFiles=[]): if not os.path.exists(inFile): raise Exception("unable to read cfg file %s"%inFile) data="" with open(inFile,'rb') as ch: config=json.load(ch) - config=mergeConfig(config,addDirs) + config=mergeConfig(config,addFiles) config=expandConfig(config) data=json.dumps(config,indent=2) writeFileIfChanged(outFile,data) @@ -274,9 +285,9 @@ class Grove: def _ss(self,z=False): if z: return self.name - return self.name if self.name is not 'Z' else '' + return self.name if self.name != 'Z' else '' def _suffix(self): - return '_'+self.name if self.name is not 'Z' else '' + return '_'+self.name if self.name != 'Z' else '' def replace(self,line): if line is None: return line @@ -377,12 +388,7 @@ def getLibs(): -def joinFiles(target,pattern,dirlist): - flist=[] - for dir in dirlist: - fn=os.path.join(dir,pattern) - if os.path.exists(fn): - flist.append(fn) +def joinFiles(target,flist): current=False if os.path.exists(target): current=True @@ -453,7 +459,28 @@ def handleDeps(env): ) env.AddBuildMiddleware(injectIncludes) +def getOption(env,name,toArray=True): + try: + opt=env.GetProjectOption(name) + if toArray: + if opt is None: + return [] + if isinstance(opt,list): + return opt + return opt.split("\n" if "\n" in opt else ",") + return opt + except: + pass + if toArray: + return [] +def getFileList(files): + base=basePath() + rt=[] + for f in files: + if f is not None and f != "": + rt.append(os.path.join(base,f)) + return rt def prebuild(env): global userTaskDirs print("#prebuild running") @@ -463,14 +490,18 @@ def prebuild(env): if ldf_mode == 'off': print("##ldf off - own dependency handling") handleDeps(env) + extraConfigs=getOption(env,'custom_config',toArray=True) + extraJs=getOption(env,'custom_js',toArray=True) + extraCss=getOption(env,'custom_css',toArray=True) + userTaskDirs=getUserTaskDirs() mergedConfig=os.path.join(outPath(),os.path.basename(CFG_FILE)) - generateMergedConfig(os.path.join(basePath(),CFG_FILE),mergedConfig,userTaskDirs) + generateMergedConfig(os.path.join(basePath(),CFG_FILE),mergedConfig,createUserItemList(userTaskDirs,"config.json", getFileList(extraConfigs))) compressFile(mergedConfig,mergedConfig+".gz") generateCfg(mergedConfig,os.path.join(outPath(),CFG_INCLUDE),False) generateCfg(mergedConfig,os.path.join(outPath(),CFG_INCLUDE_IMPL),True) - joinFiles(os.path.join(outPath(),INDEXJS+".gz"),INDEXJS,["web"]+userTaskDirs) - joinFiles(os.path.join(outPath(),INDEXCSS+".gz"),INDEXCSS,["web"]+userTaskDirs) + joinFiles(os.path.join(outPath(),INDEXJS+".gz"),createUserItemList(["web"]+userTaskDirs,INDEXJS,getFileList(extraJs))) + joinFiles(os.path.join(outPath(),INDEXCSS+".gz"),createUserItemList(["web"]+userTaskDirs,INDEXCSS,getFileList(extraCss))) embedded=getEmbeddedFiles(env) filedefs=[] for ef in embedded: diff --git a/lib/obp60task/config.json b/lib/obp60task/config_obp60.json similarity index 100% rename from lib/obp60task/config.json rename to lib/obp60task/config_obp60.json diff --git a/lib/obp60task/platformio.ini b/lib/obp60task/platformio.ini index 0f40d7e..63468c3 100644 --- a/lib/obp60task/platformio.ini +++ b/lib/obp60task/platformio.ini @@ -16,6 +16,8 @@ board_build.variants_dir = variants board = obp60_s3_n16r8 #ESP32-S3 N16R8, 16MB flash, 8MB PSRAM, production series #board_build.partitions = default_8MB.csv #ESP32-S3 N8, 8MB flash board_build.partitions = default_16MB.csv #ESP32-S3 N16, 16MB flash +custom_config = lib/obp60task/config_obp60.json +custom_script = lib/obp60task/extra_task.py framework = arduino lib_deps = ${basedeps.lib_deps} @@ -68,7 +70,8 @@ platform = espressif32@6.8.1 board_build.variants_dir = variants board = obp40_s3_n8r8 #ESP32-S3 N8R8, 8MB flash, 8MB PSRAM, OBP60 clone (CrowPanel 4.2) board_build.partitions = default_8MB.csv #ESP32-S3 N8, 8MB flash -custom_config = config_obp40.json +custom_config = lib/obp60task/config_obp40.json +custom_script = lib/obp60task/extra_task.py framework = arduino lib_deps = ${basedeps.lib_deps} From 337214d6500be838415126ec7bc38af7d3cb5dca Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Wed, 4 Feb 2026 16:48:50 +0100 Subject: [PATCH 105/121] Fix config problem for OBP60, missing setup values --- lib/obp60task/PageClock.cpp | 32 ++++++++++++++++++-------------- lib/obp60task/config_obp60.json | 4 ++-- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/lib/obp60task/PageClock.cpp b/lib/obp60task/PageClock.cpp index a0be7d7..7c7b74f 100644 --- a/lib/obp60task/PageClock.cpp +++ b/lib/obp60task/PageClock.cpp @@ -19,7 +19,12 @@ class PageClock : public Page bool simulation = false; int simtime; bool keylock = false; +#ifdef BOARD_OBP60S3 +char source = 'G'; // time source (R)TC | (G)PS | (N)TP +#endif +#ifdef BOARD_OBP40S3 char source = 'R'; // time source (R)TC | (G)PS | (N)TP +#endif char mode = 'A'; // display mode (A)nalog | (D)igital | race (T)imer char tz = 'L'; // time zone (L)ocal | (U)TC double timezone = 0; // there are timezones with non int offsets, e.g. 5.5 or 5.75 @@ -50,29 +55,28 @@ bool homevalid = false; // homelat and homelon are valid virtual int handleKey(int key){ // Time source if (key == 1) { - if (source == 'G') { - source = 'R'; - } else { - source = 'G'; + switch (source) { + case 'G': source = 'R'; break; + case 'R': source = 'G'; break; + default: source = 'G'; break; } return 0; } if (key == 2) { - if (mode == 'A') { - mode = 'D'; - } else if (mode == 'D') { - mode = 'T'; - } else { - mode = 'A'; + switch (mode) { + case 'A': mode = 'D'; break; + case 'D': mode = 'T'; break; + case 'T': mode = 'A'; break; + default: mode = 'A'; break; } return 0; } // Time zone: Local / UTC if (key == 5) { - if (tz == 'L') { - tz = 'U'; - } else { - tz = 'L'; + switch (tz) { + case 'L': tz = 'U'; break; + case 'U': tz = 'L'; break; + default: tz = 'L'; break; } return 0; } diff --git a/lib/obp60task/config_obp60.json b/lib/obp60task/config_obp60.json index 2aa21fa..8318af7 100644 --- a/lib/obp60task/config_obp60.json +++ b/lib/obp60task/config_obp60.json @@ -37,7 +37,7 @@ "name": "homeLAT", "label": "Home latitude", "type": "number", - "default": "", + "default": "0.00000", "check": "checkMinMax", "min": -90.0, "max": 90.0, @@ -51,7 +51,7 @@ "name": "homeLON", "label": "Home longitude", "type": "number", - "default": "", + "default": "0.00000", "check": "checkMinMax", "min": -180.0, "max": 180.0, From ccca784ac2f477fc45185b08bca13f2e0ec66e37 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Wed, 4 Feb 2026 16:56:11 +0100 Subject: [PATCH 106/121] Delete old extra.script.py --- extra_script.py.new | 542 -------------------------------------------- extra_script.py.old | 518 ------------------------------------------ 2 files changed, 1060 deletions(-) delete mode 100644 extra_script.py.new delete mode 100644 extra_script.py.old diff --git a/extra_script.py.new b/extra_script.py.new deleted file mode 100644 index dd9a90b..0000000 --- a/extra_script.py.new +++ /dev/null @@ -1,542 +0,0 @@ -print("running extra...") -import gzip -import shutil -import os -import sys -import inspect -import json -import glob -from datetime import datetime -import re -import pprint -from platformio.project.config import ProjectConfig -from platformio.project.exception import InvalidProjectConfError - -Import("env") -#print(env.Dump()) -OWN_FILE="extra_script.py" -GEN_DIR='lib/generated' -CFG_FILE='web/config.json' -XDR_FILE='web/xdrconfig.json' -INDEXJS="index.js" -INDEXCSS="index.css" -CFG_INCLUDE='GwConfigDefinitions.h' -CFG_INCLUDE_IMPL='GwConfigDefImpl.h' -XDR_INCLUDE='GwXdrTypeMappings.h' -TASK_INCLUDE='GwUserTasks.h' -GROVE_CONFIG="GwM5GroveGen.h" -GROVE_CONFIG_IN="lib/hardware/GwM5Grove.in" -EMBEDDED_INCLUDE="GwEmbeddedFiles.h" - -def getEmbeddedFiles(env): - rt=[] - efiles=env.GetProjectOption("board_build.embed_files") - for f in efiles.split("\n"): - if f == '': - continue - rt.append(f) - return rt - -def basePath(): - #see: https://stackoverflow.com/questions/16771894/python-nameerror-global-name-file-is-not-defined - return os.path.dirname(inspect.getfile(lambda: None)) - -def outPath(): - return os.path.join(basePath(),GEN_DIR) -def checkDir(): - dn=outPath() - if not os.path.exists(dn): - os.makedirs(dn) - if not os.path.isdir(dn): - print("unable to create %s"%dn) - return False - return True - -def isCurrent(infile,outfile): - if os.path.exists(outfile): - otime=os.path.getmtime(outfile) - itime=os.path.getmtime(infile) - if (otime >= itime): - own=os.path.join(basePath(),OWN_FILE) - if os.path.exists(own): - owntime=os.path.getmtime(own) - if owntime > otime: - return False - print("%s is newer then %s, no need to recreate"%(outfile,infile)) - return True - return False -def compressFile(inFile,outfile): - if isCurrent(inFile,outfile): - return - print("compressing %s"%inFile) - with open(inFile, 'rb') as f_in: - with gzip.open(outfile, 'wb') as f_out: - shutil.copyfileobj(f_in, f_out) - -def generateFile(infile,outfile,callback,inMode='rb',outMode='w'): - if isCurrent(infile,outfile): - return - print("creating %s"%outfile) - oh=None - with open(infile,inMode) as ch: - with open(outfile,outMode) as oh: - try: - callback(ch,oh,inFile=infile) - oh.close() - except Exception as e: - try: - oh.close() - except: - pass - os.unlink(outfile) - raise - -def writeFileIfChanged(fileName,data): - if os.path.exists(fileName): - with open(fileName,"r") as ih: - old=ih.read() - ih.close() - if old == data: - return False - print("#generating %s"%fileName) - with open(fileName,"w") as oh: - oh.write(data) - return True - -def mergeConfig(base,other): - try: - customconfig = env.GetProjectOption("custom_config") - except InvalidProjectConfError: - customconfig = None - for bdir in other: - if customconfig and os.path.exists(os.path.join(bdir,customconfig)): - cname=os.path.join(bdir,customconfig) - print("merge custom config {}".format(cname)) - with open(cname,'rb') as ah: - base += json.load(ah) - continue - cname=os.path.join(bdir,"config.json") - if os.path.exists(cname): - print("merge config %s"%cname) - with open(cname,'rb') as ah: - merge=json.load(ah) - base=base+merge - return base - -def replaceTexts(data,replacements): - if replacements is None: - return data - if isinstance(data,str): - for k,v in replacements.items(): - data=data.replace("$"+k,str(v)) - return data - if isinstance(data,list): - rt=[] - for e in data: - rt.append(replaceTexts(e,replacements)) - return rt - if isinstance(data,dict): - rt={} - for k,v in data.items(): - rt[replaceTexts(k,replacements)]=replaceTexts(v,replacements) - return rt - return data -def expandConfig(config): - rt=[] - for item in config: - type=item.get('type') - if type != 'array': - rt.append(item) - continue - replacements=item.get('replace') - children=item.get('children') - name=item.get('name') - if name is None: - name="#unknown#" - if not isinstance(replacements,list): - raise Exception("missing replacements at array %s"%name) - for replace in replacements: - if children is not None: - for c in children: - rt.append(replaceTexts(c,replace)) - return rt - -def generateMergedConfig(inFile,outFile,addDirs=[]): - if not os.path.exists(inFile): - raise Exception("unable to read cfg file %s"%inFile) - data="" - with open(inFile,'rb') as ch: - config=json.load(ch) - config=mergeConfig(config,addDirs) - config=expandConfig(config) - data=json.dumps(config,indent=2) - writeFileIfChanged(outFile,data) - -def generateCfg(inFile,outFile,impl): - if not os.path.exists(inFile): - raise Exception("unable to read cfg file %s"%inFile) - data="" - with open(inFile,'rb') as ch: - config=json.load(ch) - data+="//generated from %s\n"%inFile - l=len(config) - idx=0 - if not impl: - data+='#include "GwConfigItem.h"\n' - data+='class GwConfigDefinitions{\n' - data+=' public:\n' - data+=' int getNumConfig() const{return %d;}\n'%(l) - for item in config: - n=item.get('name') - if n is None: - continue - if len(n) > 15: - raise Exception("%s: config names must be max 15 caracters"%n) - data+=' static constexpr const char* %s="%s";\n'%(n,n) - data+="};\n" - else: - data+='void GwConfigHandler::populateConfigs(GwConfigInterface **config){\n' - for item in config: - name=item.get('name') - if name is None: - continue - data+=' configs[%d]='%(idx) - idx+=1 - secret="false"; - if item.get('type') == 'password': - secret="true" - data+=" new GwConfigInterface(%s,\"%s\",%s);\n"%(name,item.get('default'),secret) - data+='}\n' - writeFileIfChanged(outFile,data) - -def labelFilter(label): - return re.sub("[^a-zA-Z0-9]","",re.sub("\([0-9]*\)","",label)) -def generateXdrMappings(fp,oh,inFile=''): - jdoc=json.load(fp) - oh.write("static GwXDRTypeMapping* typeMappings[]={\n") - first=True - for cat in jdoc: - item=jdoc[cat] - cid=item.get('id') - if cid is None: - continue - tc=item.get('type') - if tc is not None: - if first: - first=False - else: - oh.write(",\n") - oh.write(" new GwXDRTypeMapping(%d,0,%d) /*%s*/"%(cid,tc,cat)) - fields=item.get('fields') - if fields is None: - continue - idx=0 - for fe in fields: - if not isinstance(fe,dict): - continue - tc=fe.get('t') - id=fe.get('v') - if id is None: - id=idx - idx+=1 - l=fe.get('l') or '' - if tc is None or id is None: - continue - if first: - first=False - else: - oh.write(",\n") - oh.write(" new GwXDRTypeMapping(%d,%d,%d) /*%s:%s*/"%(cid,id,tc,cat,l)) - oh.write("\n") - oh.write("};\n") - for cat in jdoc: - item=jdoc[cat] - cid=item.get('id') - if cid is None: - continue - selectors=item.get('selector') - if selectors is not None: - for selector in selectors: - label=selector.get('l') - value=selector.get('v') - if label is not None and value is not None: - label=labelFilter(label) - define=("GWXDRSEL_%s_%s"%(cat,label)).upper() - oh.write(" #define %s %s\n"%(define,value)) - fields=item.get('fields') - if fields is not None: - idx=0 - for field in fields: - v=field.get('v') - if v is None: - v=idx - else: - v=int(v) - label=field.get('l') - if v is not None and label is not None: - define=("GWXDRFIELD_%s_%s"%(cat,labelFilter(label))).upper(); - oh.write(" #define %s %s\n"%(define,str(v))) - idx+=1 - -class Grove: - def __init__(self,name) -> None: - self.name=name - def _ss(self,z=False): - if z: - return self.name - return self.name if self.name != 'Z' else '' - def _suffix(self): - return '_'+self.name if self.name != 'Z' else '' - def replace(self,line): - if line is None: - return line - return line.replace('$G$',self._ss()).replace('$Z$',self._ss(True)).replace('$GS$',self._suffix()) -def generateGroveDefs(inh,outh,inFile=''): - GROVES=[Grove('Z'),Grove('A'),Grove('B'),Grove('C')] - definition=[] - started=False - def writeConfig(): - for grove in GROVES: - for cl in definition: - outh.write(grove.replace(cl)) - - for line in inh: - if re.match(" *#GROVE",line): - started=True - if len(definition) > 0: - writeConfig() - definition=[] - continue - if started: - definition.append(line) - if len(definition) > 0: - writeConfig() - - - -userTaskDirs=[] - -def getUserTaskDirs(): - rt=[] - taskdirs=glob.glob(os.path.join( basePath(),'lib','*task*')) - for task in taskdirs: - rt.append(task) - return rt - -def checkAndAdd(file,names,ilist): - if not file.endswith('.h'): - return - match=False - for cmp in names: - #print("##check %s<->%s"%(f.lower(),cmp)) - if file.lower() == cmp: - match=True - if not match: - return - ilist.append(file) -def genereateUserTasks(outfile): - includes=[] - for task in userTaskDirs: - #print("##taskdir=%s"%task) - base=os.path.basename(task) - includeNames=[base.lower()+".h",'gw'+base.lower()+'.h'] - for f in os.listdir(task): - checkAndAdd(f,includeNames,includes) - includeData="" - for i in includes: - print("#task include %s"%i) - includeData+="#include <%s>\n"%i - writeFileIfChanged(outfile,includeData) - -def generateEmbedded(elist,outFile): - content="" - for entry in elist: - content+="EMBED_GZ_FILE(\"%s\",%s,\"%s\");\n"%entry - writeFileIfChanged(outFile,content) - -def getContentType(fn): - if (fn.endswith('.gz')): - fn=fn[0:-3] - if (fn.endswith('html')): - return "text/html" - if (fn.endswith('json')): - return "application/json" - if (fn.endswith('js')): - return "text/javascript" - if (fn.endswith('css')): - return "text/css" - return "application/octet-stream" - - -def getLibs(): - base=os.path.join(basePath(),"lib") - rt=[] - for sd in os.listdir(base): - if sd == '..': - continue - if sd == '.': - continue - fn=os.path.join(base,sd) - if os.path.isdir(fn): - rt.append(sd) - EXTRAS=['generated'] - for e in EXTRAS: - if not e in rt: - rt.append(e) - return rt - - - -def joinFiles(target,pattern,dirlist): - flist=[] - for dir in dirlist: - fn=os.path.join(dir,pattern) - if os.path.exists(fn): - flist.append(fn) - current=False - if os.path.exists(target): - current=True - for f in flist: - if not isCurrent(f,target): - current=False - break - if current: - print("%s is up to date"%target) - return - print("creating %s"%target) - with gzip.open(target,"wb") as oh: - for fn in flist: - print("adding %s to %s"%(fn,target)) - with open(fn,"rb") as rh: - shutil.copyfileobj(rh,oh) - - -OWNLIBS=getLibs()+["FS","WiFi"] -GLOBAL_INCLUDES=[] - -def handleDeps(env): - #overwrite the GetProjectConfig - #to inject all our libs - oldGetProjectConfig=env.GetProjectConfig - def GetProjectConfigX(env): - rt=oldGetProjectConfig() - cenv="env:"+env['PIOENV'] - libs=[] - for section,options in rt.as_tuple(): - if section == cenv: - for key,values in options: - if key == 'lib_deps': - libs=values - - mustUpdate=False - for lib in OWNLIBS: - if not lib in libs: - libs.append(lib) - mustUpdate=True - if mustUpdate: - update=[(cenv,[('lib_deps',libs)])] - rt.update(update) - return rt - env.AddMethod(GetProjectConfigX,"GetProjectConfig") - #store the list of all includes after we resolved - #the dependencies for our main project - #we will use them for all compilations afterwards - oldLibBuilder=env.ConfigureProjectLibBuilder - def ConfigureProjectLibBuilderX(env): - global GLOBAL_INCLUDES - project=oldLibBuilder() - #print("##ConfigureProjectLibBuilderX") - #pprint.pprint(project) - if project.depbuilders: - #print("##depbuilders %s"%",".join(map(lambda x: x.path,project.depbuilders))) - for db in project.depbuilders: - idirs=db.get_include_dirs() - for id in idirs: - if not id in GLOBAL_INCLUDES: - GLOBAL_INCLUDES.append(id) - return project - env.AddMethod(ConfigureProjectLibBuilderX,"ConfigureProjectLibBuilder") - def injectIncludes(env,node): - return env.Object( - node, - CPPPATH=env["CPPPATH"]+GLOBAL_INCLUDES - ) - env.AddBuildMiddleware(injectIncludes) - - -def prebuild(env): - global userTaskDirs - print("#prebuild running") - if not checkDir(): - sys.exit(1) - ldf_mode=env.GetProjectOption("lib_ldf_mode") - if ldf_mode == 'off': - print("##ldf off - own dependency handling") - handleDeps(env) - userTaskDirs=getUserTaskDirs() - mergedConfig=os.path.join(outPath(),os.path.basename(CFG_FILE)) - generateMergedConfig(os.path.join(basePath(),CFG_FILE),mergedConfig,userTaskDirs) - compressFile(mergedConfig,mergedConfig+".gz") - generateCfg(mergedConfig,os.path.join(outPath(),CFG_INCLUDE),False) - generateCfg(mergedConfig,os.path.join(outPath(),CFG_INCLUDE_IMPL),True) - joinFiles(os.path.join(outPath(),INDEXJS+".gz"),INDEXJS,["web"]+userTaskDirs) - joinFiles(os.path.join(outPath(),INDEXCSS+".gz"),INDEXCSS,["web"]+userTaskDirs) - embedded=getEmbeddedFiles(env) - filedefs=[] - for ef in embedded: - print("#checking embedded file %s"%ef) - (dn,fn)=os.path.split(ef) - pureName=fn - if pureName.endswith('.gz'): - pureName=pureName[0:-3] - ct=getContentType(pureName) - usname=ef.replace('/','_').replace('.','_') - filedefs.append((pureName,usname,ct)) - inFile=os.path.join(basePath(),"web",pureName) - if os.path.exists(inFile): - compressFile(inFile,ef) - else: - print("#WARNING: infile %s for %s not found"%(inFile,ef)) - generateEmbedded(filedefs,os.path.join(outPath(),EMBEDDED_INCLUDE)) - genereateUserTasks(os.path.join(outPath(), TASK_INCLUDE)) - generateFile(os.path.join(basePath(),XDR_FILE),os.path.join(outPath(),XDR_INCLUDE),generateXdrMappings) - generateFile(os.path.join(basePath(),GROVE_CONFIG_IN),os.path.join(outPath(),GROVE_CONFIG),generateGroveDefs,inMode='r') - version="dev"+datetime.now().strftime("%Y%m%d") - env.Append(CPPDEFINES=[('GWDEVVERSION',version)]) - -def cleangenerated(source, target, env): - od=outPath() - if os.path.isdir(od): - print("#cleaning up %s"%od) - for f in os.listdir(od): - if f == "." or f == "..": - continue - fn=os.path.join(od,f) - os.unlink(f) - - -print("#prescript...") -prebuild(env) -board="PLATFORM_BOARD_%s"%env["BOARD"].replace("-","_").upper() -print("Board=#%s#"%board) -print("BuildFlags=%s"%(" ".join(env["BUILD_FLAGS"]))) -env.Append( - LINKFLAGS=[ "-u", "custom_app_desc" ], - CPPDEFINES=[(board,"1")] -) -#script does not run on clean yet - maybe in the future -env.AddPostAction("clean",cleangenerated) - -#look for extra task scripts and include them here -for taskdir in userTaskDirs: - script = os.path.join(taskdir, "extra_task.py") - if os.path.isfile(script): - taskname = os.path.basename(os.path.normpath(taskdir)) - print("#extra task script for '{}'".format(taskname)) - with open(script) as fh: - try: - code = compile(fh.read(), taskname, 'exec') - except SyntaxError: - print("#ERROR: script does not compile") - continue - exec(code) diff --git a/extra_script.py.old b/extra_script.py.old deleted file mode 100644 index ed62c64..0000000 --- a/extra_script.py.old +++ /dev/null @@ -1,518 +0,0 @@ -print("running extra...") -import gzip -import shutil -import os -import sys -import inspect -import json -import glob -from datetime import datetime -import re -import pprint -from platformio.project.config import ProjectConfig - - -Import("env") -#print(env.Dump()) -OWN_FILE="extra_script.py" -GEN_DIR='lib/generated' -CFG_FILE='web/config.json' -XDR_FILE='web/xdrconfig.json' -INDEXJS="index.js" -INDEXCSS="index.css" -CFG_INCLUDE='GwConfigDefinitions.h' -CFG_INCLUDE_IMPL='GwConfigDefImpl.h' -XDR_INCLUDE='GwXdrTypeMappings.h' -TASK_INCLUDE='GwUserTasks.h' -GROVE_CONFIG="GwM5GroveGen.h" -GROVE_CONFIG_IN="lib/hardware/GwM5Grove.in" -EMBEDDED_INCLUDE="GwEmbeddedFiles.h" - -def getEmbeddedFiles(env): - rt=[] - efiles=env.GetProjectOption("board_build.embed_files") - for f in efiles.split("\n"): - if f == '': - continue - rt.append(f) - return rt - -def basePath(): - #see: https://stackoverflow.com/questions/16771894/python-nameerror-global-name-file-is-not-defined - return os.path.dirname(inspect.getfile(lambda: None)) - -def outPath(): - return os.path.join(basePath(),GEN_DIR) -def checkDir(): - dn=outPath() - if not os.path.exists(dn): - os.makedirs(dn) - if not os.path.isdir(dn): - print("unable to create %s"%dn) - return False - return True - -def isCurrent(infile,outfile): - if os.path.exists(outfile): - otime=os.path.getmtime(outfile) - itime=os.path.getmtime(infile) - if (otime >= itime): - own=os.path.join(basePath(),OWN_FILE) - if os.path.exists(own): - owntime=os.path.getmtime(own) - if owntime > otime: - return False - print("%s is newer then %s, no need to recreate"%(outfile,infile)) - return True - return False -def compressFile(inFile,outfile): - if isCurrent(inFile,outfile): - return - print("compressing %s"%inFile) - with open(inFile, 'rb') as f_in: - with gzip.open(outfile, 'wb') as f_out: - shutil.copyfileobj(f_in, f_out) - -def generateFile(infile,outfile,callback,inMode='rb',outMode='w'): - if isCurrent(infile,outfile): - return - print("creating %s"%outfile) - oh=None - with open(infile,inMode) as ch: - with open(outfile,outMode) as oh: - try: - callback(ch,oh,inFile=infile) - oh.close() - except Exception as e: - try: - oh.close() - except: - pass - os.unlink(outfile) - raise - -def writeFileIfChanged(fileName,data): - if os.path.exists(fileName): - with open(fileName,"r") as ih: - old=ih.read() - ih.close() - if old == data: - return False - print("#generating %s"%fileName) - with open(fileName,"w") as oh: - oh.write(data) - return True - -def mergeConfig(base,other): - for bdir in other: - cname=os.path.join(bdir,"config.json") - if os.path.exists(cname): - print("merge config %s"%cname) - with open(cname,'rb') as ah: - merge=json.load(ah) - base=base+merge - return base - -def replaceTexts(data,replacements): - if replacements is None: - return data - if isinstance(data,str): - for k,v in replacements.items(): - data=data.replace("$"+k,str(v)) - return data - if isinstance(data,list): - rt=[] - for e in data: - rt.append(replaceTexts(e,replacements)) - return rt - if isinstance(data,dict): - rt={} - for k,v in data.items(): - rt[replaceTexts(k,replacements)]=replaceTexts(v,replacements) - return rt - return data -def expandConfig(config): - rt=[] - for item in config: - type=item.get('type') - if type != 'array': - rt.append(item) - continue - replacements=item.get('replace') - children=item.get('children') - name=item.get('name') - if name is None: - name="#unknown#" - if not isinstance(replacements,list): - raise Exception("missing replacements at array %s"%name) - for replace in replacements: - if children is not None: - for c in children: - rt.append(replaceTexts(c,replace)) - return rt - -def generateMergedConfig(inFile,outFile,addDirs=[]): - if not os.path.exists(inFile): - raise Exception("unable to read cfg file %s"%inFile) - data="" - with open(inFile,'rb') as ch: - config=json.load(ch) - config=mergeConfig(config,addDirs) - config=expandConfig(config) - data=json.dumps(config,indent=2) - writeFileIfChanged(outFile,data) - -def generateCfg(inFile,outFile,impl): - if not os.path.exists(inFile): - raise Exception("unable to read cfg file %s"%inFile) - data="" - with open(inFile,'rb') as ch: - config=json.load(ch) - data+="//generated from %s\n"%inFile - l=len(config) - idx=0 - if not impl: - data+='#include "GwConfigItem.h"\n' - data+='class GwConfigDefinitions{\n' - data+=' public:\n' - data+=' int getNumConfig() const{return %d;}\n'%(l) - for item in config: - n=item.get('name') - if n is None: - continue - if len(n) > 15: - raise Exception("%s: config names must be max 15 caracters"%n) - data+=' static constexpr const char* %s="%s";\n'%(n,n) - data+="};\n" - else: - data+='void GwConfigHandler::populateConfigs(GwConfigInterface **config){\n' - for item in config: - name=item.get('name') - if name is None: - continue - data+=' configs[%d]='%(idx) - idx+=1 - secret="false"; - if item.get('type') == 'password': - secret="true" - data+=" new GwConfigInterface(%s,\"%s\",%s);\n"%(name,item.get('default'),secret) - data+='}\n' - writeFileIfChanged(outFile,data) - -def labelFilter(label): - return re.sub("[^a-zA-Z0-9]","",re.sub("\([0-9]*\)","",label)) -def generateXdrMappings(fp,oh,inFile=''): - jdoc=json.load(fp) - oh.write("static GwXDRTypeMapping* typeMappings[]={\n") - first=True - for cat in jdoc: - item=jdoc[cat] - cid=item.get('id') - if cid is None: - continue - tc=item.get('type') - if tc is not None: - if first: - first=False - else: - oh.write(",\n") - oh.write(" new GwXDRTypeMapping(%d,0,%d) /*%s*/"%(cid,tc,cat)) - fields=item.get('fields') - if fields is None: - continue - idx=0 - for fe in fields: - if not isinstance(fe,dict): - continue - tc=fe.get('t') - id=fe.get('v') - if id is None: - id=idx - idx+=1 - l=fe.get('l') or '' - if tc is None or id is None: - continue - if first: - first=False - else: - oh.write(",\n") - oh.write(" new GwXDRTypeMapping(%d,%d,%d) /*%s:%s*/"%(cid,id,tc,cat,l)) - oh.write("\n") - oh.write("};\n") - for cat in jdoc: - item=jdoc[cat] - cid=item.get('id') - if cid is None: - continue - selectors=item.get('selector') - if selectors is not None: - for selector in selectors: - label=selector.get('l') - value=selector.get('v') - if label is not None and value is not None: - label=labelFilter(label) - define=("GWXDRSEL_%s_%s"%(cat,label)).upper() - oh.write(" #define %s %s\n"%(define,value)) - fields=item.get('fields') - if fields is not None: - idx=0 - for field in fields: - v=field.get('v') - if v is None: - v=idx - else: - v=int(v) - label=field.get('l') - if v is not None and label is not None: - define=("GWXDRFIELD_%s_%s"%(cat,labelFilter(label))).upper(); - oh.write(" #define %s %s\n"%(define,str(v))) - idx+=1 - -class Grove: - def __init__(self,name) -> None: - self.name=name - def _ss(self,z=False): - if z: - return self.name - return self.name if self.name is not 'Z' else '' - def _suffix(self): - return '_'+self.name if self.name is not 'Z' else '' - def replace(self,line): - if line is None: - return line - return line.replace('$G$',self._ss()).replace('$Z$',self._ss(True)).replace('$GS$',self._suffix()) -def generateGroveDefs(inh,outh,inFile=''): - GROVES=[Grove('Z'),Grove('A'),Grove('B'),Grove('C')] - definition=[] - started=False - def writeConfig(): - for grove in GROVES: - for cl in definition: - outh.write(grove.replace(cl)) - - for line in inh: - if re.match(" *#GROVE",line): - started=True - if len(definition) > 0: - writeConfig() - definition=[] - continue - if started: - definition.append(line) - if len(definition) > 0: - writeConfig() - - - -userTaskDirs=[] - -def getUserTaskDirs(): - rt=[] - taskdirs=glob.glob(os.path.join( basePath(),'lib','*task*')) - for task in taskdirs: - rt.append(task) - return rt - -def checkAndAdd(file,names,ilist): - if not file.endswith('.h'): - return - match=False - for cmp in names: - #print("##check %s<->%s"%(f.lower(),cmp)) - if file.lower() == cmp: - match=True - if not match: - return - ilist.append(file) -def genereateUserTasks(outfile): - includes=[] - for task in userTaskDirs: - #print("##taskdir=%s"%task) - base=os.path.basename(task) - includeNames=[base.lower()+".h",'gw'+base.lower()+'.h'] - for f in os.listdir(task): - checkAndAdd(f,includeNames,includes) - includeData="" - for i in includes: - print("#task include %s"%i) - includeData+="#include <%s>\n"%i - writeFileIfChanged(outfile,includeData) - -def generateEmbedded(elist,outFile): - content="" - for entry in elist: - content+="EMBED_GZ_FILE(\"%s\",%s,\"%s\");\n"%entry - writeFileIfChanged(outFile,content) - -def getContentType(fn): - if (fn.endswith('.gz')): - fn=fn[0:-3] - if (fn.endswith('html')): - return "text/html" - if (fn.endswith('json')): - return "application/json" - if (fn.endswith('js')): - return "text/javascript" - if (fn.endswith('css')): - return "text/css" - return "application/octet-stream" - - -def getLibs(): - base=os.path.join(basePath(),"lib") - rt=[] - for sd in os.listdir(base): - if sd == '..': - continue - if sd == '.': - continue - fn=os.path.join(base,sd) - if os.path.isdir(fn): - rt.append(sd) - EXTRAS=['generated'] - for e in EXTRAS: - if not e in rt: - rt.append(e) - return rt - - - -def joinFiles(target,pattern,dirlist): - flist=[] - for dir in dirlist: - fn=os.path.join(dir,pattern) - if os.path.exists(fn): - flist.append(fn) - current=False - if os.path.exists(target): - current=True - for f in flist: - if not isCurrent(f,target): - current=False - break - if current: - print("%s is up to date"%target) - return - print("creating %s"%target) - with gzip.open(target,"wb") as oh: - for fn in flist: - print("adding %s to %s"%(fn,target)) - with open(fn,"rb") as rh: - shutil.copyfileobj(rh,oh) - - -OWNLIBS=getLibs()+["FS","WiFi"] -GLOBAL_INCLUDES=[] - -def handleDeps(env): - #overwrite the GetProjectConfig - #to inject all our libs - oldGetProjectConfig=env.GetProjectConfig - def GetProjectConfigX(env): - rt=oldGetProjectConfig() - cenv="env:"+env['PIOENV'] - libs=[] - for section,options in rt.as_tuple(): - if section == cenv: - for key,values in options: - if key == 'lib_deps': - libs=values - - mustUpdate=False - for lib in OWNLIBS: - if not lib in libs: - libs.append(lib) - mustUpdate=True - if mustUpdate: - update=[(cenv,[('lib_deps',libs)])] - rt.update(update) - return rt - env.AddMethod(GetProjectConfigX,"GetProjectConfig") - #store the list of all includes after we resolved - #the dependencies for our main project - #we will use them for all compilations afterwards - oldLibBuilder=env.ConfigureProjectLibBuilder - def ConfigureProjectLibBuilderX(env): - global GLOBAL_INCLUDES - project=oldLibBuilder() - #print("##ConfigureProjectLibBuilderX") - #pprint.pprint(project) - if project.depbuilders: - #print("##depbuilders %s"%",".join(map(lambda x: x.path,project.depbuilders))) - for db in project.depbuilders: - idirs=db.get_include_dirs() - for id in idirs: - if not id in GLOBAL_INCLUDES: - GLOBAL_INCLUDES.append(id) - return project - env.AddMethod(ConfigureProjectLibBuilderX,"ConfigureProjectLibBuilder") - def injectIncludes(env,node): - return env.Object( - node, - CPPPATH=env["CPPPATH"]+GLOBAL_INCLUDES - ) - env.AddBuildMiddleware(injectIncludes) - - -def prebuild(env): - global userTaskDirs - print("#prebuild running") - if not checkDir(): - sys.exit(1) - ldf_mode=env.GetProjectOption("lib_ldf_mode") - if ldf_mode == 'off': - print("##ldf off - own dependency handling") - handleDeps(env) - userTaskDirs=getUserTaskDirs() - mergedConfig=os.path.join(outPath(),os.path.basename(CFG_FILE)) - generateMergedConfig(os.path.join(basePath(),CFG_FILE),mergedConfig,userTaskDirs) - compressFile(mergedConfig,mergedConfig+".gz") - generateCfg(mergedConfig,os.path.join(outPath(),CFG_INCLUDE),False) - generateCfg(mergedConfig,os.path.join(outPath(),CFG_INCLUDE_IMPL),True) - joinFiles(os.path.join(outPath(),INDEXJS+".gz"),INDEXJS,["web"]+userTaskDirs) - joinFiles(os.path.join(outPath(),INDEXCSS+".gz"),INDEXCSS,["web"]+userTaskDirs) - embedded=getEmbeddedFiles(env) - filedefs=[] - for ef in embedded: - print("#checking embedded file %s"%ef) - (dn,fn)=os.path.split(ef) - pureName=fn - if pureName.endswith('.gz'): - pureName=pureName[0:-3] - ct=getContentType(pureName) - usname=ef.replace('/','_').replace('.','_') - filedefs.append((pureName,usname,ct)) - inFile=os.path.join(basePath(),"web",pureName) - if os.path.exists(inFile): - compressFile(inFile,ef) - else: - print("#WARNING: infile %s for %s not found"%(inFile,ef)) - generateEmbedded(filedefs,os.path.join(outPath(),EMBEDDED_INCLUDE)) - genereateUserTasks(os.path.join(outPath(), TASK_INCLUDE)) - generateFile(os.path.join(basePath(),XDR_FILE),os.path.join(outPath(),XDR_INCLUDE),generateXdrMappings) - generateFile(os.path.join(basePath(),GROVE_CONFIG_IN),os.path.join(outPath(),GROVE_CONFIG),generateGroveDefs,inMode='r') - version="dev"+datetime.now().strftime("%Y%m%d") - env.Append(CPPDEFINES=[('GWDEVVERSION',version)]) - -def cleangenerated(source, target, env): - od=outPath() - if os.path.isdir(od): - print("#cleaning up %s"%od) - for f in os.listdir(od): - if f == "." or f == "..": - continue - fn=os.path.join(od,f) - os.unlink(f) - - -print("#prescript...") -prebuild(env) -board="PLATFORM_BOARD_%s"%env["BOARD"].replace("-","_").upper() -print("Board=#%s#"%board) -print("BuildFlags=%s"%(" ".join(env["BUILD_FLAGS"]))) -env.Append( - LINKFLAGS=[ "-u", "custom_app_desc" ], - CPPDEFINES=[(board,"1")] -) -#script does not run on clean yet - maybe in the future -env.AddPostAction("clean",cleangenerated) From d516c820416a5a61f2149429d2f1c8180ac9166f Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Thu, 5 Feb 2026 16:32:15 +0100 Subject: [PATCH 107/121] New clock page with extended functions --- lib/obp60task/PageClock.cpp | 834 ++++++++++++++++++++--------- lib/obp60task/PageClock.old | 467 ++++++++++++++++ lib/obp60task/PageClock2.new | 548 +++++++++++++++++++ lib/obp60task/PageClock3.new | 777 +++++++++++++++++++++++++++ lib/obp60task/PageClockDigital.new | 224 ++++++++ 5 files changed, 2591 insertions(+), 259 deletions(-) create mode 100644 lib/obp60task/PageClock.old create mode 100644 lib/obp60task/PageClock2.new create mode 100644 lib/obp60task/PageClock3.new create mode 100644 lib/obp60task/PageClockDigital.new diff --git a/lib/obp60task/PageClock.cpp b/lib/obp60task/PageClock.cpp index 7c7b74f..3cf097a 100644 --- a/lib/obp60task/PageClock.cpp +++ b/lib/obp60task/PageClock.cpp @@ -4,95 +4,264 @@ #include "OBP60Extensions.h" /* - * TODO mode: race timer: keys - * - prepare: set countdown to 5min - * reset: abort current countdown and start over with 5min preparation - * - 5min: key press - * - 4min: key press to sync - * - 1min: buzzer signal - * - start: buzzer signal for start + * PageClock: Clock page with + * - Analog mode (mode == 'A') + * - Digital mode (mode == 'D') + * - Countdown timer mode (mode == 'T') + * - Keys in mode analog and digital clock: + * K1: MODE (A/D/T) + * K2: POS (select field: HH / MM / SS) + * K3: + * K4: + * K5: TZ (Local/UTC) * + * Timer mode: + * - Format HH:MM:SS (24h, leading zeros) + * - Keys in timer mode: + * K1: MODE (A/D/T) + * K2: POS (select field: HH / MM / SS) + * K3: + (increment selected field) + * K4: - (decrement selected field) + * K5: RUN (start/stop countdown) + * - Selection marker: line under active field (width 2px, not wider than digits) + * - Editing only possible when timer is not running + * - When page is left, running timer continues in background using RTC time + * (on re-entry, remaining time is recalculated from RTC) */ class PageClock : public Page { -bool simulation = false; -int simtime; -bool keylock = false; + bool simulation = false; + int simtime; + bool keylock = false; #ifdef BOARD_OBP60S3 -char source = 'G'; // time source (R)TC | (G)PS | (N)TP + char source = 'G'; // time source (R)TC | (G)PS | (N)TP #endif #ifdef BOARD_OBP40S3 -char source = 'R'; // time source (R)TC | (G)PS | (N)TP + char source = 'R'; // time source (R)TC | (G)PS | (N)TP #endif -char mode = 'A'; // display mode (A)nalog | (D)igital | race (T)imer -char tz = 'L'; // time zone (L)ocal | (U)TC -double timezone = 0; // there are timezones with non int offsets, e.g. 5.5 or 5.75 -double homelat; -double homelon; -bool homevalid = false; // homelat and homelon are valid + char mode = 'A'; // display mode (A)nalog | (D)igital | race (T)imer + char tz = 'L'; // time zone (L)ocal | (U)TC + double timezone = 0; // there are timezones with non int offsets, e.g. 5.5 or 5.75 + double homelat; + double homelon; + bool homevalid = false; // homelat and homelon are valid - public: - PageClock(CommonData &common){ + // Timer state (static so it survives page switches) + static bool timerInitialized; + static bool timerRunning; + static int timerHours; + static int timerMinutes; + static int timerSeconds; + static int selectedField; // 0 = hours, 1 = minutes, 2 = seconds + static bool showSelectionMarker; + static time_t timerEndEpoch; // absolute end time based on RTC + + void setupTimerDefaults() + { + if (!timerInitialized) { + timerInitialized = true; + timerRunning = false; + timerHours = 0; + timerMinutes = 0; + timerSeconds = 0; + selectedField = 0; + showSelectionMarker = true; + timerEndEpoch = 0; + } + } + + static int clamp(int value, int minVal, int maxVal) + { + if (value < minVal) return minVal; + if (value > maxVal) return maxVal; + return value; + } + + void incrementSelected() + { + if (selectedField == 0) { + timerHours = clamp(timerHours + 1, 0, 23); + } else if (selectedField == 1) { + timerMinutes = clamp(timerMinutes + 1, 0, 59); + } else { + timerSeconds = clamp(timerSeconds + 1, 0, 59); + } + } + + void decrementSelected() + { + if (selectedField == 0) { + timerHours = clamp(timerHours - 1, 0, 23); + } else if (selectedField == 1) { + timerMinutes = clamp(timerMinutes - 1, 0, 59); + } else { + timerSeconds = clamp(timerSeconds - 1, 0, 59); + } + } + + int totalTimerSeconds() const + { + return timerHours * 3600 + timerMinutes * 60 + timerSeconds; + } + +public: + PageClock(CommonData& common) + { commonData = &common; - common.logger->logDebug(GwLog::LOG,"Instantiate PageClock"); + common.logger->logDebug(GwLog::LOG, "Instantiate PageClock"); simulation = common.config->getBool(common.config->useSimuData); timezone = common.config->getString(common.config->timeZone).toDouble(); homelat = common.config->getString(common.config->homeLAT).toDouble(); homelon = common.config->getString(common.config->homeLON).toDouble(); homevalid = homelat >= -180.0 and homelat <= 180 and homelon >= -90.0 and homelon <= 90.0; simtime = 38160; // time value 11:36 + setupTimerDefaults(); } - virtual void setupKeys(){ + virtual void setupKeys() + { Page::setupKeys(); - commonData->keydata[0].label = "SRC"; - commonData->keydata[1].label = "MODE"; - commonData->keydata[4].label = "TZ"; + + if (mode == 'T') { + // Timer mode: MODE, POS, +, -, RUN + commonData->keydata[0].label = "MODE"; + commonData->keydata[1].label = "POS"; + commonData->keydata[2].label = "+"; + commonData->keydata[3].label = "-"; + commonData->keydata[4].label = "RUN"; + } else { + // Clock modes: like original + commonData->keydata[0].label = "SRC"; + commonData->keydata[1].label = "MODE"; + commonData->keydata[4].label = "TZ"; + } } // Key functions - virtual int handleKey(int key){ - // Time source - if (key == 1) { - switch (source) { - case 'G': source = 'R'; break; - case 'R': source = 'G'; break; - default: source = 'G'; break; - } - return 0; + virtual int handleKey(int key) + { + setupTimerDefaults(); + + // Keylock function + if (key == 11) { // Code for keylock + keylock = !keylock; // Toggle keylock + return 0; // Commit the key } - if (key == 2) { - switch (mode) { + + if (mode == 'T') { + // Timer mode key handling + + // MODE (K1): cycle display mode A/D/T + if (key == 1) { + switch (mode) { case 'A': mode = 'D'; break; case 'D': mode = 'T'; break; case 'T': mode = 'A'; break; default: mode = 'A'; break; + } + setupKeys(); + return 0; } - return 0; + + // POS (K2): select field HH / MM / SS (only if timer not running) + if (key == 2 && !timerRunning) { + selectedField = (selectedField + 1) % 3; + showSelectionMarker = true; + return 0; + } + + // + (K3): increment selected field (only if timer not running) + if (key == 3 && !timerRunning) { + incrementSelected(); + return 0; + } + + // - (K4): decrement selected field (only if timer not running) + if (key == 4 && !timerRunning) { + decrementSelected(); + return 0; + } + + // RUN (K5): start/stop timer + if (key == 5) { + if (!timerRunning) { + // Start timer if a non-zero duration is set + int total = totalTimerSeconds(); + if (total > 0 && commonData->data.rtcValid) { + struct tm rtcCopy = commonData->data.rtcTime; + time_t nowEpoch = mktime(&rtcCopy); + timerEndEpoch = nowEpoch + total; + timerRunning = true; + showSelectionMarker = false; + } + } else { + // Stop timer: compute remaining time and keep as new setting + if (commonData->data.rtcValid) { + struct tm rtcCopy = commonData->data.rtcTime; + time_t nowEpoch = mktime(&rtcCopy); + time_t remaining = timerEndEpoch - nowEpoch; + if (remaining < 0) remaining = 0; + int rem = static_cast(remaining); + timerHours = rem / 3600; + rem -= timerHours * 3600; + timerMinutes = rem / 60; + timerSeconds = rem % 60; + } + timerRunning = false; + // marker will become visible again only after POS press + } + return 0; + } + + // In timer mode, other keys are passed through + return key; } - // Time zone: Local / UTC - if (key == 5) { - switch (tz) { - case 'L': tz = 'U'; break; - case 'U': tz = 'L'; break; - default: tz = 'L'; break; + + // Clock (A/D) modes key handling – like original PageClock + + // Time source (K1) + if (key == 1) { + switch (source) { + case 'G': source = 'R'; break; + case 'R': source = 'G'; break; + default: source = 'G'; break; } return 0; } - // Keylock function - if(key == 11){ // Code for keylock - keylock = !keylock; // Toggle keylock - return 0; // Commit the key + // MODE (K2) + if (key == 2) { + switch (mode) { + case 'A': mode = 'D'; break; + case 'D': mode = 'T'; break; + case 'T': mode = 'A'; break; + default: mode = 'A'; break; + } + setupKeys(); + return 0; } + + // Time zone: Local / UTC (K5) + if (key == 5) { + switch (tz) { + case 'L': tz = 'U'; break; + case 'U': tz = 'L'; break; + default: tz = 'L'; break; + } + return 0; + } + return key; } - int displayPage(PageData &pageData) + int displayPage(PageData& pageData) { - GwConfigHandler *config = commonData->config; - GwLog *logger = commonData->logger; + GwConfigHandler* config = commonData->config; + GwLog* logger = commonData->logger; + + setupTimerDefaults(); + setupKeys(); // ensure correct key labels for current mode static String svalue1old = ""; static String unit1old = ""; @@ -116,58 +285,57 @@ bool homevalid = false; // homelat and homelon are valid String backlightMode = config->getString(config->backlight); // Get boat values for GPS time - GwApi::BoatValue *bvalue1 = pageData.values[0]; // First element in list (only one value by PageOneValue) + GwApi::BoatValue* bvalue1 = pageData.values[0]; // First element in list String name1 = bvalue1->getName().c_str(); // Value name name1 = name1.substring(0, 6); // String length limit for value name - if(simulation == false){ + if (simulation == false) { value1 = bvalue1->value; // Value as double in SI unit - } - else{ + } else { value1 = simtime++; // Simulation data for time value 11:36 in seconds } // Other simulation data see OBP60Formatter.cpp - bool valid1 = bvalue1->valid; // Valid information - String svalue1 = formatValue(bvalue1, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places + bool valid1 = bvalue1->valid; // Valid information + String svalue1 = formatValue(bvalue1, *commonData).svalue; // Formatted value String unit1 = formatValue(bvalue1, *commonData).unit; // Unit of value - if(valid1 == true){ + if (valid1 == true) { svalue1old = svalue1; // Save old value unit1old = unit1; // Save old unit } // Get boat values for GPS date - GwApi::BoatValue *bvalue2 = pageData.values[1]; // Second element in list (only one value by PageOneValue) + GwApi::BoatValue* bvalue2 = pageData.values[1]; // Second element in list String name2 = bvalue2->getName().c_str(); // Value name name2 = name2.substring(0, 6); // String length limit for value name value2 = bvalue2->value; // Value as double in SI unit - bool valid2 = bvalue2->valid; // Valid information - String svalue2 = formatValue(bvalue2, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places + bool valid2 = bvalue2->valid; // Valid information + String svalue2 = formatValue(bvalue2, *commonData).svalue; // Formatted value String unit2 = formatValue(bvalue2, *commonData).unit; // Unit of value - if(valid2 == true){ + if (valid2 == true) { svalue2old = svalue2; // Save old value unit2old = unit2; // Save old unit } - // Get boat values for HDOP date - GwApi::BoatValue *bvalue3 = pageData.values[2]; // Third element in list (only one value by PageOneValue) + // Get boat values for HDOP + GwApi::BoatValue* bvalue3 = pageData.values[2]; // Third element in list String name3 = bvalue3->getName().c_str(); // Value name name3 = name3.substring(0, 6); // String length limit for value name value3 = bvalue3->value; // Value as double in SI unit - bool valid3 = bvalue3->valid; // Valid information - String svalue3 = formatValue(bvalue3, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places + bool valid3 = bvalue3->valid; // Valid information + String svalue3 = formatValue(bvalue3, *commonData).svalue; // Formatted value String unit3 = formatValue(bvalue3, *commonData).unit; // Unit of value - if(valid3 == true){ + if (valid3 == true) { svalue3old = svalue3; // Save old value unit3old = unit3; // Save old unit } // Optical warning by limit violation (unused) - if(String(flashLED) == "Limit Violation"){ + if (String(flashLED) == "Limit Violation") { setBlinkingLED(false); setFlashLED(false); } // Logging boat values - if (bvalue1 == NULL) return PAGE_OK; // WTF why this statement? - LOG_DEBUG(GwLog::LOG,"Drawing at PageClock, %s:%f, %s:%f", name1.c_str(), value1, name2.c_str(), value2); + if (bvalue1 == NULL) return PAGE_OK; + LOG_DEBUG(GwLog::LOG, "Drawing at PageClock, %s:%f, %s:%f", name1.c_str(), value1, name2.c_str(), value2); // Draw page //*********************************************************** @@ -178,23 +346,22 @@ bool homevalid = false; // homelat and homelon are valid getdisplay().setTextColor(commonData->fgcolor); time_t tv = mktime(&commonData->data.rtcTime) + timezone * 3600; - struct tm *local_tm = localtime(&tv); + struct tm* local_tm = localtime(&tv); // Show values GPS date getdisplay().setFont(&Ubuntu_Bold8pt8b); getdisplay().setCursor(10, 65); if (holdvalues == false) { if (source == 'G') { - // GPS value - getdisplay().print(svalue2); + // GPS value + getdisplay().print(svalue2); } else if (commonData->data.rtcValid) { // RTC value - if (tz == 'L') { - getdisplay().print(formatDate(dateformat, local_tm->tm_year + 1900, local_tm->tm_mon + 1, local_tm->tm_mday)); - } - else { - getdisplay().print(formatDate(dateformat, commonData->data.rtcTime.tm_year + 1900, commonData->data.rtcTime.tm_mon + 1, commonData->data.rtcTime.tm_mday)); - } + if (tz == 'L') { + getdisplay().print(formatDate(dateformat, local_tm->tm_year + 1900, local_tm->tm_mon + 1, local_tm->tm_mday)); + } else { + getdisplay().print(formatDate(dateformat, commonData->data.rtcTime.tm_year + 1900, commonData->data.rtcTime.tm_mon + 1, commonData->data.rtcTime.tm_mday)); + } } else { getdisplay().print("---"); } @@ -205,29 +372,26 @@ bool homevalid = false; // homelat and homelon are valid getdisplay().setCursor(10, 95); getdisplay().print("Date"); // Name - // Horizintal separator left + // Horizontal separator left getdisplay().fillRect(0, 149, 60, 3, commonData->fgcolor); - // Show values GPS time + // Show values GPS time (small text bottom left) getdisplay().setFont(&Ubuntu_Bold8pt8b); getdisplay().setCursor(10, 250); if (holdvalues == false) { if (source == 'G') { getdisplay().print(svalue1); // Value - } - else if (commonData->data.rtcValid) { - if (tz == 'L') { - getdisplay().print(formatTime('s', local_tm->tm_hour, local_tm->tm_min, local_tm->tm_sec)); - } - else { - getdisplay().print(formatTime('s', commonData->data.rtcTime.tm_hour, commonData->data.rtcTime.tm_min, commonData->data.rtcTime.tm_sec)); - } + } else if (commonData->data.rtcValid) { + if (tz == 'L') { + getdisplay().print(formatTime('s', local_tm->tm_hour, local_tm->tm_min, local_tm->tm_sec)); + } else { + getdisplay().print(formatTime('s', commonData->data.rtcTime.tm_hour, commonData->data.rtcTime.tm_min, commonData->data.rtcTime.tm_sec)); + } } else { getdisplay().print("---"); } - } - else { - getdisplay().print(svalue1old); + } else { + getdisplay().print(svalue1old); } getdisplay().setFont(&Ubuntu_Bold12pt8b); getdisplay().setCursor(10, 220); @@ -244,19 +408,19 @@ bool homevalid = false; // homelat and homelon are valid getdisplay().setFont(&Ubuntu_Bold8pt8b); getdisplay().setCursor(335, 65); - if(holdvalues == false) getdisplay().print(sunrise); // Value + if (holdvalues == false) getdisplay().print(sunrise); // Value else getdisplay().print(svalue5old); getdisplay().setFont(&Ubuntu_Bold12pt8b); getdisplay().setCursor(335, 95); getdisplay().print("SunR"); // Name - // Horizintal separator right + // Horizontal separator right getdisplay().fillRect(340, 149, 80, 3, commonData->fgcolor); // Show values sunset String sunset = "---"; if ((valid1 and valid2 and valid3 == true) or (homevalid and commonData->data.rtcValid)) { - sunset = String(commonData->sundata.sunsetHour) + ":" + String(commonData->sundata.sunsetMinute + 100).substring(1); + sunset = String(commonData->sundata.sunsetHour) + ":" + String(commonData->sundata.sunsetMinute + 100).substring(1); svalue6old = sunset; } else if (simulation) { sunset = String("21:03"); @@ -264,204 +428,356 @@ bool homevalid = false; // homelat and homelon are valid getdisplay().setFont(&Ubuntu_Bold8pt8b); getdisplay().setCursor(335, 250); - if(holdvalues == false) getdisplay().print(sunset); // Value + if (holdvalues == false) getdisplay().print(sunset); // Value else getdisplay().print(svalue6old); getdisplay().setFont(&Ubuntu_Bold12pt8b); getdisplay().setCursor(335, 220); getdisplay().print("SunS"); // Name -//******************************************************************************************* + //******************************************************************************************* - // Draw clock - int rInstrument = 110; // Radius of clock - float pi = 3.141592; + if (mode == 'T') { + // TIMER MODE: countdown timer HH:MM:SS in the center with 7-segment font - getdisplay().fillCircle(200, 150, rInstrument + 10, commonData->fgcolor); // Outer circle - getdisplay().fillCircle(200, 150, rInstrument + 7, commonData->bgcolor); // Outer circle + int dispH = timerHours; + int dispM = timerMinutes; + int dispS = timerSeconds; - for(int i=0; i<360; i=i+1) - { - // Scaling values - float x = 200 + (rInstrument-30)*sin(i/180.0*pi); // x-coordinate dots - float y = 150 - (rInstrument-30)*cos(i/180.0*pi); // y-coordinate cots - const char *ii = ""; - switch (i) - { - case 0: ii="12"; break; - case 30 : ii=""; break; - case 60 : ii=""; break; - case 90 : ii="3"; break; - case 120 : ii=""; break; - case 150 : ii=""; break; - case 180 : ii="6"; break; - case 210 : ii=""; break; - case 240 : ii=""; break; - case 270 : ii="9"; break; - case 300 : ii=""; break; - case 330 : ii=""; break; - default: break; + // Update remaining time if timer is running (based on RTC) + if (timerRunning && commonData->data.rtcValid) { + struct tm rtcCopy = commonData->data.rtcTime; + time_t nowEpoch = mktime(&rtcCopy); + time_t remaining = timerEndEpoch - nowEpoch; + if (remaining <= 0) { + remaining = 0; + timerRunning = false; + } + int rem = static_cast(remaining); + dispH = rem / 3600; + rem -= dispH * 3600; + dispM = rem / 60; + dispS = rem % 60; } - // Print text centered on position x, y - int16_t x1, y1; // Return values of getTextBounds - uint16_t w, h; // Return values of getTextBounds - getdisplay().getTextBounds(ii, int(x), int(y), &x1, &y1, &w, &h); // Calc width of new string - getdisplay().setCursor(x-w/2, y+h/2); - if(i % 30 == 0){ - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().print(ii); + char buf[9]; // "HH:MM:SS" + snprintf(buf, sizeof(buf), "%02d:%02d:%02d", dispH, dispM, dispS); + String timeStr = String(buf); + + // Clear central area and draw large digital time + getdisplay().fillRect(0, 110, getdisplay().width(), 80, commonData->bgcolor); + + getdisplay().setFont(&DSEG7Classic_BoldItalic60pt7b); + + // Determine widths for digits and colon to position selection underline exactly + int16_t x0, y0; + uint16_t wDigit, hDigit; + uint16_t wColon, hColon; + + getdisplay().getTextBounds("00", 0, 0, &x0, &y0, &wDigit, &hDigit); + getdisplay().getTextBounds(":", 0, 0, &x0, &y0, &wColon, &hColon); + + uint16_t totalWidth = 3 * wDigit + 2 * wColon; + + int16_t baseX = (static_cast(getdisplay().width()) - static_cast(totalWidth)) / 2; + int16_t centerY = 150; + + // Draw time string centered + int16_t x1b, y1b; + uint16_t wb, hb; + getdisplay().getTextBounds(timeStr, 0, 0, &x1b, &y1b, &wb, &hb); + int16_t textX = (static_cast(getdisplay().width()) - static_cast(wb)) / 2; + int16_t textY = centerY + hb / 2; + + getdisplay().setCursor(textX, textY); + getdisplay().print(timeStr); + + // Selection marker (only visible when not running and POS pressed) + if (!timerRunning && showSelectionMarker) { + int16_t selX = baseX; + if (selectedField == 1) { + selX = baseX + wDigit + wColon; // minutes start + } else if (selectedField == 2) { + selX = baseX + 2 * wDigit + 2 * wColon; // seconds start + } + + int16_t underlineY = centerY + hb / 2 + 2; + getdisplay().fillRect(selX, underlineY, wDigit, 2, commonData->fgcolor); } - // Draw sub scale with dots - float sinx = 0; - float cosx = 0; - if(i % 6 == 0){ - float x1c = 200 + rInstrument*sin(i/180.0*pi); - float y1c = 150 - rInstrument*cos(i/180.0*pi); - getdisplay().fillCircle((int)x1c, (int)y1c, 2, commonData->fgcolor); - sinx=sin(i/180.0*pi); - cosx=cos(i/180.0*pi); - } - - // Draw sub scale with lines (two triangles) - if(i % 30 == 0){ - float dx=2; // Line thickness = 2*dx+1 - float xx1 = -dx; - float xx2 = +dx; - float yy1 = -(rInstrument-10); - float yy2 = -(rInstrument+10); - getdisplay().fillTriangle(200+(int)(cosx*xx1-sinx*yy1),150+(int)(sinx*xx1+cosx*yy1), - 200+(int)(cosx*xx2-sinx*yy1),150+(int)(sinx*xx2+cosx*yy1), - 200+(int)(cosx*xx1-sinx*yy2),150+(int)(sinx*xx1+cosx*yy2),commonData->fgcolor); - getdisplay().fillTriangle(200+(int)(cosx*xx2-sinx*yy1),150+(int)(sinx*xx2+cosx*yy1), - 200+(int)(cosx*xx1-sinx*yy2),150+(int)(sinx*xx1+cosx*yy2), - 200+(int)(cosx*xx2-sinx*yy2),150+(int)(sinx*xx2+cosx*yy2),commonData->fgcolor); - } - } - - // Print Unit in clock - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(175, 110); - if(holdvalues == false){ - getdisplay().print(tz == 'L' ? "LOT" : "UTC"); - } - else{ - getdisplay().print(unit2old); // date unit - } - - getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(185, 190); - if (source == 'G') { - getdisplay().print("GPS"); - } else { - getdisplay().print("RTC"); - } - - // Clock values - double hour = 0; - double minute = 0; - if (source == 'R') { - if (tz == 'L') { - time_t tv = mktime(&commonData->data.rtcTime) + timezone * 3600; - struct tm *local_tm = localtime(&tv); - minute = local_tm->tm_min; - hour = local_tm->tm_hour; + // Small indicators: timezone and source + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(180, 110); + if (holdvalues == false) { + getdisplay().print(tz == 'L' ? "LOT" : "UTC"); } else { - minute = commonData->data.rtcTime.tm_min; - hour = commonData->data.rtcTime.tm_hour; + getdisplay().print(unit2old); // date unit } - hour += minute / 60; + + getdisplay().setCursor(185, 190); + if (source == 'G') { + getdisplay().print("GPS"); + } else { + getdisplay().print("RTC"); + } + + } else if (mode == 'D') { + // DIGITAL CLOCK MODE: large 7-segment time based on GPS/RTC + + int hour24 = 0; + int minute24 = 0; + int second24 = 0; + + if (source == 'R' && commonData->data.rtcValid) { + time_t tv2 = mktime(&commonData->data.rtcTime); + if (tz == 'L') { + tv2 += static_cast(timezone * 3600); + } + struct tm* tm2 = localtime(&tv2); + hour24 = tm2->tm_hour; + minute24 = tm2->tm_min; + second24 = tm2->tm_sec; + } else { + double t = value1; + if (tz == 'L') { + t += timezone * 3600; + } + if (t >= 86400) t -= 86400; + if (t < 0) t += 86400; + hour24 = static_cast(t / 3600.0); + int rest = static_cast(t) - hour24 * 3600; + minute24 = rest / 60; + second24 = rest % 60; + } + + char buf[9]; // "HH:MM:SS" + snprintf(buf, sizeof(buf), "%02d:%02d:%02d", hour24, minute24, second24); + String timeStr = String(buf); + + getdisplay().fillRect(0, 110, getdisplay().width(), 80, commonData->bgcolor); + + getdisplay().setFont(&DSEG7Classic_BoldItalic60pt7b); + + int16_t x1b, y1b; + uint16_t wb, hb; + getdisplay().getTextBounds(timeStr, 0, 0, &x1b, &y1b, &wb, &hb); + + int16_t x = (static_cast(getdisplay().width()) - static_cast(wb)) / 2; + int16_t y = 150 + hb / 2; + + getdisplay().setCursor(x, y); + getdisplay().print(timeStr); + + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(180, 110); + if (holdvalues == false) { + getdisplay().print(tz == 'L' ? "LOT" : "UTC"); + } else { + getdisplay().print(unit2old); // date unit + } + + getdisplay().setCursor(185, 190); + if (source == 'G') { + getdisplay().print("GPS"); + } else { + getdisplay().print("RTC"); + } + } else { - if (tz == 'L') { - value1 += timezone * 3600; + // ANALOG CLOCK MODE (mode == 'A') + + int rInstrument = 110; // Radius of clock + float pi = 3.141592; + + getdisplay().fillCircle(200, 150, rInstrument + 10, commonData->fgcolor); // Outer circle + getdisplay().fillCircle(200, 150, rInstrument + 7, commonData->bgcolor); // Outer circle + + for (int i = 0; i < 360; i = i + 1) + { + // Scaling values + float x = 200 + (rInstrument - 30) * sin(i / 180.0 * pi); // x-coordinate dots + float y = 150 - (rInstrument - 30) * cos(i / 180.0 * pi); // y-coordinate dots + const char* ii = ""; + switch (i) + { + case 0: ii = "12"; break; + case 90: ii = "3"; break; + case 180: ii = "6"; break; + case 270: ii = "9"; break; + default: break; + } + + // Print text centered on position x, y + int16_t x1c, y1c; // Return values of getTextBounds + uint16_t wc, hc; // Return values of getTextBounds + getdisplay().getTextBounds(ii, int(x), int(y), &x1c, &y1c, &wc, &hc); // Calc width of new string + getdisplay().setCursor(x - wc / 2, y + hc / 2); + if (i % 90 == 0) { + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().print(ii); + } + + // Draw sub scale with dots + float sinx = 0; + float cosx = 0; + if (i % 6 == 0) { + float x1d = 200 + rInstrument * sin(i / 180.0 * pi); + float y1d = 150 - rInstrument * cos(i / 180.0 * pi); + getdisplay().fillCircle((int)x1d, (int)y1d, 2, commonData->fgcolor); + sinx = sin(i / 180.0 * pi); + cosx = cos(i / 180.0 * pi); + } + + // Draw sub scale with lines (two triangles) + if (i % 30 == 0) { + float dx = 2; // Line thickness = 2*dx+1 + float xx1 = -dx; + float xx2 = +dx; + float yy1 = -(rInstrument - 10); + float yy2 = -(rInstrument + 10); + getdisplay().fillTriangle(200 + (int)(cosx * xx1 - sinx * yy1), 150 + (int)(sinx * xx1 + cosx * yy1), + 200 + (int)(cosx * xx2 - sinx * yy1), 150 + (int)(sinx * xx2 + cosx * yy1), + 200 + (int)(cosx * xx1 - sinx * yy2), 150 + (int)(sinx * xx1 + cosx * yy2), commonData->fgcolor); + getdisplay().fillTriangle(200 + (int)(cosx * xx2 - sinx * yy1), 150 + (int)(sinx * xx2 + cosx * yy1), + 200 + (int)(cosx * xx1 - sinx * yy2), 150 + (int)(sinx * xx1 + cosx * yy2), + 200 + (int)(cosx * xx2 - sinx * yy2), 150 + (int)(sinx * xx2 + cosx * yy2), commonData->fgcolor); + } } - if (value1 > 86400) {value1 -= 86400;} - if (value1 < 0) {value1 += 86400;} - hour = (value1 / 3600.0); - // minute = (hour - int(hour)) * 3600.0 / 60.0; // Analog minute pointer smooth moving - minute = int((hour - int(hour)) * 3600.0 / 60.0); // Jumping minute pointer from minute to minute - } - if (hour > 12) { - hour -= 12.0; - } - LOG_DEBUG(GwLog::DEBUG,"... PageClock, value1: %f hour: %f minute:%f", value1, hour, minute); - // Draw hour pointer - float startwidth = 8; // Start width of pointer - if(valid1 == true || (source == 'R' && commonData->data.rtcValid) || holdvalues == true || simulation == true){ - float sinx=sin(hour * 30.0 * pi / 180); // Hour - float cosx=cos(hour * 30.0 * pi / 180); - // Normal pointer - // Pointer as triangle with center base 2*width - float xx1 = -startwidth; - float xx2 = startwidth; - float yy1 = -startwidth; - float yy2 = -(rInstrument * 0.5); - getdisplay().fillTriangle(200+(int)(cosx*xx1-sinx*yy1),150+(int)(sinx*xx1+cosx*yy1), - 200+(int)(cosx*xx2-sinx*yy1),150+(int)(sinx*xx2+cosx*yy1), - 200+(int)(cosx*0-sinx*yy2),150+(int)(sinx*0+cosx*yy2),commonData->fgcolor); - // Inverted pointer - // Pointer as triangle with center base 2*width - float endwidth = 2; // End width of pointer - float ix1 = endwidth; - float ix2 = -endwidth; - float iy1 = -(rInstrument * 0.5); - float iy2 = -endwidth; - getdisplay().fillTriangle(200+(int)(cosx*ix1-sinx*iy1),150+(int)(sinx*ix1+cosx*iy1), - 200+(int)(cosx*ix2-sinx*iy1),150+(int)(sinx*ix2+cosx*iy1), - 200+(int)(cosx*0-sinx*iy2),150+(int)(sinx*0+cosx*iy2),commonData->fgcolor); - } + // Print Unit in clock + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().setCursor(175, 110); + if (holdvalues == false) { + getdisplay().print(tz == 'L' ? "LOT" : "UTC"); + } else { + getdisplay().print(unit2old); // date unit + } - // Draw minute pointer - startwidth = 8; // Start width of pointer - if(valid1 == true || (source == 'R' && commonData->data.rtcValid) || holdvalues == true || simulation == true){ - float sinx=sin(minute * 6.0 * pi / 180); // Minute - float cosx=cos(minute * 6.0 * pi / 180); - // Normal pointer - // Pointer as triangle with center base 2*width - float xx1 = -startwidth; - float xx2 = startwidth; - float yy1 = -startwidth; - float yy2 = -(rInstrument - 15); - getdisplay().fillTriangle(200+(int)(cosx*xx1-sinx*yy1),150+(int)(sinx*xx1+cosx*yy1), - 200+(int)(cosx*xx2-sinx*yy1),150+(int)(sinx*xx2+cosx*yy1), - 200+(int)(cosx*0-sinx*yy2),150+(int)(sinx*0+cosx*yy2),commonData->fgcolor); - // Inverted pointer - // Pointer as triangle with center base 2*width - float endwidth = 2; // End width of pointer - float ix1 = endwidth; - float ix2 = -endwidth; - float iy1 = -(rInstrument - 15); - float iy2 = -endwidth; - getdisplay().fillTriangle(200+(int)(cosx*ix1-sinx*iy1),150+(int)(sinx*ix1+cosx*iy1), - 200+(int)(cosx*ix2-sinx*iy1),150+(int)(sinx*ix2+cosx*iy1), - 200+(int)(cosx*0-sinx*iy2),150+(int)(sinx*0+cosx*iy2),commonData->fgcolor); - } + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(185, 190); + if (source == 'G') { + getdisplay().print("GPS"); + } else { + getdisplay().print("RTC"); + } - // Center circle - getdisplay().fillCircle(200, 150, startwidth + 6, commonData->bgcolor); - getdisplay().fillCircle(200, 150, startwidth + 4, commonData->fgcolor); + // Clock values + double hour = 0; + double minute = 0; + if (source == 'R') { + if (tz == 'L') { + time_t tv2 = mktime(&commonData->data.rtcTime) + timezone * 3600; + struct tm* local_tm2 = localtime(&tv2); + minute = local_tm2->tm_min; + hour = local_tm2->tm_hour; + } else { + minute = commonData->data.rtcTime.tm_min; + hour = commonData->data.rtcTime.tm_hour; + } + hour += minute / 60; + } else { + if (tz == 'L') { + value1 += timezone * 3600; + } + if (value1 > 86400) { value1 -= 86400; } + if (value1 < 0) { value1 += 86400; } + hour = (value1 / 3600.0); + // minute = (hour - int(hour)) * 3600.0 / 60.0; // Analog minute pointer smooth moving + minute = int((hour - int(hour)) * 3600.0 / 60.0); // Jumping minute pointer from minute to minute + } + if (hour > 12) { + hour -= 12.0; + } + LOG_DEBUG(GwLog::DEBUG, "... PageClock, value1: %f hour: %f minute:%f", value1, hour, minute); + + // Draw hour pointer + float startwidth = 8; // Start width of pointer + if (valid1 == true || (source == 'R' && commonData->data.rtcValid) || holdvalues == true || simulation == true) { + float sinx = sin(hour * 30.0 * pi / 180); // Hour + float cosx = cos(hour * 30.0 * pi / 180); + // Normal pointer + // Pointer as triangle with center base 2*width + float xx1 = -startwidth; + float xx2 = startwidth; + float yy1 = -startwidth; + float yy2 = -(rInstrument * 0.5); + getdisplay().fillTriangle(200 + (int)(cosx * xx1 - sinx * yy1), 150 + (int)(sinx * xx1 + cosx * yy1), + 200 + (int)(cosx * xx2 - sinx * yy1), 150 + (int)(sinx * xx2 + cosx * yy1), + 200 + (int)(cosx * 0 - sinx * yy2), 150 + (int)(sinx * 0 + cosx * yy2), commonData->fgcolor); + // Inverted pointer + // Pointer as triangle with center base 2*width + float endwidth = 2; // End width of pointer + float ix1 = endwidth; + float ix2 = -endwidth; + float iy1 = -(rInstrument * 0.5); + float iy2 = -endwidth; + getdisplay().fillTriangle(200 + (int)(cosx * ix1 - sinx * iy1), 150 + (int)(sinx * ix1 + cosx * iy1), + 200 + (int)(cosx * ix2 - sinx * iy1), 150 + (int)(sinx * ix2 + cosx * iy1), + 200 + (int)(cosx * 0 - sinx * iy2), 150 + (int)(sinx * 0 + cosx * iy2), commonData->fgcolor); + } + + // Draw minute pointer + startwidth = 8; // Start width of pointer + if (valid1 == true || (source == 'R' && commonData->data.rtcValid) || holdvalues == true || simulation == true) { + float sinx = sin(minute * 6.0 * pi / 180); // Minute + float cosx = cos(minute * 6.0 * pi / 180); + // Normal pointer + // Pointer as triangle with center base 2*width + float xx1 = -startwidth; + float xx2 = startwidth; + float yy1 = -startwidth; + float yy2 = -(rInstrument - 15); + getdisplay().fillTriangle(200 + (int)(cosx * xx1 - sinx * yy1), 150 + (int)(sinx * xx1 + cosx * yy1), + 200 + (int)(cosx * xx2 - sinx * yy1), 150 + (int)(sinx * xx2 + cosx * yy1), + 200 + (int)(cosx * 0 - sinx * yy2), 150 + (int)(sinx * 0 + cosx * yy2), commonData->fgcolor); + // Inverted pointer + // Pointer as triangle with center base 2*width + float endwidth = 2; // End width of pointer + float ix1 = endwidth; + float ix2 = -endwidth; + float iy1 = -(rInstrument - 15); + float iy2 = -endwidth; + getdisplay().fillTriangle(200 + (int)(cosx * ix1 - sinx * iy1), 150 + (int)(sinx * ix1 + cosx * iy1), + 200 + (int)(cosx * ix2 - sinx * iy1), 150 + (int)(sinx * ix2 + cosx * iy1), + 200 + (int)(cosx * 0 - sinx * iy2), 150 + (int)(sinx * 0 + cosx * iy2), commonData->fgcolor); + } + + // Center circle + getdisplay().fillCircle(200, 150, startwidth + 6, commonData->bgcolor); + getdisplay().fillCircle(200, 150, startwidth + 4, commonData->fgcolor); + } return PAGE_UPDATE; }; }; -static Page *createPage(CommonData &common){ +// Static member definitions +bool PageClock::timerInitialized = false; +bool PageClock::timerRunning = false; +int PageClock::timerHours = 0; +int PageClock::timerMinutes = 0; +int PageClock::timerSeconds = 0; +int PageClock::selectedField = 0; +bool PageClock::showSelectionMarker = true; +time_t PageClock::timerEndEpoch = 0; + +static Page* createPage(CommonData& common) +{ return new PageClock(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 (0 here) - * and will will provide the names of the fixed values we need + * we provide the number of user parameters we expect (0 here) + * and we provide the names of the fixed values we need */ PageDescription registerPageClock( - "Clock", // Page name - createPage, // Action - 0, // Number of bus values depends on selection in Web configuration + "Clock", // Page name + createPage, // Action + 0, // Number of bus values depends on selection in Web configuration {"GPST", "GPSD", "HDOP"}, // Bus values we need in the page - true // Show display header on/off + true // Show display header on/off ); #endif + diff --git a/lib/obp60task/PageClock.old b/lib/obp60task/PageClock.old new file mode 100644 index 0000000..7c7b74f --- /dev/null +++ b/lib/obp60task/PageClock.old @@ -0,0 +1,467 @@ +#if defined BOARD_OBP60S3 || defined BOARD_OBP40S3 + +#include "Pagedata.h" +#include "OBP60Extensions.h" + +/* + * TODO mode: race timer: keys + * - prepare: set countdown to 5min + * reset: abort current countdown and start over with 5min preparation + * - 5min: key press + * - 4min: key press to sync + * - 1min: buzzer signal + * - start: buzzer signal for start + * + */ + +class PageClock : public Page +{ +bool simulation = false; +int simtime; +bool keylock = false; +#ifdef BOARD_OBP60S3 +char source = 'G'; // time source (R)TC | (G)PS | (N)TP +#endif +#ifdef BOARD_OBP40S3 +char source = 'R'; // time source (R)TC | (G)PS | (N)TP +#endif +char mode = 'A'; // display mode (A)nalog | (D)igital | race (T)imer +char tz = 'L'; // time zone (L)ocal | (U)TC +double timezone = 0; // there are timezones with non int offsets, e.g. 5.5 or 5.75 +double homelat; +double homelon; +bool homevalid = false; // homelat and homelon are valid + + public: + PageClock(CommonData &common){ + commonData = &common; + common.logger->logDebug(GwLog::LOG,"Instantiate PageClock"); + simulation = common.config->getBool(common.config->useSimuData); + timezone = common.config->getString(common.config->timeZone).toDouble(); + homelat = common.config->getString(common.config->homeLAT).toDouble(); + homelon = common.config->getString(common.config->homeLON).toDouble(); + homevalid = homelat >= -180.0 and homelat <= 180 and homelon >= -90.0 and homelon <= 90.0; + simtime = 38160; // time value 11:36 + } + + virtual void setupKeys(){ + Page::setupKeys(); + commonData->keydata[0].label = "SRC"; + commonData->keydata[1].label = "MODE"; + commonData->keydata[4].label = "TZ"; + } + + // Key functions + virtual int handleKey(int key){ + // Time source + if (key == 1) { + switch (source) { + case 'G': source = 'R'; break; + case 'R': source = 'G'; break; + default: source = 'G'; break; + } + return 0; + } + if (key == 2) { + switch (mode) { + case 'A': mode = 'D'; break; + case 'D': mode = 'T'; break; + case 'T': mode = 'A'; break; + default: mode = 'A'; break; + } + return 0; + } + // Time zone: Local / UTC + if (key == 5) { + switch (tz) { + case 'L': tz = 'U'; break; + case 'U': tz = 'L'; break; + default: tz = 'L'; break; + } + return 0; + } + + // Keylock function + if(key == 11){ // Code for keylock + keylock = !keylock; // Toggle keylock + return 0; // Commit the key + } + return key; + } + + int displayPage(PageData &pageData) + { + GwConfigHandler *config = commonData->config; + GwLog *logger = commonData->logger; + + static String svalue1old = ""; + static String unit1old = ""; + static String svalue2old = ""; + static String unit2old = ""; + static String svalue3old = ""; + static String unit3old = ""; + + static String svalue5old = ""; + static String svalue6old = ""; + + double value1 = 0; + double value2 = 0; + double value3 = 0; + + // Get config data + String lengthformat = config->getString(config->lengthFormat); + String dateformat = config->getString(config->dateFormat); + bool holdvalues = config->getBool(config->holdvalues); + String flashLED = config->getString(config->flashLED); + String backlightMode = config->getString(config->backlight); + + // Get boat values for GPS time + GwApi::BoatValue *bvalue1 = pageData.values[0]; // First element in list (only one value by PageOneValue) + String name1 = bvalue1->getName().c_str(); // Value name + name1 = name1.substring(0, 6); // String length limit for value name + if(simulation == false){ + value1 = bvalue1->value; // Value as double in SI unit + } + else{ + value1 = simtime++; // Simulation data for time value 11:36 in seconds + } // Other simulation data see OBP60Formatter.cpp + bool valid1 = bvalue1->valid; // Valid information + String svalue1 = formatValue(bvalue1, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places + String unit1 = formatValue(bvalue1, *commonData).unit; // Unit of value + if(valid1 == true){ + svalue1old = svalue1; // Save old value + unit1old = unit1; // Save old unit + } + + // Get boat values for GPS date + GwApi::BoatValue *bvalue2 = pageData.values[1]; // Second element in list (only one value by PageOneValue) + String name2 = bvalue2->getName().c_str(); // Value name + name2 = name2.substring(0, 6); // String length limit for value name + value2 = bvalue2->value; // Value as double in SI unit + bool valid2 = bvalue2->valid; // Valid information + String svalue2 = formatValue(bvalue2, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places + String unit2 = formatValue(bvalue2, *commonData).unit; // Unit of value + if(valid2 == true){ + svalue2old = svalue2; // Save old value + unit2old = unit2; // Save old unit + } + + // Get boat values for HDOP date + GwApi::BoatValue *bvalue3 = pageData.values[2]; // Third element in list (only one value by PageOneValue) + String name3 = bvalue3->getName().c_str(); // Value name + name3 = name3.substring(0, 6); // String length limit for value name + value3 = bvalue3->value; // Value as double in SI unit + bool valid3 = bvalue3->valid; // Valid information + String svalue3 = formatValue(bvalue3, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places + String unit3 = formatValue(bvalue3, *commonData).unit; // Unit of value + if(valid3 == true){ + svalue3old = svalue3; // Save old value + unit3old = unit3; // Save old unit + } + + // Optical warning by limit violation (unused) + if(String(flashLED) == "Limit Violation"){ + setBlinkingLED(false); + setFlashLED(false); + } + + // Logging boat values + if (bvalue1 == NULL) return PAGE_OK; // WTF why this statement? + LOG_DEBUG(GwLog::LOG,"Drawing at PageClock, %s:%f, %s:%f", name1.c_str(), value1, name2.c_str(), value2); + + // Draw page + //*********************************************************** + + // Set display in partial refresh mode + getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + + getdisplay().setTextColor(commonData->fgcolor); + + time_t tv = mktime(&commonData->data.rtcTime) + timezone * 3600; + struct tm *local_tm = localtime(&tv); + + // Show values GPS date + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(10, 65); + if (holdvalues == false) { + if (source == 'G') { + // GPS value + getdisplay().print(svalue2); + } else if (commonData->data.rtcValid) { + // RTC value + if (tz == 'L') { + getdisplay().print(formatDate(dateformat, local_tm->tm_year + 1900, local_tm->tm_mon + 1, local_tm->tm_mday)); + } + else { + getdisplay().print(formatDate(dateformat, commonData->data.rtcTime.tm_year + 1900, commonData->data.rtcTime.tm_mon + 1, commonData->data.rtcTime.tm_mday)); + } + } else { + getdisplay().print("---"); + } + } else { + getdisplay().print(svalue2old); + } + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().setCursor(10, 95); + getdisplay().print("Date"); // Name + + // Horizintal separator left + getdisplay().fillRect(0, 149, 60, 3, commonData->fgcolor); + + // Show values GPS time + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(10, 250); + if (holdvalues == false) { + if (source == 'G') { + getdisplay().print(svalue1); // Value + } + else if (commonData->data.rtcValid) { + if (tz == 'L') { + getdisplay().print(formatTime('s', local_tm->tm_hour, local_tm->tm_min, local_tm->tm_sec)); + } + else { + getdisplay().print(formatTime('s', commonData->data.rtcTime.tm_hour, commonData->data.rtcTime.tm_min, commonData->data.rtcTime.tm_sec)); + } + } else { + getdisplay().print("---"); + } + } + else { + getdisplay().print(svalue1old); + } + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().setCursor(10, 220); + getdisplay().print("Time"); // Name + + // Show values sunrise + String sunrise = "---"; + if ((valid1 and valid2 and valid3 == true) or (homevalid and commonData->data.rtcValid)) { + sunrise = String(commonData->sundata.sunriseHour) + ":" + String(commonData->sundata.sunriseMinute + 100).substring(1); + svalue5old = sunrise; + } else if (simulation) { + sunrise = String("06:42"); + } + + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(335, 65); + if(holdvalues == false) getdisplay().print(sunrise); // Value + else getdisplay().print(svalue5old); + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().setCursor(335, 95); + getdisplay().print("SunR"); // Name + + // Horizintal separator right + getdisplay().fillRect(340, 149, 80, 3, commonData->fgcolor); + + // Show values sunset + String sunset = "---"; + if ((valid1 and valid2 and valid3 == true) or (homevalid and commonData->data.rtcValid)) { + sunset = String(commonData->sundata.sunsetHour) + ":" + String(commonData->sundata.sunsetMinute + 100).substring(1); + svalue6old = sunset; + } else if (simulation) { + sunset = String("21:03"); + } + + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(335, 250); + if(holdvalues == false) getdisplay().print(sunset); // Value + else getdisplay().print(svalue6old); + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().setCursor(335, 220); + getdisplay().print("SunS"); // Name + +//******************************************************************************************* + + // Draw clock + int rInstrument = 110; // Radius of clock + float pi = 3.141592; + + getdisplay().fillCircle(200, 150, rInstrument + 10, commonData->fgcolor); // Outer circle + getdisplay().fillCircle(200, 150, rInstrument + 7, commonData->bgcolor); // Outer circle + + for(int i=0; i<360; i=i+1) + { + // Scaling values + float x = 200 + (rInstrument-30)*sin(i/180.0*pi); // x-coordinate dots + float y = 150 - (rInstrument-30)*cos(i/180.0*pi); // y-coordinate cots + const char *ii = ""; + switch (i) + { + case 0: ii="12"; break; + case 30 : ii=""; break; + case 60 : ii=""; break; + case 90 : ii="3"; break; + case 120 : ii=""; break; + case 150 : ii=""; break; + case 180 : ii="6"; break; + case 210 : ii=""; break; + case 240 : ii=""; break; + case 270 : ii="9"; break; + case 300 : ii=""; break; + case 330 : ii=""; break; + default: break; + } + + // Print text centered on position x, y + int16_t x1, y1; // Return values of getTextBounds + uint16_t w, h; // Return values of getTextBounds + getdisplay().getTextBounds(ii, int(x), int(y), &x1, &y1, &w, &h); // Calc width of new string + getdisplay().setCursor(x-w/2, y+h/2); + if(i % 30 == 0){ + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().print(ii); + } + + // Draw sub scale with dots + float sinx = 0; + float cosx = 0; + if(i % 6 == 0){ + float x1c = 200 + rInstrument*sin(i/180.0*pi); + float y1c = 150 - rInstrument*cos(i/180.0*pi); + getdisplay().fillCircle((int)x1c, (int)y1c, 2, commonData->fgcolor); + sinx=sin(i/180.0*pi); + cosx=cos(i/180.0*pi); + } + + // Draw sub scale with lines (two triangles) + if(i % 30 == 0){ + float dx=2; // Line thickness = 2*dx+1 + float xx1 = -dx; + float xx2 = +dx; + float yy1 = -(rInstrument-10); + float yy2 = -(rInstrument+10); + getdisplay().fillTriangle(200+(int)(cosx*xx1-sinx*yy1),150+(int)(sinx*xx1+cosx*yy1), + 200+(int)(cosx*xx2-sinx*yy1),150+(int)(sinx*xx2+cosx*yy1), + 200+(int)(cosx*xx1-sinx*yy2),150+(int)(sinx*xx1+cosx*yy2),commonData->fgcolor); + getdisplay().fillTriangle(200+(int)(cosx*xx2-sinx*yy1),150+(int)(sinx*xx2+cosx*yy1), + 200+(int)(cosx*xx1-sinx*yy2),150+(int)(sinx*xx1+cosx*yy2), + 200+(int)(cosx*xx2-sinx*yy2),150+(int)(sinx*xx2+cosx*yy2),commonData->fgcolor); + } + } + + // Print Unit in clock + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().setCursor(175, 110); + if(holdvalues == false){ + getdisplay().print(tz == 'L' ? "LOT" : "UTC"); + } + else{ + getdisplay().print(unit2old); // date unit + } + + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(185, 190); + if (source == 'G') { + getdisplay().print("GPS"); + } else { + getdisplay().print("RTC"); + } + + // Clock values + double hour = 0; + double minute = 0; + if (source == 'R') { + if (tz == 'L') { + time_t tv = mktime(&commonData->data.rtcTime) + timezone * 3600; + struct tm *local_tm = localtime(&tv); + minute = local_tm->tm_min; + hour = local_tm->tm_hour; + } else { + minute = commonData->data.rtcTime.tm_min; + hour = commonData->data.rtcTime.tm_hour; + } + hour += minute / 60; + } else { + if (tz == 'L') { + value1 += timezone * 3600; + } + if (value1 > 86400) {value1 -= 86400;} + if (value1 < 0) {value1 += 86400;} + hour = (value1 / 3600.0); + // minute = (hour - int(hour)) * 3600.0 / 60.0; // Analog minute pointer smooth moving + minute = int((hour - int(hour)) * 3600.0 / 60.0); // Jumping minute pointer from minute to minute + } + if (hour > 12) { + hour -= 12.0; + } + LOG_DEBUG(GwLog::DEBUG,"... PageClock, value1: %f hour: %f minute:%f", value1, hour, minute); + + // Draw hour pointer + float startwidth = 8; // Start width of pointer + if(valid1 == true || (source == 'R' && commonData->data.rtcValid) || holdvalues == true || simulation == true){ + float sinx=sin(hour * 30.0 * pi / 180); // Hour + float cosx=cos(hour * 30.0 * pi / 180); + // Normal pointer + // Pointer as triangle with center base 2*width + float xx1 = -startwidth; + float xx2 = startwidth; + float yy1 = -startwidth; + float yy2 = -(rInstrument * 0.5); + getdisplay().fillTriangle(200+(int)(cosx*xx1-sinx*yy1),150+(int)(sinx*xx1+cosx*yy1), + 200+(int)(cosx*xx2-sinx*yy1),150+(int)(sinx*xx2+cosx*yy1), + 200+(int)(cosx*0-sinx*yy2),150+(int)(sinx*0+cosx*yy2),commonData->fgcolor); + // Inverted pointer + // Pointer as triangle with center base 2*width + float endwidth = 2; // End width of pointer + float ix1 = endwidth; + float ix2 = -endwidth; + float iy1 = -(rInstrument * 0.5); + float iy2 = -endwidth; + getdisplay().fillTriangle(200+(int)(cosx*ix1-sinx*iy1),150+(int)(sinx*ix1+cosx*iy1), + 200+(int)(cosx*ix2-sinx*iy1),150+(int)(sinx*ix2+cosx*iy1), + 200+(int)(cosx*0-sinx*iy2),150+(int)(sinx*0+cosx*iy2),commonData->fgcolor); + } + + // Draw minute pointer + startwidth = 8; // Start width of pointer + if(valid1 == true || (source == 'R' && commonData->data.rtcValid) || holdvalues == true || simulation == true){ + float sinx=sin(minute * 6.0 * pi / 180); // Minute + float cosx=cos(minute * 6.0 * pi / 180); + // Normal pointer + // Pointer as triangle with center base 2*width + float xx1 = -startwidth; + float xx2 = startwidth; + float yy1 = -startwidth; + float yy2 = -(rInstrument - 15); + getdisplay().fillTriangle(200+(int)(cosx*xx1-sinx*yy1),150+(int)(sinx*xx1+cosx*yy1), + 200+(int)(cosx*xx2-sinx*yy1),150+(int)(sinx*xx2+cosx*yy1), + 200+(int)(cosx*0-sinx*yy2),150+(int)(sinx*0+cosx*yy2),commonData->fgcolor); + // Inverted pointer + // Pointer as triangle with center base 2*width + float endwidth = 2; // End width of pointer + float ix1 = endwidth; + float ix2 = -endwidth; + float iy1 = -(rInstrument - 15); + float iy2 = -endwidth; + getdisplay().fillTriangle(200+(int)(cosx*ix1-sinx*iy1),150+(int)(sinx*ix1+cosx*iy1), + 200+(int)(cosx*ix2-sinx*iy1),150+(int)(sinx*ix2+cosx*iy1), + 200+(int)(cosx*0-sinx*iy2),150+(int)(sinx*0+cosx*iy2),commonData->fgcolor); + } + + // Center circle + getdisplay().fillCircle(200, 150, startwidth + 6, commonData->bgcolor); + getdisplay().fillCircle(200, 150, startwidth + 4, commonData->fgcolor); + + return PAGE_UPDATE; + }; +}; + +static Page *createPage(CommonData &common){ + return new PageClock(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 (0 here) + * and will will provide the names of the fixed values we need + */ +PageDescription registerPageClock( + "Clock", // Page name + createPage, // Action + 0, // Number of bus values depends on selection in Web configuration + {"GPST", "GPSD", "HDOP"}, // Bus values we need in the page + true // Show display header on/off +); + +#endif diff --git a/lib/obp60task/PageClock2.new b/lib/obp60task/PageClock2.new new file mode 100644 index 0000000..3e0af87 --- /dev/null +++ b/lib/obp60task/PageClock2.new @@ -0,0 +1,548 @@ +#if defined BOARD_OBP60S3 || defined BOARD_OBP40S3 + +#include "Pagedata.h" +#include "OBP60Extensions.h" + +/* + * Variant of PageClock with switchable analog / digital clock display. + * mode: (A)nalog | (D)igital | race (T)imer (T not implemented yet, falls back to analog) + */ + +class PageClock2 : public Page +{ + bool simulation = false; + int simtime; + bool keylock = false; +#ifdef BOARD_OBP60S3 + char source = 'G'; // time source (R)TC | (G)PS | (N)TP +#endif +#ifdef BOARD_OBP40S3 + char source = 'R'; // time source (R)TC | (G)PS | (N)TP +#endif + char mode = 'A'; // display mode (A)nalog | (D)igital | race (T)imer + char tz = 'L'; // time zone (L)ocal | (U)TC + double timezone = 0; // there are timezones with non int offsets, e.g. 5.5 or 5.75 + double homelat; + double homelon; + bool homevalid = false; // homelat and homelon are valid + +public: + PageClock2(CommonData& common) + { + commonData = &common; + common.logger->logDebug(GwLog::LOG, "Instantiate PageClock2"); + simulation = common.config->getBool(common.config->useSimuData); + timezone = common.config->getString(common.config->timeZone).toDouble(); + homelat = common.config->getString(common.config->homeLAT).toDouble(); + homelon = common.config->getString(common.config->homeLON).toDouble(); + homevalid = homelat >= -180.0 and homelat <= 180 and homelon >= -90.0 and homelon <= 90.0; + simtime = 38160; // time value 11:36 + } + + virtual void setupKeys() + { + Page::setupKeys(); + commonData->keydata[0].label = "SRC"; + commonData->keydata[1].label = "MODE"; + commonData->keydata[4].label = "TZ"; + } + + // Key functions + virtual int handleKey(int key) + { + // Time source + if (key == 1) { + switch (source) { + case 'G': + source = 'R'; + break; + case 'R': + source = 'G'; + break; + default: + source = 'G'; + break; + } + return 0; + } + if (key == 2) { + switch (mode) { + case 'A': + mode = 'D'; + break; + case 'D': + mode = 'T'; + break; + case 'T': + mode = 'A'; + break; + default: + mode = 'A'; + break; + } + return 0; + } + // Time zone: Local / UTC + if (key == 5) { + switch (tz) { + case 'L': + tz = 'U'; + break; + case 'U': + tz = 'L'; + break; + default: + tz = 'L'; + break; + } + return 0; + } + + // Keylock function + if (key == 11) { // Code for keylock + keylock = !keylock; // Toggle keylock + return 0; // Commit the key + } + return key; + } + + int displayPage(PageData& pageData) + { + GwConfigHandler* config = commonData->config; + + static String svalue1old = ""; + static String unit1old = ""; + static String svalue2old = ""; + static String unit2old = ""; + static String svalue3old = ""; + static String unit3old = ""; + + static String svalue5old = ""; + static String svalue6old = ""; + + double value1 = 0; + double value2 = 0; + double value3 = 0; + + // Get config data + String lengthformat = config->getString(config->lengthFormat); + String dateformat = config->getString(config->dateFormat); + bool holdvalues = config->getBool(config->holdvalues); + String flashLED = config->getString(config->flashLED); + String backlightMode = config->getString(config->backlight); + + // Get boat values for GPS time + GwApi::BoatValue* bvalue1 = pageData.values[0]; // First element in list (only one value by PageOneValue) + String name1 = bvalue1->getName().c_str(); // Value name + name1 = name1.substring(0, 6); // String length limit for value name + if (simulation == false) { + value1 = bvalue1->value; // Value as double in SI unit + } else { + value1 = simtime++; // Simulation data for time value 11:36 in seconds + } // Other simulation data see OBP60Formatter.cpp + bool valid1 = bvalue1->valid; // Valid information + String svalue1 = formatValue(bvalue1, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places + String unit1 = formatValue(bvalue1, *commonData).unit; // Unit of value + if (valid1 == true) { + svalue1old = svalue1; // Save old value + unit1old = unit1; // Save old unit + } + + // Get boat values for GPS date + GwApi::BoatValue* bvalue2 = pageData.values[1]; // Second element in list (only one value by PageOneValue) + String name2 = bvalue2->getName().c_str(); // Value name + name2 = name2.substring(0, 6); // String length limit for value name + value2 = bvalue2->value; // Value as double in SI unit + bool valid2 = bvalue2->valid; // Valid information + String svalue2 = formatValue(bvalue2, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places + String unit2 = formatValue(bvalue2, *commonData).unit; // Unit of value + if (valid2 == true) { + svalue2old = svalue2; // Save old value + unit2old = unit2; // Save old unit + } + + // Get boat values for HDOP date + GwApi::BoatValue* bvalue3 = pageData.values[2]; // Third element in list (only one value by PageOneValue) + String name3 = bvalue3->getName().c_str(); // Value name + name3 = name3.substring(0, 6); // String length limit for value name + value3 = bvalue3->value; // Value as double in SI unit + bool valid3 = bvalue3->valid; // Valid information + String svalue3 = formatValue(bvalue3, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places + String unit3 = formatValue(bvalue3, *commonData).unit; // Unit of value + if (valid3 == true) { + svalue3old = svalue3; // Save old value + unit3old = unit3; // Save old unit + } + + // Optical warning by limit violation (unused) + if (String(flashLED) == "Limit Violation") { + setBlinkingLED(false); + setFlashLED(false); + } + + // Logging boat values + if (bvalue1 == NULL) + return PAGE_OK; // WTF why this statement? + LOG_DEBUG(GwLog::LOG, "Drawing at PageClock2, %s:%f, %s:%f", name1.c_str(), value1, name2.c_str(), value2); + + // Draw page + //*********************************************************** + + // Set display in partial refresh mode + getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + + getdisplay().setTextColor(commonData->fgcolor); + + time_t tv = mktime(&commonData->data.rtcTime) + timezone * 3600; + struct tm* local_tm = localtime(&tv); + + // Show values GPS date + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(10, 65); + if (holdvalues == false) { + if (source == 'G') { + // GPS value + getdisplay().print(svalue2); + } else if (commonData->data.rtcValid) { + // RTC value + if (tz == 'L') { + getdisplay().print(formatDate(dateformat, local_tm->tm_year + 1900, local_tm->tm_mon + 1, local_tm->tm_mday)); + } else { + getdisplay().print(formatDate(dateformat, commonData->data.rtcTime.tm_year + 1900, commonData->data.rtcTime.tm_mon + 1, commonData->data.rtcTime.tm_mday)); + } + } else { + getdisplay().print("---"); + } + } else { + getdisplay().print(svalue2old); + } + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().setCursor(10, 95); + getdisplay().print("Date"); // Name + + // Horizintal separator left + getdisplay().fillRect(0, 149, 60, 3, commonData->fgcolor); + + // Show values GPS time (small text bottom left, same as original) + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(10, 250); + if (holdvalues == false) { + if (source == 'G') { + getdisplay().print(svalue1); // Value + } else if (commonData->data.rtcValid) { + if (tz == 'L') { + getdisplay().print(formatTime('s', local_tm->tm_hour, local_tm->tm_min, local_tm->tm_sec)); + } else { + getdisplay().print(formatTime('s', commonData->data.rtcTime.tm_hour, commonData->data.rtcTime.tm_min, commonData->data.rtcTime.tm_sec)); + } + } else { + getdisplay().print("---"); + } + } else { + getdisplay().print(svalue1old); + } + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().setCursor(10, 220); + getdisplay().print("Time"); // Name + + // Show values sunrise + String sunrise = "---"; + if ((valid1 and valid2 and valid3 == true) or (homevalid and commonData->data.rtcValid)) { + sunrise = String(commonData->sundata.sunriseHour) + ":" + String(commonData->sundata.sunriseMinute + 100).substring(1); + svalue5old = sunrise; + } else if (simulation) { + sunrise = String("06:42"); + } + + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(335, 65); + if (holdvalues == false) + getdisplay().print(sunrise); // Value + else + getdisplay().print(svalue5old); + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().setCursor(335, 95); + getdisplay().print("SunR"); // Name + + // Horizintal separator right + getdisplay().fillRect(340, 149, 80, 3, commonData->fgcolor); + + // Show values sunset + String sunset = "---"; + if ((valid1 and valid2 and valid3 == true) or (homevalid and commonData->data.rtcValid)) { + sunset = String(commonData->sundata.sunsetHour) + ":" + String(commonData->sundata.sunsetMinute + 100).substring(1); + svalue6old = sunset; + } else if (simulation) { + sunset = String("21:03"); + } + + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(335, 250); + if (holdvalues == false) + getdisplay().print(sunset); // Value + else + getdisplay().print(svalue6old); + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().setCursor(335, 220); + getdisplay().print("SunS"); // Name + + //******************************************************************************************* + + if (mode == 'D') { + // Digital clock mode: large 7-segment time centered on display + + // Determine current time in hours/minutes/seconds (24h) + int hour24 = 0; + int minute24 = 0; + int second24 = 0; + + if (source == 'R' && commonData->data.rtcValid) { + // RTC based + time_t tv2 = mktime(&commonData->data.rtcTime); + if (tz == 'L') { + tv2 += static_cast(timezone * 3600); + } + struct tm* tm2 = localtime(&tv2); + hour24 = tm2->tm_hour; + minute24 = tm2->tm_min; + second24 = tm2->tm_sec; + } else { + // GPS / simulation based + double t = value1; + if (tz == 'L') { + t += timezone * 3600; + } + if (t >= 86400) + t -= 86400; + if (t < 0) + t += 86400; + hour24 = static_cast(t / 3600.0); + int rest = static_cast(t) - hour24 * 3600; + minute24 = rest / 60; + second24 = rest % 60; + } + + char buf[9]; // "HH:MM:SS" + '\0' + snprintf(buf, sizeof(buf), "%02d:%02d:%02d", hour24, minute24, second24); + String timeStr = String(buf); + + // Clear central area and draw large digital time + getdisplay().fillRect(0, 110, getdisplay().width(), 80, commonData->bgcolor); + + getdisplay().setFont(&DSEG7Classic_BoldItalic60pt7b); + + int16_t x1b, y1b; + uint16_t wb, hb; + getdisplay().getTextBounds(timeStr, 0, 0, &x1b, &y1b, &wb, &hb); + + int16_t x = (static_cast(getdisplay().width()) - static_cast(wb)) / 2; + int16_t y = 150 + hb / 2; // vertically around center (y=150) + + getdisplay().setCursor(x, y); + getdisplay().print(timeStr); + + // Small indicators inside the digital area + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(180, 110); + if (holdvalues == false) { + getdisplay().print(tz == 'L' ? "LOT" : "UTC"); + } else { + getdisplay().print(unit2old); // date unit + } + + getdisplay().setCursor(185, 190); + if (source == 'G') { + getdisplay().print("GPS"); + } else { + getdisplay().print("RTC"); + } + + } else { + // Analog clock mode (A/T) - original drawing + + int rInstrument = 110; // Radius of clock + float pi = 3.141592; + + getdisplay().fillCircle(200, 150, rInstrument + 10, commonData->fgcolor); // Outer circle + getdisplay().fillCircle(200, 150, rInstrument + 7, commonData->bgcolor); // Outer circle + + for (int i = 0; i < 360; i = i + 1) + { + // Scaling values + float x = 200 + (rInstrument - 30) * sin(i / 180.0 * pi); // x-coordinate dots + float y = 150 - (rInstrument - 30) * cos(i / 180.0 * pi); // y-coordinate dots + const char* ii = ""; + switch (i) + { + case 0: ii = "12"; break; + case 90: ii = "3"; break; + case 180: ii = "6"; break; + case 270: ii = "9"; break; + default: break; + } + + // Print text centered on position x, y + int16_t x1c, y1c; // Return values of getTextBounds + uint16_t wc, hc; // Return values of getTextBounds + getdisplay().getTextBounds(ii, int(x), int(y), &x1c, &y1c, &wc, &hc); // Calc width of new string + getdisplay().setCursor(x - wc / 2, y + hc / 2); + if (i % 90 == 0) { + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().print(ii); + } + + // Draw sub scale with dots + float sinx = 0; + float cosx = 0; + if (i % 6 == 0) { + float x1d = 200 + rInstrument * sin(i / 180.0 * pi); + float y1d = 150 - rInstrument * cos(i / 180.0 * pi); + getdisplay().fillCircle((int)x1d, (int)y1d, 2, commonData->fgcolor); + sinx = sin(i / 180.0 * pi); + cosx = cos(i / 180.0 * pi); + } + + // Draw sub scale with lines (two triangles) + if (i % 30 == 0) { + float dx = 2; // Line thickness = 2*dx+1 + float xx1 = -dx; + float xx2 = +dx; + float yy1 = -(rInstrument - 10); + float yy2 = -(rInstrument + 10); + getdisplay().fillTriangle(200 + (int)(cosx * xx1 - sinx * yy1), 150 + (int)(sinx * xx1 + cosx * yy1), + 200 + (int)(cosx * xx2 - sinx * yy1), 150 + (int)(sinx * xx2 + cosx * yy1), + 200 + (int)(cosx * xx1 - sinx * yy2), 150 + (int)(sinx * xx1 + cosx * yy2), commonData->fgcolor); + getdisplay().fillTriangle(200 + (int)(cosx * xx2 - sinx * yy1), 150 + (int)(sinx * xx2 + cosx * yy1), + 200 + (int)(cosx * xx1 - sinx * yy2), 150 + (int)(sinx * xx1 + cosx * yy2), + 200 + (int)(cosx * xx2 - sinx * yy2), 150 + (int)(sinx * xx2 + cosx * yy2), commonData->fgcolor); + } + } + + // Print Unit in clock + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().setCursor(175, 110); + if (holdvalues == false) { + getdisplay().print(tz == 'L' ? "LOT" : "UTC"); + } else { + getdisplay().print(unit2old); // date unit + } + + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(185, 190); + if (source == 'G') { + getdisplay().print("GPS"); + } else { + getdisplay().print("RTC"); + } + + // Clock values + double hour = 0; + double minute = 0; + if (source == 'R') { + if (tz == 'L') { + time_t tv2 = mktime(&commonData->data.rtcTime) + timezone * 3600; + struct tm* local_tm2 = localtime(&tv2); + minute = local_tm2->tm_min; + hour = local_tm2->tm_hour; + } else { + minute = commonData->data.rtcTime.tm_min; + hour = commonData->data.rtcTime.tm_hour; + } + hour += minute / 60; + } else { + if (tz == 'L') { + value1 += timezone * 3600; + } + if (value1 > 86400) { value1 -= 86400; } + if (value1 < 0) { value1 += 86400; } + hour = (value1 / 3600.0); + // minute = (hour - int(hour)) * 3600.0 / 60.0; // Analog minute pointer smooth moving + minute = int((hour - int(hour)) * 3600.0 / 60.0); // Jumping minute pointer from minute to minute + } + if (hour > 12) { + hour -= 12.0; + } + LOG_DEBUG(GwLog::DEBUG, "... PageClock2, value1: %f hour: %f minute:%f", value1, hour, minute); + + // Draw hour pointer + float startwidth = 8; // Start width of pointer + if (valid1 == true || (source == 'R' && commonData->data.rtcValid) || holdvalues == true || simulation == true) { + float sinx = sin(hour * 30.0 * pi / 180); // Hour + float cosx = cos(hour * 30.0 * pi / 180); + // Normal pointer + // Pointer as triangle with center base 2*width + float xx1 = -startwidth; + float xx2 = startwidth; + float yy1 = -startwidth; + float yy2 = -(rInstrument * 0.5); + getdisplay().fillTriangle(200 + (int)(cosx * xx1 - sinx * yy1), 150 + (int)(sinx * xx1 + cosx * yy1), + 200 + (int)(cosx * xx2 - sinx * yy1), 150 + (int)(sinx * xx2 + cosx * yy1), + 200 + (int)(cosx * 0 - sinx * yy2), 150 + (int)(sinx * 0 + cosx * yy2), commonData->fgcolor); + // Inverted pointer + // Pointer as triangle with center base 2*width + float endwidth = 2; // End width of pointer + float ix1 = endwidth; + float ix2 = -endwidth; + float iy1 = -(rInstrument * 0.5); + float iy2 = -endwidth; + getdisplay().fillTriangle(200 + (int)(cosx * ix1 - sinx * iy1), 150 + (int)(sinx * ix1 + cosx * iy1), + 200 + (int)(cosx * ix2 - sinx * iy1), 150 + (int)(sinx * ix2 + cosx * iy1), + 200 + (int)(cosx * 0 - sinx * iy2), 150 + (int)(sinx * 0 + cosx * iy2), commonData->fgcolor); + } + + // Draw minute pointer + startwidth = 8; // Start width of pointer + if (valid1 == true || (source == 'R' && commonData->data.rtcValid) || holdvalues == true || simulation == true) { + float sinx = sin(minute * 6.0 * pi / 180); // Minute + float cosx = cos(minute * 6.0 * pi / 180); + // Normal pointer + // Pointer as triangle with center base 2*width + float xx1 = -startwidth; + float xx2 = startwidth; + float yy1 = -startwidth; + float yy2 = -(rInstrument - 15); + getdisplay().fillTriangle(200 + (int)(cosx * xx1 - sinx * yy1), 150 + (int)(sinx * xx1 + cosx * yy1), + 200 + (int)(cosx * xx2 - sinx * yy1), 150 + (int)(sinx * xx2 + cosx * yy1), + 200 + (int)(cosx * 0 - sinx * yy2), 150 + (int)(sinx * 0 + cosx * yy2), commonData->fgcolor); + // Inverted pointer + // Pointer as triangle with center base 2*width + float endwidth = 2; // End width of pointer + float ix1 = endwidth; + float ix2 = -endwidth; + float iy1 = -(rInstrument - 15); + float iy2 = -endwidth; + getdisplay().fillTriangle(200 + (int)(cosx * ix1 - sinx * iy1), 150 + (int)(sinx * ix1 + cosx * iy1), + 200 + (int)(cosx * ix2 - sinx * iy1), 150 + (int)(sinx * ix2 + cosx * iy1), + 200 + (int)(cosx * 0 - sinx * iy2), 150 + (int)(sinx * 0 + cosx * iy2), commonData->fgcolor); + } + + // Center circle + getdisplay().fillCircle(200, 150, startwidth + 6, commonData->bgcolor); + getdisplay().fillCircle(200, 150, startwidth + 4, commonData->fgcolor); + } + + return PAGE_UPDATE; + }; +}; + +static Page* createPage(CommonData& common) +{ + return new PageClock2(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 + * we provide the number of user parameters we expect (0 here) + * and we provide the names of the fixed values we need + */ +PageDescription registerPageClock2( + "Clock2", // Page name + createPage, // Action + 0, // Number of bus values depends on selection in Web configuration + {"GPST", "GPSD", "HDOP"}, // Bus values we need in the page + true // Show display header on/off +); + +#endif + diff --git a/lib/obp60task/PageClock3.new b/lib/obp60task/PageClock3.new new file mode 100644 index 0000000..2814c8b --- /dev/null +++ b/lib/obp60task/PageClock3.new @@ -0,0 +1,777 @@ +#if defined BOARD_OBP60S3 || defined BOARD_OBP40S3 + +#include "Pagedata.h" +#include "OBP60Extensions.h" + +/* + * PageClock3: Clock page with + * - Analog mode (mode == 'A') + * - Digital mode (mode == 'D') + * - Countdown timer mode (mode == 'T') + * + * Timer mode: + * - Format HH:MM:SS (24h, leading zeros) + * - Keys in timer mode: + * K1: MODE (A/D/T) + * K2: POS (select field: HH / MM / SS) + * K3: + (increment selected field) + * K4: - (decrement selected field) + * K5: RUN (start/stop countdown) + * - Selection marker: line under active field (width 2px, not wider than digits) + * - Editing only possible when timer is not running + * - When page is left, running timer continues in background using RTC time + * (on re-entry, remaining time is recalculated from RTC) + */ + +class PageClock3 : public Page +{ + bool simulation = false; + int simtime; + bool keylock = false; +#ifdef BOARD_OBP60S3 + char source = 'G'; // time source (R)TC | (G)PS | (N)TP +#endif +#ifdef BOARD_OBP40S3 + char source = 'R'; // time source (R)TC | (G)PS | (N)TP +#endif + char mode = 'A'; // display mode (A)nalog | (D)igital | race (T)imer + char tz = 'L'; // time zone (L)ocal | (U)TC + double timezone = 0; // there are timezones with non int offsets, e.g. 5.5 or 5.75 + double homelat; + double homelon; + bool homevalid = false; // homelat and homelon are valid + + // Timer state (static so it survives page switches) + static bool timerInitialized; + static bool timerRunning; + static int timerHours; + static int timerMinutes; + static int timerSeconds; + static int selectedField; // 0 = hours, 1 = minutes, 2 = seconds + static bool showSelectionMarker; + static time_t timerEndEpoch; // absolute end time based on RTC + + void setupTimerDefaults() + { + if (!timerInitialized) { + timerInitialized = true; + timerRunning = false; + timerHours = 0; + timerMinutes = 0; + timerSeconds = 0; + selectedField = 0; + showSelectionMarker = true; + timerEndEpoch = 0; + } + } + + static int clamp(int value, int minVal, int maxVal) + { + if (value < minVal) return minVal; + if (value > maxVal) return maxVal; + return value; + } + + void incrementSelected() + { + if (selectedField == 0) { + timerHours = clamp(timerHours + 1, 0, 23); + } else if (selectedField == 1) { + timerMinutes = clamp(timerMinutes + 1, 0, 59); + } else { + timerSeconds = clamp(timerSeconds + 1, 0, 59); + } + } + + void decrementSelected() + { + if (selectedField == 0) { + timerHours = clamp(timerHours - 1, 0, 23); + } else if (selectedField == 1) { + timerMinutes = clamp(timerMinutes - 1, 0, 59); + } else { + timerSeconds = clamp(timerSeconds - 1, 0, 59); + } + } + + int totalTimerSeconds() const + { + return timerHours * 3600 + timerMinutes * 60 + timerSeconds; + } + +public: + PageClock3(CommonData& common) + { + commonData = &common; + common.logger->logDebug(GwLog::LOG, "Instantiate PageClock3"); + simulation = common.config->getBool(common.config->useSimuData); + timezone = common.config->getString(common.config->timeZone).toDouble(); + homelat = common.config->getString(common.config->homeLAT).toDouble(); + homelon = common.config->getString(common.config->homeLON).toDouble(); + homevalid = homelat >= -180.0 and homelat <= 180 and homelon >= -90.0 and homelon <= 90.0; + simtime = 38160; // time value 11:36 + setupTimerDefaults(); + } + + virtual void setupKeys() + { + Page::setupKeys(); + + if (mode == 'T') { + // Timer mode: MODE, POS, +, -, RUN + commonData->keydata[0].label = "MODE"; + commonData->keydata[1].label = "POS"; + commonData->keydata[2].label = "+"; + commonData->keydata[3].label = "-"; + commonData->keydata[4].label = "RUN"; + } else { + // Clock modes: like original + commonData->keydata[0].label = "SRC"; + commonData->keydata[1].label = "MODE"; + commonData->keydata[4].label = "TZ"; + } + } + + // Key functions + virtual int handleKey(int key) + { + setupTimerDefaults(); + + // Keylock function + if (key == 11) { // Code for keylock + keylock = !keylock; // Toggle keylock + return 0; // Commit the key + } + + if (mode == 'T') { + // Timer mode key handling + + // MODE (K1): cycle display mode A/D/T + if (key == 1) { + switch (mode) { + case 'A': mode = 'D'; break; + case 'D': mode = 'T'; break; + case 'T': mode = 'A'; break; + default: mode = 'A'; break; + } + setupKeys(); + return 0; + } + + // POS (K2): select field HH / MM / SS (only if timer not running) + if (key == 2 && !timerRunning) { + selectedField = (selectedField + 1) % 3; + showSelectionMarker = true; + return 0; + } + + // + (K3): increment selected field (only if timer not running) + if (key == 3 && !timerRunning) { + incrementSelected(); + return 0; + } + + // - (K4): decrement selected field (only if timer not running) + if (key == 4 && !timerRunning) { + decrementSelected(); + return 0; + } + + // RUN (K5): start/stop timer + if (key == 5) { + if (!timerRunning) { + // Start timer if a non-zero duration is set + int total = totalTimerSeconds(); + if (total > 0 && commonData->data.rtcValid) { + struct tm rtcCopy = commonData->data.rtcTime; + time_t nowEpoch = mktime(&rtcCopy); + timerEndEpoch = nowEpoch + total; + timerRunning = true; + showSelectionMarker = false; + } + } else { + // Stop timer: compute remaining time and keep as new setting + if (commonData->data.rtcValid) { + struct tm rtcCopy = commonData->data.rtcTime; + time_t nowEpoch = mktime(&rtcCopy); + time_t remaining = timerEndEpoch - nowEpoch; + if (remaining < 0) remaining = 0; + int rem = static_cast(remaining); + timerHours = rem / 3600; + rem -= timerHours * 3600; + timerMinutes = rem / 60; + timerSeconds = rem % 60; + } + timerRunning = false; + // marker will become visible again only after POS press + } + return 0; + } + + // In timer mode, other keys are passed through + return key; + } + + // Clock (A/D) modes key handling – like original PageClock + + // Time source (K1) + if (key == 1) { + switch (source) { + case 'G': source = 'R'; break; + case 'R': source = 'G'; break; + default: source = 'G'; break; + } + return 0; + } + + // MODE (K2) + if (key == 2) { + switch (mode) { + case 'A': mode = 'D'; break; + case 'D': mode = 'T'; break; + case 'T': mode = 'A'; break; + default: mode = 'A'; break; + } + setupKeys(); + return 0; + } + + // Time zone: Local / UTC (K5) + if (key == 5) { + switch (tz) { + case 'L': tz = 'U'; break; + case 'U': tz = 'L'; break; + default: tz = 'L'; break; + } + return 0; + } + + return key; + } + + int displayPage(PageData& pageData) + { + GwConfigHandler* config = commonData->config; + GwLog* logger = commonData->logger; + + setupTimerDefaults(); + setupKeys(); // ensure correct key labels for current mode + + static String svalue1old = ""; + static String unit1old = ""; + static String svalue2old = ""; + static String unit2old = ""; + static String svalue3old = ""; + static String unit3old = ""; + + static String svalue5old = ""; + static String svalue6old = ""; + + double value1 = 0; + double value2 = 0; + double value3 = 0; + + // Get config data + String lengthformat = config->getString(config->lengthFormat); + String dateformat = config->getString(config->dateFormat); + bool holdvalues = config->getBool(config->holdvalues); + String flashLED = config->getString(config->flashLED); + String backlightMode = config->getString(config->backlight); + + // Get boat values for GPS time + GwApi::BoatValue* bvalue1 = pageData.values[0]; // First element in list + String name1 = bvalue1->getName().c_str(); // Value name + name1 = name1.substring(0, 6); // String length limit for value name + if (simulation == false) { + value1 = bvalue1->value; // Value as double in SI unit + } else { + value1 = simtime++; // Simulation data for time value 11:36 in seconds + } // Other simulation data see OBP60Formatter.cpp + bool valid1 = bvalue1->valid; // Valid information + String svalue1 = formatValue(bvalue1, *commonData).svalue; // Formatted value + String unit1 = formatValue(bvalue1, *commonData).unit; // Unit of value + if (valid1 == true) { + svalue1old = svalue1; // Save old value + unit1old = unit1; // Save old unit + } + + // Get boat values for GPS date + GwApi::BoatValue* bvalue2 = pageData.values[1]; // Second element in list + String name2 = bvalue2->getName().c_str(); // Value name + name2 = name2.substring(0, 6); // String length limit for value name + value2 = bvalue2->value; // Value as double in SI unit + bool valid2 = bvalue2->valid; // Valid information + String svalue2 = formatValue(bvalue2, *commonData).svalue; // Formatted value + String unit2 = formatValue(bvalue2, *commonData).unit; // Unit of value + if (valid2 == true) { + svalue2old = svalue2; // Save old value + unit2old = unit2; // Save old unit + } + + // Get boat values for HDOP + GwApi::BoatValue* bvalue3 = pageData.values[2]; // Third element in list + String name3 = bvalue3->getName().c_str(); // Value name + name3 = name3.substring(0, 6); // String length limit for value name + value3 = bvalue3->value; // Value as double in SI unit + bool valid3 = bvalue3->valid; // Valid information + String svalue3 = formatValue(bvalue3, *commonData).svalue; // Formatted value + String unit3 = formatValue(bvalue3, *commonData).unit; // Unit of value + if (valid3 == true) { + svalue3old = svalue3; // Save old value + unit3old = unit3; // Save old unit + } + + // Optical warning by limit violation (unused) + if (String(flashLED) == "Limit Violation") { + setBlinkingLED(false); + setFlashLED(false); + } + + // Logging boat values + if (bvalue1 == NULL) return PAGE_OK; + LOG_DEBUG(GwLog::LOG, "Drawing at PageClock3, %s:%f, %s:%f", name1.c_str(), value1, name2.c_str(), value2); + + // Draw page + //*********************************************************** + + // Set display in partial refresh mode + getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + + getdisplay().setTextColor(commonData->fgcolor); + + time_t tv = mktime(&commonData->data.rtcTime) + timezone * 3600; + struct tm* local_tm = localtime(&tv); + + // Show values GPS date + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(10, 65); + if (holdvalues == false) { + if (source == 'G') { + // GPS value + getdisplay().print(svalue2); + } else if (commonData->data.rtcValid) { + // RTC value + if (tz == 'L') { + getdisplay().print(formatDate(dateformat, local_tm->tm_year + 1900, local_tm->tm_mon + 1, local_tm->tm_mday)); + } else { + getdisplay().print(formatDate(dateformat, commonData->data.rtcTime.tm_year + 1900, commonData->data.rtcTime.tm_mon + 1, commonData->data.rtcTime.tm_mday)); + } + } else { + getdisplay().print("---"); + } + } else { + getdisplay().print(svalue2old); + } + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().setCursor(10, 95); + getdisplay().print("Date"); // Name + + // Horizontal separator left + getdisplay().fillRect(0, 149, 60, 3, commonData->fgcolor); + + // Show values GPS time (small text bottom left) + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(10, 250); + if (holdvalues == false) { + if (source == 'G') { + getdisplay().print(svalue1); // Value + } else if (commonData->data.rtcValid) { + if (tz == 'L') { + getdisplay().print(formatTime('s', local_tm->tm_hour, local_tm->tm_min, local_tm->tm_sec)); + } else { + getdisplay().print(formatTime('s', commonData->data.rtcTime.tm_hour, commonData->data.rtcTime.tm_min, commonData->data.rtcTime.tm_sec)); + } + } else { + getdisplay().print("---"); + } + } else { + getdisplay().print(svalue1old); + } + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().setCursor(10, 220); + getdisplay().print("Time"); // Name + + // Show values sunrise + String sunrise = "---"; + if ((valid1 and valid2 and valid3 == true) or (homevalid and commonData->data.rtcValid)) { + sunrise = String(commonData->sundata.sunriseHour) + ":" + String(commonData->sundata.sunriseMinute + 100).substring(1); + svalue5old = sunrise; + } else if (simulation) { + sunrise = String("06:42"); + } + + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(335, 65); + if (holdvalues == false) getdisplay().print(sunrise); // Value + else getdisplay().print(svalue5old); + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().setCursor(335, 95); + getdisplay().print("SunR"); // Name + + // Horizontal separator right + getdisplay().fillRect(340, 149, 80, 3, commonData->fgcolor); + + // Show values sunset + String sunset = "---"; + if ((valid1 and valid2 and valid3 == true) or (homevalid and commonData->data.rtcValid)) { + sunset = String(commonData->sundata.sunsetHour) + ":" + String(commonData->sundata.sunsetMinute + 100).substring(1); + svalue6old = sunset; + } else if (simulation) { + sunset = String("21:03"); + } + + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(335, 250); + if (holdvalues == false) getdisplay().print(sunset); // Value + else getdisplay().print(svalue6old); + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().setCursor(335, 220); + getdisplay().print("SunS"); // Name + + //******************************************************************************************* + + if (mode == 'T') { + // TIMER MODE: countdown timer HH:MM:SS in the center with 7-segment font + + int dispH = timerHours; + int dispM = timerMinutes; + int dispS = timerSeconds; + + // Update remaining time if timer is running (based on RTC) + if (timerRunning && commonData->data.rtcValid) { + struct tm rtcCopy = commonData->data.rtcTime; + time_t nowEpoch = mktime(&rtcCopy); + time_t remaining = timerEndEpoch - nowEpoch; + if (remaining <= 0) { + remaining = 0; + timerRunning = false; + } + int rem = static_cast(remaining); + dispH = rem / 3600; + rem -= dispH * 3600; + dispM = rem / 60; + dispS = rem % 60; + } + + char buf[9]; // "HH:MM:SS" + snprintf(buf, sizeof(buf), "%02d:%02d:%02d", dispH, dispM, dispS); + String timeStr = String(buf); + + // Clear central area and draw large digital time + getdisplay().fillRect(0, 110, getdisplay().width(), 80, commonData->bgcolor); + + getdisplay().setFont(&DSEG7Classic_BoldItalic60pt7b); + + // Determine widths for digits and colon to position selection underline exactly + int16_t x0, y0; + uint16_t wDigit, hDigit; + uint16_t wColon, hColon; + + getdisplay().getTextBounds("00", 0, 0, &x0, &y0, &wDigit, &hDigit); + getdisplay().getTextBounds(":", 0, 0, &x0, &y0, &wColon, &hColon); + + uint16_t totalWidth = 3 * wDigit + 2 * wColon; + + int16_t baseX = (static_cast(getdisplay().width()) - static_cast(totalWidth)) / 2; + int16_t centerY = 150; + + // Draw time string centered + int16_t x1b, y1b; + uint16_t wb, hb; + getdisplay().getTextBounds(timeStr, 0, 0, &x1b, &y1b, &wb, &hb); + int16_t textX = (static_cast(getdisplay().width()) - static_cast(wb)) / 2; + int16_t textY = centerY + hb / 2; + + getdisplay().setCursor(textX, textY); + getdisplay().print(timeStr); + + // Selection marker (only visible when not running and POS pressed) + if (!timerRunning && showSelectionMarker) { + int16_t selX = baseX; + if (selectedField == 1) { + selX = baseX + wDigit + wColon; // minutes start + } else if (selectedField == 2) { + selX = baseX + 2 * wDigit + 2 * wColon; // seconds start + } + + int16_t underlineY = centerY + hb / 2 + 2; + getdisplay().fillRect(selX, underlineY, wDigit, 2, commonData->fgcolor); + } + + // Small indicators: timezone and source + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(180, 110); + if (holdvalues == false) { + getdisplay().print(tz == 'L' ? "LOT" : "UTC"); + } else { + getdisplay().print(unit2old); // date unit + } + + getdisplay().setCursor(185, 190); + if (source == 'G') { + getdisplay().print("GPS"); + } else { + getdisplay().print("RTC"); + } + + } else if (mode == 'D') { + // DIGITAL CLOCK MODE: large 7-segment time based on GPS/RTC + + int hour24 = 0; + int minute24 = 0; + int second24 = 0; + + if (source == 'R' && commonData->data.rtcValid) { + time_t tv2 = mktime(&commonData->data.rtcTime); + if (tz == 'L') { + tv2 += static_cast(timezone * 3600); + } + struct tm* tm2 = localtime(&tv2); + hour24 = tm2->tm_hour; + minute24 = tm2->tm_min; + second24 = tm2->tm_sec; + } else { + double t = value1; + if (tz == 'L') { + t += timezone * 3600; + } + if (t >= 86400) t -= 86400; + if (t < 0) t += 86400; + hour24 = static_cast(t / 3600.0); + int rest = static_cast(t) - hour24 * 3600; + minute24 = rest / 60; + second24 = rest % 60; + } + + char buf[9]; // "HH:MM:SS" + snprintf(buf, sizeof(buf), "%02d:%02d:%02d", hour24, minute24, second24); + String timeStr = String(buf); + + getdisplay().fillRect(0, 110, getdisplay().width(), 80, commonData->bgcolor); + + getdisplay().setFont(&DSEG7Classic_BoldItalic60pt7b); + + int16_t x1b, y1b; + uint16_t wb, hb; + getdisplay().getTextBounds(timeStr, 0, 0, &x1b, &y1b, &wb, &hb); + + int16_t x = (static_cast(getdisplay().width()) - static_cast(wb)) / 2; + int16_t y = 150 + hb / 2; + + getdisplay().setCursor(x, y); + getdisplay().print(timeStr); + + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(180, 110); + if (holdvalues == false) { + getdisplay().print(tz == 'L' ? "LOT" : "UTC"); + } else { + getdisplay().print(unit2old); // date unit + } + + getdisplay().setCursor(185, 190); + if (source == 'G') { + getdisplay().print("GPS"); + } else { + getdisplay().print("RTC"); + } + + } else { + // ANALOG CLOCK MODE (mode == 'A') + + int rInstrument = 110; // Radius of clock + float pi = 3.141592; + + getdisplay().fillCircle(200, 150, rInstrument + 10, commonData->fgcolor); // Outer circle + getdisplay().fillCircle(200, 150, rInstrument + 7, commonData->bgcolor); // Outer circle + + for (int i = 0; i < 360; i = i + 1) + { + // Scaling values + float x = 200 + (rInstrument - 30) * sin(i / 180.0 * pi); // x-coordinate dots + float y = 150 - (rInstrument - 30) * cos(i / 180.0 * pi); // y-coordinate dots + const char* ii = ""; + switch (i) + { + case 0: ii = "12"; break; + case 90: ii = "3"; break; + case 180: ii = "6"; break; + case 270: ii = "9"; break; + default: break; + } + + // Print text centered on position x, y + int16_t x1c, y1c; // Return values of getTextBounds + uint16_t wc, hc; // Return values of getTextBounds + getdisplay().getTextBounds(ii, int(x), int(y), &x1c, &y1c, &wc, &hc); // Calc width of new string + getdisplay().setCursor(x - wc / 2, y + hc / 2); + if (i % 90 == 0) { + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().print(ii); + } + + // Draw sub scale with dots + float sinx = 0; + float cosx = 0; + if (i % 6 == 0) { + float x1d = 200 + rInstrument * sin(i / 180.0 * pi); + float y1d = 150 - rInstrument * cos(i / 180.0 * pi); + getdisplay().fillCircle((int)x1d, (int)y1d, 2, commonData->fgcolor); + sinx = sin(i / 180.0 * pi); + cosx = cos(i / 180.0 * pi); + } + + // Draw sub scale with lines (two triangles) + if (i % 30 == 0) { + float dx = 2; // Line thickness = 2*dx+1 + float xx1 = -dx; + float xx2 = +dx; + float yy1 = -(rInstrument - 10); + float yy2 = -(rInstrument + 10); + getdisplay().fillTriangle(200 + (int)(cosx * xx1 - sinx * yy1), 150 + (int)(sinx * xx1 + cosx * yy1), + 200 + (int)(cosx * xx2 - sinx * yy1), 150 + (int)(sinx * xx2 + cosx * yy1), + 200 + (int)(cosx * xx1 - sinx * yy2), 150 + (int)(sinx * xx1 + cosx * yy2), commonData->fgcolor); + getdisplay().fillTriangle(200 + (int)(cosx * xx2 - sinx * yy1), 150 + (int)(sinx * xx2 + cosx * yy1), + 200 + (int)(cosx * xx1 - sinx * yy2), 150 + (int)(sinx * xx1 + cosx * yy2), + 200 + (int)(cosx * xx2 - sinx * yy2), 150 + (int)(sinx * xx2 + cosx * yy2), commonData->fgcolor); + } + } + + // Print Unit in clock + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().setCursor(175, 110); + if (holdvalues == false) { + getdisplay().print(tz == 'L' ? "LOT" : "UTC"); + } else { + getdisplay().print(unit2old); // date unit + } + + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(185, 190); + if (source == 'G') { + getdisplay().print("GPS"); + } else { + getdisplay().print("RTC"); + } + + // Clock values + double hour = 0; + double minute = 0; + if (source == 'R') { + if (tz == 'L') { + time_t tv2 = mktime(&commonData->data.rtcTime) + timezone * 3600; + struct tm* local_tm2 = localtime(&tv2); + minute = local_tm2->tm_min; + hour = local_tm2->tm_hour; + } else { + minute = commonData->data.rtcTime.tm_min; + hour = commonData->data.rtcTime.tm_hour; + } + hour += minute / 60; + } else { + if (tz == 'L') { + value1 += timezone * 3600; + } + if (value1 > 86400) { value1 -= 86400; } + if (value1 < 0) { value1 += 86400; } + hour = (value1 / 3600.0); + // minute = (hour - int(hour)) * 3600.0 / 60.0; // Analog minute pointer smooth moving + minute = int((hour - int(hour)) * 3600.0 / 60.0); // Jumping minute pointer from minute to minute + } + if (hour > 12) { + hour -= 12.0; + } + LOG_DEBUG(GwLog::DEBUG, "... PageClock3, value1: %f hour: %f minute:%f", value1, hour, minute); + + // Draw hour pointer + float startwidth = 8; // Start width of pointer + if (valid1 == true || (source == 'R' && commonData->data.rtcValid) || holdvalues == true || simulation == true) { + float sinx = sin(hour * 30.0 * pi / 180); // Hour + float cosx = cos(hour * 30.0 * pi / 180); + // Normal pointer + // Pointer as triangle with center base 2*width + float xx1 = -startwidth; + float xx2 = startwidth; + float yy1 = -startwidth; + float yy2 = -(rInstrument * 0.5); + getdisplay().fillTriangle(200 + (int)(cosx * xx1 - sinx * yy1), 150 + (int)(sinx * xx1 + cosx * yy1), + 200 + (int)(cosx * xx2 - sinx * yy1), 150 + (int)(sinx * xx2 + cosx * yy1), + 200 + (int)(cosx * 0 - sinx * yy2), 150 + (int)(sinx * 0 + cosx * yy2), commonData->fgcolor); + // Inverted pointer + // Pointer as triangle with center base 2*width + float endwidth = 2; // End width of pointer + float ix1 = endwidth; + float ix2 = -endwidth; + float iy1 = -(rInstrument * 0.5); + float iy2 = -endwidth; + getdisplay().fillTriangle(200 + (int)(cosx * ix1 - sinx * iy1), 150 + (int)(sinx * ix1 + cosx * iy1), + 200 + (int)(cosx * ix2 - sinx * iy1), 150 + (int)(sinx * ix2 + cosx * iy1), + 200 + (int)(cosx * 0 - sinx * iy2), 150 + (int)(sinx * 0 + cosx * iy2), commonData->fgcolor); + } + + // Draw minute pointer + startwidth = 8; // Start width of pointer + if (valid1 == true || (source == 'R' && commonData->data.rtcValid) || holdvalues == true || simulation == true) { + float sinx = sin(minute * 6.0 * pi / 180); // Minute + float cosx = cos(minute * 6.0 * pi / 180); + // Normal pointer + // Pointer as triangle with center base 2*width + float xx1 = -startwidth; + float xx2 = startwidth; + float yy1 = -startwidth; + float yy2 = -(rInstrument - 15); + getdisplay().fillTriangle(200 + (int)(cosx * xx1 - sinx * yy1), 150 + (int)(sinx * xx1 + cosx * yy1), + 200 + (int)(cosx * xx2 - sinx * yy1), 150 + (int)(sinx * xx2 + cosx * yy1), + 200 + (int)(cosx * 0 - sinx * yy2), 150 + (int)(sinx * 0 + cosx * yy2), commonData->fgcolor); + // Inverted pointer + // Pointer as triangle with center base 2*width + float endwidth = 2; // End width of pointer + float ix1 = endwidth; + float ix2 = -endwidth; + float iy1 = -(rInstrument - 15); + float iy2 = -endwidth; + getdisplay().fillTriangle(200 + (int)(cosx * ix1 - sinx * iy1), 150 + (int)(sinx * ix1 + cosx * iy1), + 200 + (int)(cosx * ix2 - sinx * iy1), 150 + (int)(sinx * ix2 + cosx * iy1), + 200 + (int)(cosx * 0 - sinx * iy2), 150 + (int)(sinx * 0 + cosx * iy2), commonData->fgcolor); + } + + // Center circle + getdisplay().fillCircle(200, 150, startwidth + 6, commonData->bgcolor); + getdisplay().fillCircle(200, 150, startwidth + 4, commonData->fgcolor); + } + + return PAGE_UPDATE; + }; +}; + +// Static member definitions +bool PageClock3::timerInitialized = false; +bool PageClock3::timerRunning = false; +int PageClock3::timerHours = 0; +int PageClock3::timerMinutes = 0; +int PageClock3::timerSeconds = 0; +int PageClock3::selectedField = 0; +bool PageClock3::showSelectionMarker = true; +time_t PageClock3::timerEndEpoch = 0; + +static Page* createPage(CommonData& common) +{ + return new PageClock3(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 + * we provide the number of user parameters we expect (0 here) + * and we provide the names of the fixed values we need + */ +PageDescription registerPageClock3( + "Clock3", // Page name + createPage, // Action + 0, // Number of bus values depends on selection in Web configuration + {"GPST", "GPSD", "HDOP"}, // Bus values we need in the page + true // Show display header on/off +); + +#endif + diff --git a/lib/obp60task/PageClockDigital.new b/lib/obp60task/PageClockDigital.new new file mode 100644 index 0000000..d4a3984 --- /dev/null +++ b/lib/obp60task/PageClockDigital.new @@ -0,0 +1,224 @@ +#if defined BOARD_OBP60S3 || defined BOARD_OBP40S3 + +#include "Pagedata.h" +#include "OBP60Extensions.h" + +/** + * Simple digital clock page. + * + * - Shows system time as large digital value in the center + * - Uses same data sources and configuration as PageClock (GPS / RTC, time zone) + * - Keys: + * K1: toggle time source (GPS / RTC) + * K5: toggle time zone (Local / UTC) + * K11: keylock + */ +class PageClockDigital : public Page +{ + bool simulation = false; + int simtime = 0; + char source; // time source (R)TC | (G)PS + char tz = 'L'; // time zone (L)ocal | (U)TC + double timezone = 0.0; + +public: + PageClockDigital(CommonData& common) + { + commonData = &common; + common.logger->logDebug(GwLog::LOG, "Instantiate PageClockDigital"); + + simulation = common.config->getBool(common.config->useSimuData); + timezone = common.config->getString(common.config->timeZone).toDouble(); + +#ifdef BOARD_OBP60S3 + source = 'G'; // default to GPS time on OBP60 +#endif +#ifdef BOARD_OBP40S3 + source = 'R'; // default to RTC time on OBP40 +#endif + simtime = 38160; // time value 11:36 for simulation (seconds) + } + + virtual void setupKeys() + { + Page::setupKeys(); + commonData->keydata[0].label = "SRC"; + commonData->keydata[4].label = "TZ"; + } + + // Key functions + virtual int handleKey(int key) + { + // Time source + if (key == 1) { + switch (source) { + case 'G': + source = 'R'; + break; + case 'R': + source = 'G'; + break; + default: + source = 'G'; + break; + } + return 0; + } + + // Time zone: Local / UTC + if (key == 5) { + switch (tz) { + case 'L': + tz = 'U'; + break; + case 'U': + tz = 'L'; + break; + default: + tz = 'L'; + break; + } + return 0; + } + + // Keylock function + if (key == 11) { // Code for keylock + commonData->keylock = !commonData->keylock; + return 0; // Commit the key + } + return key; + } + + int displayPage(PageData& pageData) + { + GwConfigHandler* config = commonData->config; + + static String svalueTimeOld = ""; + static String svalueDateOld = ""; + + // Get config data + bool holdvalues = config->getBool(config->holdvalues); + String flashLED = config->getString(config->flashLED); + + // Get boat values for GPS time and date (same as PageClock) + if (pageData.values.size() < 2) { + return PAGE_OK; + } + + GwApi::BoatValue* bvalueTime = pageData.values[0]; + GwApi::BoatValue* bvalueDate = pageData.values[1]; + + if (bvalueTime == nullptr || bvalueDate == nullptr) { + return PAGE_OK; + } + + double valueTime = 0; + if (!simulation) { + valueTime = bvalueTime->value; // Value as double in SI unit (seconds) + } else { + valueTime = simtime++; // Simulation data + } + bool validTime = bvalueTime->valid; + String svalueTime = formatValue(bvalueTime, *commonData).svalue; // formatted time string + if (validTime) { + svalueTimeOld = svalueTime; // Save old value + } + + bool validDate = bvalueDate->valid; + String svalueDate = formatValue(bvalueDate, *commonData).svalue; // formatted date string + if (validDate) { + svalueDateOld = svalueDate; // Save old value + } + + // Optical warning by limit violation (unused) + if (flashLED == "Limit Violation") { + setBlinkingLED(false); + setFlashLED(false); + } + + // Draw page + //*********************************************************** + + // Set display in partial refresh mode + getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + getdisplay().setTextColor(commonData->fgcolor); + + // Build time string depending on source and configuration + String timeStr = "---"; + + if (!holdvalues) { + if (source == 'G') { + // GPS value as formatted by formatter + timeStr = svalueTime; + } else if (commonData->data.rtcValid) { + // RTC value + time_t tv = mktime(&commonData->data.rtcTime); + if (tz == 'L') { + tv += static_cast(timezone * 3600); + } + struct tm* local_tm = localtime(&tv); + timeStr = formatTime('s', local_tm->tm_hour, local_tm->tm_min, local_tm->tm_sec); + } + } else { + timeStr = svalueTimeOld; + } + + // Clear central area and draw large digital time + getdisplay().fillRect(0, 80, getdisplay().width(), 140, commonData->bgcolor); + + getdisplay().setFont(&DSEG7Classic_BoldItalic60pt7b); + + int16_t x1, y1; + uint16_t w, h; + getdisplay().getTextBounds(timeStr, 0, 0, &x1, &y1, &w, &h); + + int16_t x = (static_cast(getdisplay().width()) - static_cast(w)) / 2; + int16_t y = (static_cast(getdisplay().height()) + static_cast(h)) / 2; + + getdisplay().setCursor(x, y); + getdisplay().print(timeStr); + + // Show date in the upper left corner + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().setCursor(10, 40); + getdisplay().print("Date"); + + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(10, 60); + if (!holdvalues) { + getdisplay().print(svalueDate); + } else { + getdisplay().print(svalueDateOld); + } + + // Show small labels for source and timezone in the lower right corner + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(getdisplay().width() - 80, getdisplay().height() - 40); + getdisplay().print(source == 'G' ? "GPS" : "RTC"); + + getdisplay().setCursor(getdisplay().width() - 80, getdisplay().height() - 20); + getdisplay().print(tz == 'L' ? "LOC" : "UTC"); + + return PAGE_UPDATE; + }; +}; + +static Page* createPage(CommonData& common) +{ + return new PageClockDigital(common); +} + +/** + * Register page so it can be selected in the configuration. + * Uses the same fixed values as PageClock. + */ +PageDescription registerPageClockDigital( + "ClockDigital", // Page name + createPage, // Action + 0, // Number of user parameters + {"GPST", "GPSD", "HDOP"}, // Bus values we need in the page + true // Show display header on/off +); + +#endif + From fbba6ffff260a2ba872afbee45043937fdd23841 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Thu, 5 Feb 2026 23:52:10 +0100 Subject: [PATCH 108/121] Fix for PageClock --- lib/obp60task/PageClock.cpp | 42 ++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/obp60task/PageClock.cpp b/lib/obp60task/PageClock.cpp index 3cf097a..b39ed1d 100644 --- a/lib/obp60task/PageClock.cpp +++ b/lib/obp60task/PageClock.cpp @@ -132,8 +132,8 @@ public: commonData->keydata[4].label = "RUN"; } else { // Clock modes: like original - commonData->keydata[0].label = "SRC"; - commonData->keydata[1].label = "MODE"; + commonData->keydata[0].label = "MODE"; + commonData->keydata[1].label = "SRC"; commonData->keydata[4].label = "TZ"; } } @@ -220,18 +220,8 @@ public: // Clock (A/D) modes key handling – like original PageClock - // Time source (K1) + // MODE (K1) if (key == 1) { - switch (source) { - case 'G': source = 'R'; break; - case 'R': source = 'G'; break; - default: source = 'G'; break; - } - return 0; - } - - // MODE (K2) - if (key == 2) { switch (mode) { case 'A': mode = 'D'; break; case 'D': mode = 'T'; break; @@ -241,6 +231,16 @@ public: setupKeys(); return 0; } + + // Time source (K2) + if (key == 2) { + switch (source) { + case 'G': source = 'R'; break; + case 'R': source = 'G'; break; + default: source = 'G'; break; + } + return 0; + } // Time zone: Local / UTC (K5) if (key == 5) { @@ -466,7 +466,7 @@ public: // Clear central area and draw large digital time getdisplay().fillRect(0, 110, getdisplay().width(), 80, commonData->bgcolor); - getdisplay().setFont(&DSEG7Classic_BoldItalic60pt7b); + getdisplay().setFont(&DSEG7Classic_BoldItalic30pt7b); // Determine widths for digits and colon to position selection underline exactly int16_t x0, y0; @@ -500,20 +500,20 @@ public: selX = baseX + 2 * wDigit + 2 * wColon; // seconds start } - int16_t underlineY = centerY + hb / 2 + 2; - getdisplay().fillRect(selX, underlineY, wDigit, 2, commonData->fgcolor); + int16_t underlineY = centerY + hb / 2 + 5; + getdisplay().fillRect(selX, underlineY, wDigit, 6, commonData->fgcolor); } // Small indicators: timezone and source getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(180, 110); + getdisplay().setCursor(185, 110); if (holdvalues == false) { getdisplay().print(tz == 'L' ? "LOT" : "UTC"); } else { getdisplay().print(unit2old); // date unit } - getdisplay().setCursor(185, 190); + getdisplay().setCursor(185, 210); if (source == 'G') { getdisplay().print("GPS"); } else { @@ -555,7 +555,7 @@ public: getdisplay().fillRect(0, 110, getdisplay().width(), 80, commonData->bgcolor); - getdisplay().setFont(&DSEG7Classic_BoldItalic60pt7b); + getdisplay().setFont(&DSEG7Classic_BoldItalic30pt7b); int16_t x1b, y1b; uint16_t wb, hb; @@ -568,14 +568,14 @@ public: getdisplay().print(timeStr); getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(180, 110); + getdisplay().setCursor(185, 110); if (holdvalues == false) { getdisplay().print(tz == 'L' ? "LOT" : "UTC"); } else { getdisplay().print(unit2old); // date unit } - getdisplay().setCursor(185, 190); + getdisplay().setCursor(185, 210); if (source == 'G') { getdisplay().print("GPS"); } else { From a70d976a6eb4adce0da7a1718f73cc6341be94b6 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 6 Feb 2026 11:46:35 +0100 Subject: [PATCH 109/121] Change design for PageClock --- lib/obp60task/PageClock.cpp | 301 +++++++++++++++++++----------------- 1 file changed, 163 insertions(+), 138 deletions(-) diff --git a/lib/obp60task/PageClock.cpp b/lib/obp60task/PageClock.cpp index b39ed1d..b1ec8a4 100644 --- a/lib/obp60task/PageClock.cpp +++ b/lib/obp60task/PageClock.cpp @@ -35,17 +35,17 @@ class PageClock : public Page int simtime; bool keylock = false; #ifdef BOARD_OBP60S3 - char source = 'G'; // time source (R)TC | (G)PS | (N)TP + char source = 'G'; // Time source (R)TC | (G)PS | (N)TP #endif #ifdef BOARD_OBP40S3 char source = 'R'; // time source (R)TC | (G)PS | (N)TP #endif - char mode = 'A'; // display mode (A)nalog | (D)igital | race (T)imer - char tz = 'L'; // time zone (L)ocal | (U)TC - double timezone = 0; // there are timezones with non int offsets, e.g. 5.5 or 5.75 + char mode = 'A'; // Display mode (A)nalog | (D)igital | race (T)imer + char tz = 'L'; // Time zone (L)ocal | (U)TC + double timezone = 0; // There are timezones with non int offsets, e.g. 5.5 or 5.75 double homelat; double homelon; - bool homevalid = false; // homelat and homelon are valid + bool homevalid = false; // Homelat and homelon are valid // Timer state (static so it survives page switches) static bool timerInitialized; @@ -53,9 +53,13 @@ class PageClock : public Page static int timerHours; static int timerMinutes; static int timerSeconds; + // Initial timer setting at start (so we can restore it) + static int timerStartHours; + static int timerStartMinutes; + static int timerStartSeconds; static int selectedField; // 0 = hours, 1 = minutes, 2 = seconds static bool showSelectionMarker; - static time_t timerEndEpoch; // absolute end time based on RTC + static time_t timerEndEpoch; // Absolute end time based on RTC void setupTimerDefaults() { @@ -65,16 +69,20 @@ class PageClock : public Page timerHours = 0; timerMinutes = 0; timerSeconds = 0; + timerStartHours = 0; + timerStartMinutes = 0; + timerStartSeconds = 0; selectedField = 0; showSelectionMarker = true; timerEndEpoch = 0; } } + // Limiter for overrun settings values static int clamp(int value, int minVal, int maxVal) { - if (value < minVal) return minVal; - if (value > maxVal) return maxVal; + if (value < minVal) return maxVal; + if (value > maxVal) return minVal; return value; } @@ -189,6 +197,11 @@ public: // Start timer if a non-zero duration is set int total = totalTimerSeconds(); if (total > 0 && commonData->data.rtcValid) { + // Remember initial timer setting at start + timerStartHours = timerHours; + timerStartMinutes = timerMinutes; + timerStartSeconds = timerSeconds; + struct tm rtcCopy = commonData->data.rtcTime; time_t nowEpoch = mktime(&rtcCopy); timerEndEpoch = nowEpoch + total; @@ -196,18 +209,10 @@ public: showSelectionMarker = false; } } else { - // Stop timer: compute remaining time and keep as new setting - if (commonData->data.rtcValid) { - struct tm rtcCopy = commonData->data.rtcTime; - time_t nowEpoch = mktime(&rtcCopy); - time_t remaining = timerEndEpoch - nowEpoch; - if (remaining < 0) remaining = 0; - int rem = static_cast(remaining); - timerHours = rem / 3600; - rem -= timerHours * 3600; - timerMinutes = rem / 60; - timerSeconds = rem % 60; - } + // Stop timer: restore initial start setting + timerHours = timerStartHours; + timerMinutes = timerStartMinutes; + timerSeconds = timerStartSeconds; timerRunning = false; // marker will become visible again only after POS press } @@ -261,7 +266,7 @@ public: GwLog* logger = commonData->logger; setupTimerDefaults(); - setupKeys(); // ensure correct key labels for current mode + setupKeys(); // Ensure correct key labels for current mode static String svalue1old = ""; static String unit1old = ""; @@ -306,7 +311,7 @@ public: String name2 = bvalue2->getName().c_str(); // Value name name2 = name2.substring(0, 6); // String length limit for value name value2 = bvalue2->value; // Value as double in SI unit - bool valid2 = bvalue2->valid; // Valid information + bool valid2 = bvalue2->valid; // Valid informationgetdisplay().print("RTC"); String svalue2 = formatValue(bvalue2, *commonData).svalue; // Formatted value String unit2 = formatValue(bvalue2, *commonData).unit; // Unit of value if (valid2 == true) { @@ -348,109 +353,41 @@ public: time_t tv = mktime(&commonData->data.rtcTime) + timezone * 3600; struct tm* local_tm = localtime(&tv); - // Show values GPS date - getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(10, 65); - if (holdvalues == false) { - if (source == 'G') { - // GPS value - getdisplay().print(svalue2); - } else if (commonData->data.rtcValid) { - // RTC value - if (tz == 'L') { - getdisplay().print(formatDate(dateformat, local_tm->tm_year + 1900, local_tm->tm_mon + 1, local_tm->tm_mday)); - } else { - getdisplay().print(formatDate(dateformat, commonData->data.rtcTime.tm_year + 1900, commonData->data.rtcTime.tm_mon + 1, commonData->data.rtcTime.tm_mday)); - } - } else { - getdisplay().print("---"); - } - } else { - getdisplay().print(svalue2old); - } - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(10, 95); - getdisplay().print("Date"); // Name - - // Horizontal separator left - getdisplay().fillRect(0, 149, 60, 3, commonData->fgcolor); - - // Show values GPS time (small text bottom left) - getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(10, 250); - if (holdvalues == false) { - if (source == 'G') { - getdisplay().print(svalue1); // Value - } else if (commonData->data.rtcValid) { - if (tz == 'L') { - getdisplay().print(formatTime('s', local_tm->tm_hour, local_tm->tm_min, local_tm->tm_sec)); - } else { - getdisplay().print(formatTime('s', commonData->data.rtcTime.tm_hour, commonData->data.rtcTime.tm_min, commonData->data.rtcTime.tm_sec)); - } - } else { - getdisplay().print("---"); - } - } else { - getdisplay().print(svalue1old); - } - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(10, 220); - getdisplay().print("Time"); // Name - - // Show values sunrise - String sunrise = "---"; - if ((valid1 and valid2 and valid3 == true) or (homevalid and commonData->data.rtcValid)) { - sunrise = String(commonData->sundata.sunriseHour) + ":" + String(commonData->sundata.sunriseMinute + 100).substring(1); - svalue5old = sunrise; - } else if (simulation) { - sunrise = String("06:42"); - } - - getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(335, 65); - if (holdvalues == false) getdisplay().print(sunrise); // Value - else getdisplay().print(svalue5old); - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(335, 95); - getdisplay().print("SunR"); // Name - - // Horizontal separator right - getdisplay().fillRect(340, 149, 80, 3, commonData->fgcolor); - - // Show values sunset - String sunset = "---"; - if ((valid1 and valid2 and valid3 == true) or (homevalid and commonData->data.rtcValid)) { - sunset = String(commonData->sundata.sunsetHour) + ":" + String(commonData->sundata.sunsetMinute + 100).substring(1); - svalue6old = sunset; - } else if (simulation) { - sunset = String("21:03"); - } - - getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(335, 250); - if (holdvalues == false) getdisplay().print(sunset); // Value - else getdisplay().print(svalue6old); - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(335, 220); - getdisplay().print("SunS"); // Name - - //******************************************************************************************* - if (mode == 'T') { // TIMER MODE: countdown timer HH:MM:SS in the center with 7-segment font + //************************************************************************ int dispH = timerHours; int dispM = timerMinutes; int dispS = timerSeconds; // Update remaining time if timer is running (based on RTC) - if (timerRunning && commonData->data.rtcValid) { + if (timerRunning && commonData->data.rtcValid) { struct tm rtcCopy = commonData->data.rtcTime; time_t nowEpoch = mktime(&rtcCopy); time_t remaining = timerEndEpoch - nowEpoch; + if(remaining <= 5){ + // Short buzzer alarm (100% power) + setBuzzerPower(100); + buzzer(TONE4, 75); + setBuzzerPower(config->getInt(config->buzzerPower)); + } if (remaining <= 0) { remaining = 0; timerRunning = false; + commonData->keydata[4].label = "RUN"; + // Buzzer alarm (100% power) + setBuzzerPower(100); + buzzer(TONE4, 800); + setBuzzerPower(config->getInt(config->buzzerPower)); + + // When countdown is finished, restore the initial start time + timerHours = timerStartHours; + timerMinutes = timerStartMinutes; + timerSeconds = timerStartSeconds; + } + else{ + commonData->keydata[4].label = "STOP"; } int rem = static_cast(remaining); dispH = rem / 3600; @@ -493,35 +430,26 @@ public: // Selection marker (only visible when not running and POS pressed) if (!timerRunning && showSelectionMarker) { - int16_t selX = baseX; + int16_t selX = baseX - 8; // Hours start if (selectedField == 1) { - selX = baseX + wDigit + wColon; // minutes start + selX = baseX + wDigit + wColon; // Minutes start } else if (selectedField == 2) { - selX = baseX + 2 * wDigit + 2 * wColon; // seconds start + selX = baseX + 2 * wDigit + 2 * wColon + 12; // Seconds start } int16_t underlineY = centerY + hb / 2 + 5; - getdisplay().fillRect(selX, underlineY, wDigit, 6, commonData->fgcolor); + //getdisplay().fillRect(selX, underlineY, wDigit, 6, commonData->fgcolor); + getdisplay().fillRoundRect(selX, underlineY, wDigit, 6, 2, commonData->fgcolor); } - // Small indicators: timezone and source - getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(185, 110); - if (holdvalues == false) { - getdisplay().print(tz == 'L' ? "LOT" : "UTC"); - } else { - getdisplay().print(unit2old); // date unit - } - - getdisplay().setCursor(185, 210); - if (source == 'G') { - getdisplay().print("GPS"); - } else { - getdisplay().print("RTC"); - } + // Page label + getdisplay().setFont(&Ubuntu_Bold16pt8b); + getdisplay().setCursor(80, 70); + getdisplay().print("Count Dow Timer"); } else if (mode == 'D') { // DIGITAL CLOCK MODE: large 7-segment time based on GPS/RTC + //********************************************************** int hour24 = 0; int minute24 = 0; @@ -565,26 +493,120 @@ public: int16_t y = 150 + hb / 2; getdisplay().setCursor(x, y); - getdisplay().print(timeStr); + getdisplay().print(timeStr); // Display actual time + // Small indicators: timezone and source getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(185, 110); - if (holdvalues == false) { - getdisplay().print(tz == 'L' ? "LOT" : "UTC"); - } else { - getdisplay().print(unit2old); // date unit - } - getdisplay().setCursor(185, 210); + getdisplay().setCursor(x, 110); if (source == 'G') { getdisplay().print("GPS"); } else { getdisplay().print("RTC"); } + getdisplay().setCursor(x + 40, 110); + if (holdvalues == false) { + getdisplay().print(tz == 'L' ? "LOT" : "UTC"); + } else { + getdisplay().print(unit2old); // date unit + } + + // Page label + getdisplay().setFont(&Ubuntu_Bold16pt8b); + getdisplay().setCursor(100, 70); + getdisplay().print("Digital Clock"); + } else { // ANALOG CLOCK MODE (mode == 'A') + //******************************** + // Show values GPS date + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(10, 65); + if (holdvalues == false) { + if (source == 'G') { + // GPS value + getdisplay().print(svalue2); + } else if (commonData->data.rtcValid) { + // RTC value + if (tz == 'L') { + getdisplay().print(formatDate(dateformat, local_tm->tm_year + 1900, local_tm->tm_mon + 1, local_tm->tm_mday)); + } else { + getdisplay().print(formatDate(dateformat, commonData->data.rtcTime.tm_year + 1900, commonData->data.rtcTime.tm_mon + 1, commonData->data.rtcTime.tm_mday)); + } + } else { + getdisplay().print("---"); + } + } else { + getdisplay().print(svalue2old); + } + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().setCursor(10, 95); + getdisplay().print("Date"); // Name + + // Horizontal separator left + getdisplay().fillRect(0, 149, 60, 3, commonData->fgcolor); + + // Show values GPS time (small text bottom left) + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(10, 250); + if (holdvalues == false) { + if (source == 'G') { + getdisplay().print(svalue1); // Value + } else if (commonData->data.rtcValid) { + if (tz == 'L') { + getdisplay().print(formatTime('s', local_tm->tm_hour, local_tm->tm_min, local_tm->tm_sec)); + } else { + getdisplay().print(formatTime('s', commonData->data.rtcTime.tm_hour, commonData->data.rtcTime.tm_min, commonData->data.rtcTime.tm_sec)); + } + } else { + getdisplay().print("---"); + } + } else { + getdisplay().print(svalue1old); + } + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().setCursor(10, 220); + getdisplay().print("Time"); // Name + + // Show values sunrise + String sunrise = "---"; + if ((valid1 and valid2 and valid3 == true) or (homevalid and commonData->data.rtcValid)) { + sunrise = String(commonData->sundata.sunriseHour) + ":" + String(commonData->sundata.sunriseMinute + 100).substring(1); + svalue5old = sunrise; + } else if (simulation) { + sunrise = String("06:42"); + } + + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(335, 65); + if (holdvalues == false) getdisplay().print(sunrise); // Value + else getdisplay().print(svalue5old); + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().setCursor(335, 95); + getdisplay().print("SunR"); // Name + + // Horizontal separator right + getdisplay().fillRect(340, 149, 80, 3, commonData->fgcolor); + + // Show values sunset + String sunset = "---"; + if ((valid1 and valid2 and valid3 == true) or (homevalid and commonData->data.rtcValid)) { + sunset = String(commonData->sundata.sunsetHour) + ":" + String(commonData->sundata.sunsetMinute + 100).substring(1); + svalue6old = sunset; + } else if (simulation) { + sunset = String("21:03"); + } + + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(335, 250); + if (holdvalues == false) getdisplay().print(sunset); // Value + else getdisplay().print(svalue6old); + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().setCursor(335, 220); + getdisplay().print("SunS"); // Name + int rInstrument = 110; // Radius of clock float pi = 3.141592; @@ -756,6 +778,9 @@ bool PageClock::timerRunning = false; int PageClock::timerHours = 0; int PageClock::timerMinutes = 0; int PageClock::timerSeconds = 0; +int PageClock::timerStartHours = 0; +int PageClock::timerStartMinutes = 0; +int PageClock::timerStartSeconds = 0; int PageClock::selectedField = 0; bool PageClock::showSelectionMarker = true; time_t PageClock::timerEndEpoch = 0; From 6870c9b8a4f3ef0f1692bc92cb1e4a099da6fe8a Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 6 Feb 2026 12:52:14 +0100 Subject: [PATCH 110/121] Modify button labels in PageClock --- lib/obp60task/PageClock.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/obp60task/PageClock.cpp b/lib/obp60task/PageClock.cpp index b1ec8a4..fa5bb42 100644 --- a/lib/obp60task/PageClock.cpp +++ b/lib/obp60task/PageClock.cpp @@ -137,7 +137,7 @@ public: commonData->keydata[1].label = "POS"; commonData->keydata[2].label = "+"; commonData->keydata[3].label = "-"; - commonData->keydata[4].label = "RUN"; + commonData->keydata[4].label = "START"; } else { // Clock modes: like original commonData->keydata[0].label = "MODE"; @@ -362,7 +362,7 @@ public: int dispS = timerSeconds; // Update remaining time if timer is running (based on RTC) - if (timerRunning && commonData->data.rtcValid) { + if (timerRunning && commonData->data.rtcValid) { struct tm rtcCopy = commonData->data.rtcTime; time_t nowEpoch = mktime(&rtcCopy); time_t remaining = timerEndEpoch - nowEpoch; @@ -375,7 +375,7 @@ public: if (remaining <= 0) { remaining = 0; timerRunning = false; - commonData->keydata[4].label = "RUN"; + commonData->keydata[4].label = "START"; // Buzzer alarm (100% power) setBuzzerPower(100); buzzer(TONE4, 800); @@ -387,7 +387,7 @@ public: timerSeconds = timerStartSeconds; } else{ - commonData->keydata[4].label = "STOP"; + commonData->keydata[4].label = "RESET"; } int rem = static_cast(remaining); dispH = rem / 3600; From ef4546a2e6efc4280da759f9cc7182dbc2464028 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 6 Feb 2026 15:20:43 +0100 Subject: [PATCH 111/121] Fix for PageClock --- lib/obp60task/PageClock.cpp | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/lib/obp60task/PageClock.cpp b/lib/obp60task/PageClock.cpp index fa5bb42..ae52d75 100644 --- a/lib/obp60task/PageClock.cpp +++ b/lib/obp60task/PageClock.cpp @@ -184,12 +184,18 @@ public: incrementSelected(); return 0; } + if (key == 3 && timerRunning) { // No action if timer running + return 0; + } // - (K4): decrement selected field (only if timer not running) if (key == 4 && !timerRunning) { decrementSelected(); return 0; } + if (key == 4 && timerRunning) { // No action if timer running + return 0; + } // RUN (K5): start/stop timer if (key == 5) { @@ -366,10 +372,10 @@ public: struct tm rtcCopy = commonData->data.rtcTime; time_t nowEpoch = mktime(&rtcCopy); time_t remaining = timerEndEpoch - nowEpoch; - if(remaining <= 5){ - // Short buzzer alarm (100% power) + if(remaining <= 5 && remaining != 0){ + // Short pre buzzer alarm (100% power) setBuzzerPower(100); - buzzer(TONE4, 75); + buzzer(TONE2, 75); setBuzzerPower(config->getInt(config->buzzerPower)); } if (remaining <= 0) { @@ -378,7 +384,7 @@ public: commonData->keydata[4].label = "START"; // Buzzer alarm (100% power) setBuzzerPower(100); - buzzer(TONE4, 800); + buzzer(TONE2, 800); setBuzzerPower(config->getInt(config->buzzerPower)); // When countdown is finished, restore the initial start time @@ -492,20 +498,21 @@ public: int16_t x = (static_cast(getdisplay().width()) - static_cast(wb)) / 2; int16_t y = 150 + hb / 2; - getdisplay().setCursor(x, y); + //getdisplay().setCursor(x, y); + getdisplay().setCursor(47, y); getdisplay().print(timeStr); // Display actual time // Small indicators: timezone and source getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(x, 110); + getdisplay().setCursor(47, 110); if (source == 'G') { getdisplay().print("GPS"); } else { getdisplay().print("RTC"); } - getdisplay().setCursor(x + 40, 110); + getdisplay().setCursor(47 + 40, 110); if (holdvalues == false) { getdisplay().print(tz == 'L' ? "LOT" : "UTC"); } else { From 6b92a5e69c8107af28ddde8b84ad57c672dc9e92 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 6 Feb 2026 22:41:34 +0100 Subject: [PATCH 112/121] Add more functionality for time and date synchrinisation --- lib/obp60task/OBPSensorTask.cpp | 161 ++++++++++++++++++++++++-------- lib/obp60task/Pagedata.h | 2 +- 2 files changed, 123 insertions(+), 40 deletions(-) diff --git a/lib/obp60task/OBPSensorTask.cpp b/lib/obp60task/OBPSensorTask.cpp index 31c754c..d296ac2 100644 --- a/lib/obp60task/OBPSensorTask.cpp +++ b/lib/obp60task/OBPSensorTask.cpp @@ -49,8 +49,10 @@ void sensorTask(void *param){ // Init sensor stuff bool oneWire_ready = false; // 1Wire initialized and ready to use + bool iRTC_ready = false; // Software RTC initialized and ready to use bool RTC_ready = false; // DS1388 initialized and ready to use bool GPS_ready = false; // GPS initialized and ready to use + bool N2K_GPS_ready = false; // GPS time on N2K bus bool BME280_ready = false; // BME280 initialized and ready to use bool BMP280_ready = false; // BMP280 initialized and ready to use bool BMP180_ready = false; // BMP180 initialized and ready to use @@ -382,6 +384,7 @@ void sensorTask(void *param){ if (getLocalTime(&timeinfo)) { api->getLogger()->logDebug(GwLog::LOG,"NTP time: %04d-%02d-%02d %02d:%02d:%02d UTC", timeinfo.tm_year+1900, timeinfo.tm_mon+1, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec); rtc.setTimeStruct(timeinfo); + iRTC_ready = true; sensors.rtcValid = true; } else { api->getLogger()->logDebug(GwLog::LOG,"NTP time fetch failed!"); @@ -400,7 +403,7 @@ void sensorTask(void *param){ if (millis() > starttime0 + 100) { starttime0 = millis(); - // Send NMEA0183 GPS data on several bus systems all 100ms + // Send NMEA0183 GPS data on several bus systems (N2K an 0183) all 100ms if (GPS_ready == true && hdop->value <= hdopAccuracy) { SNMEA0183Msg NMEA0183Msg; @@ -412,9 +415,55 @@ void sensorTask(void *param){ } - // If RTC DS1388 ready, then copy GPS data to RTC all 5min + /* + Time set logic for RTC and N2K + ############################### + + iRTC = Software RTC updatetd with NTP via internet + RTC = RTC chip on PCB + GPS = GPS Receiver on PCB + N2K = GPS time on N2K od 183 bus + 0 = device not ready + 1 = device ready + X = undependend + () = source for set time N2K + -> = set RTC via iRTC + <- = set RTC via GPS + + iRTC RTC GPS N2K + 0 0 0 (1) + 0 0 (1) (X) + 0 (1) 0 (X) + 0 1 <-(1) (X) + (1) 0 0 (X) + 1 0 (1) (X) + 1 ->(1) 0 (X) + 1 1 <-(1) (X) + + */ + + // If RTC DS1388 ready, then copy iRTC and GPS data to RTC all 5min if(millis() > starttime11 + 5*60*1000){ starttime11 = millis(); + // Set RTC chip via iRTC (NTP) + if(iRTC_ready == true && RTC_ready == true && GPS_ready == false){ + GwApi::Status status; + api->getStatus(status); + // Check WiFi connection + if (status.wifiClientConnected) { + sensors.rtcTime = rtc.getTimeStruct(); // Get time from software RTC (iRTC) + DateTime now = DateTime( + sensors.rtcTime.tm_year + 1900, + sensors.rtcTime.tm_mon + 1, + sensors.rtcTime.tm_mday, + sensors.rtcTime.tm_hour, + sensors.rtcTime.tm_min, + sensors.rtcTime.tm_sec + ); + ds1388.adjust(now); + } + } + // Set RTC chip via internal GPS if(rtcOn == "DS1388" && RTC_ready == true && GPS_ready == true){ api->getBoatDataValues(3,valueList); if(gpsdays->valid && gpsseconds->valid && hdop->valid){ @@ -422,40 +471,33 @@ void sensorTask(void *param){ // sample input: date = "Dec 26 2009", time = "12:34:56" // ds1388.adjust(DateTime("Dec 26 2009", "12:34:56")); DateTime adjusttime(ts); - api->getLogger()->logDebug(GwLog::LOG,"Adjust RTC time: %04d/%02d/%02d %02d:%02d:%02d",adjusttime.year(), adjusttime.month(), adjusttime.day(), adjusttime.hour(), adjusttime.minute(), adjusttime.second()); + api->getLogger()->logDebug(GwLog::LOG,"Adjust RTC time via internal GPS: %04d/%02d/%02d %02d:%02d:%02d",adjusttime.year(), adjusttime.month(), adjusttime.day(), adjusttime.hour(), adjusttime.minute(), adjusttime.second()); // Adjust RTC time as unix time value ds1388.adjust(adjusttime); } - } + } } - // Send 1Wire data for all temperature sensors all 2s - if(millis() > starttime13 + 2000 && String(oneWireOn) == "DS18B20" && oneWire_ready == true){ - starttime13 = millis(); - float tempC; - ds18b20.requestTemperatures(); // Collect all temperature values (max.8) - for(int i=0;igetLogger()->logDebug(GwLog::DEBUG,"DS18B20-%1d Temp: %.1f",i,tempC); - SetN2kPGN130316(N2kMsg, 0, i, N2kts_OutsideTemperature, CToKelvin(tempC), N2kDoubleNA); - api->sendN2kMessage(N2kMsg); - } - } - } - loopCounter++; + // Set RTC chip via N2K or 183 in case the internal GPS is off (only one time) + if(N2K_GPS_ready == false && RTC_ready == true && GPS_ready == false){ + api->getBoatDataValues(3,valueList); + if(gpsdays->valid && gpsseconds->valid && hdop->valid){ + long ts = tNMEA0183Msg::daysToTime_t(gpsdays->value - (30*365+7))+floor(gpsseconds->value); // Adjusted to reference year 2000 (-30 years and 7 days for switch years) + // sample input: date = "Dec 26 2009", time = "12:34:56" + // ds1388.adjust(DateTime("Dec 26 2009", "12:34:56")); + DateTime adjusttime(ts); + api->getLogger()->logDebug(GwLog::LOG,"Adjust RTC time via N2K/183: %04d/%02d/%02d %02d:%02d:%02d",adjusttime.year(), adjusttime.month(), adjusttime.day(), adjusttime.hour(), adjusttime.minute(), adjusttime.second()); + // Adjust RTC time as unix time value + ds1388.adjust(adjusttime); + // N2K GPS time ready + N2K_GPS_ready = true; + } } - // Get current RTC date and time all 500ms + // Send RTC date and time to N2K all 500ms if (millis() > starttime12 + 500) { starttime12 = millis(); + // Send date and time from RTC chip if (rtcOn == "DS1388" && RTC_ready) { DateTime dt = ds1388.now(); sensors.rtcTime.tm_year = dt.year() - 1900; // Save values in SensorData @@ -481,21 +523,62 @@ void sensorTask(void *param){ } // N2K sysTime is double in n2klib double sysTime = (dt.hour() * 3600) + (dt.minute() * 60) + dt.second(); - // WHY? isnan should always fail here - //if(!isnan(daysAt1970) && !isnan(sysTime)){ + if(!isnan(daysAt1970) && !isnan(sysTime)){ //api->getLogger()->logDebug(GwLog::LOG,"RTC time: %04d/%02d/%02d %02d:%02d:%02d",sensors.rtcTime.tm_year+1900,sensors.rtcTime.tm_mon, sensors.rtcTime.tm_mday, sensors.rtcTime.tm_hour, sensors.rtcTime.tm_min, sensors.rtcTime.tm_sec); //api->getLogger()->logDebug(GwLog::LOG,"Send PGN126992: %10d %10d",daysAt1970, (uint16_t)sysTime); SetN2kPGN126992(N2kMsg,0,daysAt1970,sysTime,N2ktimes_LocalCrystalClock); api->sendN2kMessage(N2kMsg); - // } + } + } + } + // Send date and time from software RTC (iRTC) + if (iRTC_ready == true && RTC_ready == false && GPS_ready == false) { + // Use internal RTC feature + sensors.rtcTime = rtc.getTimeStruct(); // Save software RTC values in SensorData + // TODO implement daysAt1970 and sysTime as methods of DateTime + const short daysOfYear[12] = {0,31,59,90,120,151,181,212,243,273,304,334}; + uint16_t switchYear = ((sensors.rtcTime.tm_year-1)-1968)/4 - ((sensors.rtcTime.tm_year-1)-1900)/100 + ((sensors.rtcTime.tm_year-1)-1600)/400; + long daysAt1970 = (sensors.rtcTime.tm_year-1970)*365 + switchYear + daysOfYear[sensors.rtcTime.tm_mon-1] + sensors.rtcTime.tm_mday-1; + // If switch year then add one day + if ((sensors.rtcTime.tm_mon > 2) && (sensors.rtcTime.tm_year % 4 == 0 && (sensors.rtcTime.tm_year % 100 != 0 || sensors.rtcTime.tm_year % 400 == 0))) { + daysAt1970 += 1; + } + // N2K sysTime is double in n2klib + double sysTime = (sensors.rtcTime.tm_hour * 3600) + (sensors.rtcTime.tm_min * 60) + sensors.rtcTime.tm_sec; + if(!isnan(daysAt1970) && !isnan(sysTime)){ + //api->getLogger()->logDebug(GwLog::LOG,"RTC time: %04d/%02d/%02d %02d:%02d:%02d",sensors.rtcTime.tm_year+1900,sensors.rtcTime.tm_mon, sensors.rtcTime.tm_mday, sensors.rtcTime.tm_hour, sensors.rtcTime.tm_min, sensors.rtcTime.tm_sec); + //api->getLogger()->logDebug(GwLog::LOG,"Send PGN126992: %10d %10d",daysAt1970, (uint16_t)sysTime); + SetN2kPGN126992(N2kMsg,0,daysAt1970,sysTime,N2ktimes_LocalCrystalClock); + api->sendN2kMessage(N2kMsg); } - } else if (sensors.rtcValid) { - // use internal rtc feature - sensors.rtcTime = rtc.getTimeStruct(); } } - // Send supply voltage value all 1s + // Send 1Wire data for all temperature sensors to N2K all 2s + if(millis() > starttime13 + 2000 && String(oneWireOn) == "DS18B20" && oneWire_ready == true){ + starttime13 = millis(); + float tempC; + ds18b20.requestTemperatures(); // Collect all temperature values (max.8) + for(int i=0;igetLogger()->logDebug(GwLog::DEBUG,"DS18B20-%1d Temp: %.1f",i,tempC); + SetN2kPGN130316(N2kMsg, 0, i, N2kts_OutsideTemperature, CToKelvin(tempC), N2kDoubleNA); + api->sendN2kMessage(N2kMsg); + } + } + } + loopCounter++; + } + + // Send supply voltage value to N2K all 1s if(millis() > starttime5 + 1000 && String(powsensor1) == "off"){ starttime5 = millis(); float rawVoltage = 0; // Default value @@ -565,7 +648,7 @@ void sensorTask(void *param){ #endif } - // Send data from environment sensor all 2s + // Send data from environment sensor to N2K all 2s if(millis() > starttime6 + 2000){ starttime6 = millis(); unsigned char TempSource = 2; // Inside temperature @@ -630,7 +713,7 @@ void sensorTask(void *param){ } } - // Send rotation angle all 500ms + // Send rotation angle to N2K all 500ms if(millis() > starttime7 + 500){ starttime7 = millis(); double rotationAngle=0; @@ -678,7 +761,7 @@ void sensorTask(void *param){ } } - // Send battery power value all 1s + // Send battery power value to N2K all 1s if(millis() > starttime8 + 1000 && (String(powsensor1) == "INA219" || String(powsensor1) == "INA226")){ starttime8 = millis(); if(String(powsensor1) == "INA226" && INA226_1_ready == true){ @@ -720,7 +803,7 @@ void sensorTask(void *param){ } } - // Send solar power value all 1s + // Send solar power value to N2K all 1s if(millis() > starttime9 + 1000 && (String(powsensor2) == "INA219" || String(powsensor2) == "INA226")){ starttime9 = millis(); if(String(powsensor2) == "INA226" && INA226_2_ready == true){ @@ -750,7 +833,7 @@ void sensorTask(void *param){ } } - // Send generator power value all 1s + // Send generator power value to N2K all 1s if(millis() > starttime10 + 1000 && (String(powsensor3) == "INA219" || String(powsensor3) == "INA226")){ starttime10 = millis(); if(String(powsensor3) == "INA226" && INA226_3_ready == true){ diff --git a/lib/obp60task/Pagedata.h b/lib/obp60task/Pagedata.h index 02afba9..1cef664 100644 --- a/lib/obp60task/Pagedata.h +++ b/lib/obp60task/Pagedata.h @@ -51,7 +51,7 @@ typedef struct{ double rotationAngle = 0; // Rotation angle in radiant bool validRotAngle = false; // Valid flag magnet present for rotation sensor struct tm rtcTime; // UTC time from internal RTC - bool rtcValid = false; + bool rtcValid = false; // Internal RTC chip int sunsetHour = 0; int sunsetMinute = 0; int sunriseHour = 0; From fb2fbc85a4bbd5c1a3e0de1fa3a06e8c808ed2a8 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 6 Feb 2026 23:05:26 +0100 Subject: [PATCH 113/121] Typo and formats for PageClock --- lib/obp60task/PageClock.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/obp60task/PageClock.cpp b/lib/obp60task/PageClock.cpp index ae52d75..3512c89 100644 --- a/lib/obp60task/PageClock.cpp +++ b/lib/obp60task/PageClock.cpp @@ -431,7 +431,8 @@ public: int16_t textX = (static_cast(getdisplay().width()) - static_cast(wb)) / 2; int16_t textY = centerY + hb / 2; - getdisplay().setCursor(textX, textY); + //getdisplay().setCursor(textX, textY); + getdisplay().setCursor(47, textY); getdisplay().print(timeStr); // Selection marker (only visible when not running and POS pressed) @@ -450,8 +451,8 @@ public: // Page label getdisplay().setFont(&Ubuntu_Bold16pt8b); - getdisplay().setCursor(80, 70); - getdisplay().print("Count Dow Timer"); + getdisplay().setCursor(65, 70); + getdisplay().print("Count Down Timer"); } else if (mode == 'D') { // DIGITAL CLOCK MODE: large 7-segment time based on GPS/RTC From e9bf54e99fb2d498db6c95562a8b743a45325abc Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sat, 7 Feb 2026 14:34:54 +0000 Subject: [PATCH 114/121] Add sync button for Timer --- lib/obp60task/PageClock.cpp | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/lib/obp60task/PageClock.cpp b/lib/obp60task/PageClock.cpp index 3512c89..c3bcfa8 100644 --- a/lib/obp60task/PageClock.cpp +++ b/lib/obp60task/PageClock.cpp @@ -53,6 +53,8 @@ class PageClock : public Page static int timerHours; static int timerMinutes; static int timerSeconds; + // Preset seconds for sync button (default 4 minutes) + static const int timerPresetSeconds = 4 * 60; // Initial timer setting at start (so we can restore it) static int timerStartHours; static int timerStartMinutes; @@ -135,9 +137,10 @@ public: // Timer mode: MODE, POS, +, -, RUN commonData->keydata[0].label = "MODE"; commonData->keydata[1].label = "POS"; - commonData->keydata[2].label = "+"; + // K3: '+' while editing, 'SYNC' while running to set a preset countdown + commonData->keydata[2].label = timerRunning ? "SYNC" : "+"; commonData->keydata[3].label = "-"; - commonData->keydata[4].label = "START"; + commonData->keydata[4].label = timerRunning ? "RESET" : "START"; } else { // Clock modes: like original commonData->keydata[0].label = "MODE"; @@ -184,7 +187,26 @@ public: incrementSelected(); return 0; } - if (key == 3 && timerRunning) { // No action if timer running + if (key == 3 && timerRunning) { + // When timer is running, K3 acts as a synchronization button: + // set remaining countdown to the preset value (e.g. 4 minutes). + if (commonData->data.rtcValid) { + int preset = timerPresetSeconds; + // update start-setting so STOP will restore this preset + timerStartHours = preset / 3600; + timerStartMinutes = (preset % 3600) / 60; + timerStartSeconds = preset % 60; + + struct tm rtcCopy = commonData->data.rtcTime; + time_t nowEpoch = mktime(&rtcCopy); + timerEndEpoch = nowEpoch + preset; + + // Update visible timer fields immediately + timerHours = timerStartHours; + timerMinutes = timerStartMinutes; + timerSeconds = timerStartSeconds; + commonData->keydata[4].label = "RESET"; + } return 0; } From a0a88fa2c96052e161f489105bda26d8d560160a Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Sat, 7 Feb 2026 17:05:41 +0100 Subject: [PATCH 115/121] Modify reagatta timer funktion --- lib/obp60task/OBPSensorTask.cpp | 8 ++++---- lib/obp60task/PageClock.cpp | 18 +++++++++++------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/lib/obp60task/OBPSensorTask.cpp b/lib/obp60task/OBPSensorTask.cpp index d296ac2..25d861b 100644 --- a/lib/obp60task/OBPSensorTask.cpp +++ b/lib/obp60task/OBPSensorTask.cpp @@ -425,7 +425,7 @@ void sensorTask(void *param){ N2K = GPS time on N2K od 183 bus 0 = device not ready 1 = device ready - X = undependend + X = independend () = source for set time N2K -> = set RTC via iRTC <- = set RTC via GPS @@ -442,8 +442,8 @@ void sensorTask(void *param){ */ - // If RTC DS1388 ready, then copy iRTC and GPS data to RTC all 5min - if(millis() > starttime11 + 5*60*1000){ + // If RTC DS1388 ready, then copy iRTC and GPS data to RTC all 1min + if(millis() > starttime11 + 1*60*1000){ starttime11 = millis(); // Set RTC chip via iRTC (NTP) if(iRTC_ready == true && RTC_ready == true && GPS_ready == false){ @@ -497,7 +497,7 @@ void sensorTask(void *param){ // Send RTC date and time to N2K all 500ms if (millis() > starttime12 + 500) { starttime12 = millis(); - // Send date and time from RTC chip + // Send date and time from RTC chip if GPS not ready if (rtcOn == "DS1388" && RTC_ready) { DateTime dt = ds1388.now(); sensors.rtcTime.tm_year = dt.year() - 1900; // Save values in SensorData diff --git a/lib/obp60task/PageClock.cpp b/lib/obp60task/PageClock.cpp index c3bcfa8..a1d505c 100644 --- a/lib/obp60task/PageClock.cpp +++ b/lib/obp60task/PageClock.cpp @@ -205,7 +205,7 @@ public: timerHours = timerStartHours; timerMinutes = timerStartMinutes; timerSeconds = timerStartSeconds; - commonData->keydata[4].label = "RESET"; +// commonData->keydata[4].label = "RESET"; } return 0; } @@ -242,6 +242,7 @@ public: timerMinutes = timerStartMinutes; timerSeconds = timerStartSeconds; timerRunning = false; + showSelectionMarker = true; // marker will become visible again only after POS press } return 0; @@ -403,7 +404,9 @@ public: if (remaining <= 0) { remaining = 0; timerRunning = false; + commonData->keydata[3].label = "-"; commonData->keydata[4].label = "START"; + showSelectionMarker = true; // Buzzer alarm (100% power) setBuzzerPower(100); buzzer(TONE2, 800); @@ -415,6 +418,7 @@ public: timerSeconds = timerStartSeconds; } else{ + commonData->keydata[3].label = ""; commonData->keydata[4].label = "RESET"; } int rem = static_cast(remaining); @@ -453,8 +457,8 @@ public: int16_t textX = (static_cast(getdisplay().width()) - static_cast(wb)) / 2; int16_t textY = centerY + hb / 2; - //getdisplay().setCursor(textX, textY); - getdisplay().setCursor(47, textY); + //getdisplay().setCursor(textX, textY); // horzontal jitter + getdisplay().setCursor(47, textY); // static X position getdisplay().print(timeStr); // Selection marker (only visible when not running and POS pressed) @@ -473,8 +477,8 @@ public: // Page label getdisplay().setFont(&Ubuntu_Bold16pt8b); - getdisplay().setCursor(65, 70); - getdisplay().print("Count Down Timer"); + getdisplay().setCursor(100, 70); + getdisplay().print("Regatta Timer"); } else if (mode == 'D') { // DIGITAL CLOCK MODE: large 7-segment time based on GPS/RTC @@ -521,8 +525,8 @@ public: int16_t x = (static_cast(getdisplay().width()) - static_cast(wb)) / 2; int16_t y = 150 + hb / 2; - //getdisplay().setCursor(x, y); - getdisplay().setCursor(47, y); + //getdisplay().setCursor(x, y); // horizontal jitter + getdisplay().setCursor(47, y); // static X position getdisplay().print(timeStr); // Display actual time // Small indicators: timezone and source From ded1b2b22e6d0561e294612eb5776f6a03695421 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Sat, 7 Feb 2026 17:10:59 +0100 Subject: [PATCH 116/121] Typo --- lib/obp60task/PageClock.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/obp60task/PageClock.cpp b/lib/obp60task/PageClock.cpp index a1d505c..3c5a63f 100644 --- a/lib/obp60task/PageClock.cpp +++ b/lib/obp60task/PageClock.cpp @@ -15,7 +15,7 @@ * K4: * K5: TZ (Local/UTC) * - * Timer mode: + * Regatta timer mode: * - Format HH:MM:SS (24h, leading zeros) * - Keys in timer mode: * K1: MODE (A/D/T) @@ -383,8 +383,8 @@ public: struct tm* local_tm = localtime(&tv); if (mode == 'T') { - // TIMER MODE: countdown timer HH:MM:SS in the center with 7-segment font - //************************************************************************ + // REGATTA TIMER MODE: countdown timer HH:MM:SS in the center with 7-segment font + //******************************************************************************* int dispH = timerHours; int dispM = timerMinutes; From bbecf5e55f6f2fe6c1a94bf96ca4006534ccebc0 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Sat, 7 Feb 2026 17:36:14 +0100 Subject: [PATCH 117/121] Code ceaning --- lib/obp60task/PageClock.old | 467 ----------------- lib/obp60task/PageClock2.new | 548 -------------------- lib/obp60task/PageClock3.new | 777 ----------------------------- lib/obp60task/PageClockDigital.new | 224 --------- 4 files changed, 2016 deletions(-) delete mode 100644 lib/obp60task/PageClock.old delete mode 100644 lib/obp60task/PageClock2.new delete mode 100644 lib/obp60task/PageClock3.new delete mode 100644 lib/obp60task/PageClockDigital.new diff --git a/lib/obp60task/PageClock.old b/lib/obp60task/PageClock.old deleted file mode 100644 index 7c7b74f..0000000 --- a/lib/obp60task/PageClock.old +++ /dev/null @@ -1,467 +0,0 @@ -#if defined BOARD_OBP60S3 || defined BOARD_OBP40S3 - -#include "Pagedata.h" -#include "OBP60Extensions.h" - -/* - * TODO mode: race timer: keys - * - prepare: set countdown to 5min - * reset: abort current countdown and start over with 5min preparation - * - 5min: key press - * - 4min: key press to sync - * - 1min: buzzer signal - * - start: buzzer signal for start - * - */ - -class PageClock : public Page -{ -bool simulation = false; -int simtime; -bool keylock = false; -#ifdef BOARD_OBP60S3 -char source = 'G'; // time source (R)TC | (G)PS | (N)TP -#endif -#ifdef BOARD_OBP40S3 -char source = 'R'; // time source (R)TC | (G)PS | (N)TP -#endif -char mode = 'A'; // display mode (A)nalog | (D)igital | race (T)imer -char tz = 'L'; // time zone (L)ocal | (U)TC -double timezone = 0; // there are timezones with non int offsets, e.g. 5.5 or 5.75 -double homelat; -double homelon; -bool homevalid = false; // homelat and homelon are valid - - public: - PageClock(CommonData &common){ - commonData = &common; - common.logger->logDebug(GwLog::LOG,"Instantiate PageClock"); - simulation = common.config->getBool(common.config->useSimuData); - timezone = common.config->getString(common.config->timeZone).toDouble(); - homelat = common.config->getString(common.config->homeLAT).toDouble(); - homelon = common.config->getString(common.config->homeLON).toDouble(); - homevalid = homelat >= -180.0 and homelat <= 180 and homelon >= -90.0 and homelon <= 90.0; - simtime = 38160; // time value 11:36 - } - - virtual void setupKeys(){ - Page::setupKeys(); - commonData->keydata[0].label = "SRC"; - commonData->keydata[1].label = "MODE"; - commonData->keydata[4].label = "TZ"; - } - - // Key functions - virtual int handleKey(int key){ - // Time source - if (key == 1) { - switch (source) { - case 'G': source = 'R'; break; - case 'R': source = 'G'; break; - default: source = 'G'; break; - } - return 0; - } - if (key == 2) { - switch (mode) { - case 'A': mode = 'D'; break; - case 'D': mode = 'T'; break; - case 'T': mode = 'A'; break; - default: mode = 'A'; break; - } - return 0; - } - // Time zone: Local / UTC - if (key == 5) { - switch (tz) { - case 'L': tz = 'U'; break; - case 'U': tz = 'L'; break; - default: tz = 'L'; break; - } - return 0; - } - - // Keylock function - if(key == 11){ // Code for keylock - keylock = !keylock; // Toggle keylock - return 0; // Commit the key - } - return key; - } - - int displayPage(PageData &pageData) - { - GwConfigHandler *config = commonData->config; - GwLog *logger = commonData->logger; - - static String svalue1old = ""; - static String unit1old = ""; - static String svalue2old = ""; - static String unit2old = ""; - static String svalue3old = ""; - static String unit3old = ""; - - static String svalue5old = ""; - static String svalue6old = ""; - - double value1 = 0; - double value2 = 0; - double value3 = 0; - - // Get config data - String lengthformat = config->getString(config->lengthFormat); - String dateformat = config->getString(config->dateFormat); - bool holdvalues = config->getBool(config->holdvalues); - String flashLED = config->getString(config->flashLED); - String backlightMode = config->getString(config->backlight); - - // Get boat values for GPS time - GwApi::BoatValue *bvalue1 = pageData.values[0]; // First element in list (only one value by PageOneValue) - String name1 = bvalue1->getName().c_str(); // Value name - name1 = name1.substring(0, 6); // String length limit for value name - if(simulation == false){ - value1 = bvalue1->value; // Value as double in SI unit - } - else{ - value1 = simtime++; // Simulation data for time value 11:36 in seconds - } // Other simulation data see OBP60Formatter.cpp - bool valid1 = bvalue1->valid; // Valid information - String svalue1 = formatValue(bvalue1, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places - String unit1 = formatValue(bvalue1, *commonData).unit; // Unit of value - if(valid1 == true){ - svalue1old = svalue1; // Save old value - unit1old = unit1; // Save old unit - } - - // Get boat values for GPS date - GwApi::BoatValue *bvalue2 = pageData.values[1]; // Second element in list (only one value by PageOneValue) - String name2 = bvalue2->getName().c_str(); // Value name - name2 = name2.substring(0, 6); // String length limit for value name - value2 = bvalue2->value; // Value as double in SI unit - bool valid2 = bvalue2->valid; // Valid information - String svalue2 = formatValue(bvalue2, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places - String unit2 = formatValue(bvalue2, *commonData).unit; // Unit of value - if(valid2 == true){ - svalue2old = svalue2; // Save old value - unit2old = unit2; // Save old unit - } - - // Get boat values for HDOP date - GwApi::BoatValue *bvalue3 = pageData.values[2]; // Third element in list (only one value by PageOneValue) - String name3 = bvalue3->getName().c_str(); // Value name - name3 = name3.substring(0, 6); // String length limit for value name - value3 = bvalue3->value; // Value as double in SI unit - bool valid3 = bvalue3->valid; // Valid information - String svalue3 = formatValue(bvalue3, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places - String unit3 = formatValue(bvalue3, *commonData).unit; // Unit of value - if(valid3 == true){ - svalue3old = svalue3; // Save old value - unit3old = unit3; // Save old unit - } - - // Optical warning by limit violation (unused) - if(String(flashLED) == "Limit Violation"){ - setBlinkingLED(false); - setFlashLED(false); - } - - // Logging boat values - if (bvalue1 == NULL) return PAGE_OK; // WTF why this statement? - LOG_DEBUG(GwLog::LOG,"Drawing at PageClock, %s:%f, %s:%f", name1.c_str(), value1, name2.c_str(), value2); - - // Draw page - //*********************************************************** - - // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update - - getdisplay().setTextColor(commonData->fgcolor); - - time_t tv = mktime(&commonData->data.rtcTime) + timezone * 3600; - struct tm *local_tm = localtime(&tv); - - // Show values GPS date - getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(10, 65); - if (holdvalues == false) { - if (source == 'G') { - // GPS value - getdisplay().print(svalue2); - } else if (commonData->data.rtcValid) { - // RTC value - if (tz == 'L') { - getdisplay().print(formatDate(dateformat, local_tm->tm_year + 1900, local_tm->tm_mon + 1, local_tm->tm_mday)); - } - else { - getdisplay().print(formatDate(dateformat, commonData->data.rtcTime.tm_year + 1900, commonData->data.rtcTime.tm_mon + 1, commonData->data.rtcTime.tm_mday)); - } - } else { - getdisplay().print("---"); - } - } else { - getdisplay().print(svalue2old); - } - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(10, 95); - getdisplay().print("Date"); // Name - - // Horizintal separator left - getdisplay().fillRect(0, 149, 60, 3, commonData->fgcolor); - - // Show values GPS time - getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(10, 250); - if (holdvalues == false) { - if (source == 'G') { - getdisplay().print(svalue1); // Value - } - else if (commonData->data.rtcValid) { - if (tz == 'L') { - getdisplay().print(formatTime('s', local_tm->tm_hour, local_tm->tm_min, local_tm->tm_sec)); - } - else { - getdisplay().print(formatTime('s', commonData->data.rtcTime.tm_hour, commonData->data.rtcTime.tm_min, commonData->data.rtcTime.tm_sec)); - } - } else { - getdisplay().print("---"); - } - } - else { - getdisplay().print(svalue1old); - } - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(10, 220); - getdisplay().print("Time"); // Name - - // Show values sunrise - String sunrise = "---"; - if ((valid1 and valid2 and valid3 == true) or (homevalid and commonData->data.rtcValid)) { - sunrise = String(commonData->sundata.sunriseHour) + ":" + String(commonData->sundata.sunriseMinute + 100).substring(1); - svalue5old = sunrise; - } else if (simulation) { - sunrise = String("06:42"); - } - - getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(335, 65); - if(holdvalues == false) getdisplay().print(sunrise); // Value - else getdisplay().print(svalue5old); - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(335, 95); - getdisplay().print("SunR"); // Name - - // Horizintal separator right - getdisplay().fillRect(340, 149, 80, 3, commonData->fgcolor); - - // Show values sunset - String sunset = "---"; - if ((valid1 and valid2 and valid3 == true) or (homevalid and commonData->data.rtcValid)) { - sunset = String(commonData->sundata.sunsetHour) + ":" + String(commonData->sundata.sunsetMinute + 100).substring(1); - svalue6old = sunset; - } else if (simulation) { - sunset = String("21:03"); - } - - getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(335, 250); - if(holdvalues == false) getdisplay().print(sunset); // Value - else getdisplay().print(svalue6old); - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(335, 220); - getdisplay().print("SunS"); // Name - -//******************************************************************************************* - - // Draw clock - int rInstrument = 110; // Radius of clock - float pi = 3.141592; - - getdisplay().fillCircle(200, 150, rInstrument + 10, commonData->fgcolor); // Outer circle - getdisplay().fillCircle(200, 150, rInstrument + 7, commonData->bgcolor); // Outer circle - - for(int i=0; i<360; i=i+1) - { - // Scaling values - float x = 200 + (rInstrument-30)*sin(i/180.0*pi); // x-coordinate dots - float y = 150 - (rInstrument-30)*cos(i/180.0*pi); // y-coordinate cots - const char *ii = ""; - switch (i) - { - case 0: ii="12"; break; - case 30 : ii=""; break; - case 60 : ii=""; break; - case 90 : ii="3"; break; - case 120 : ii=""; break; - case 150 : ii=""; break; - case 180 : ii="6"; break; - case 210 : ii=""; break; - case 240 : ii=""; break; - case 270 : ii="9"; break; - case 300 : ii=""; break; - case 330 : ii=""; break; - default: break; - } - - // Print text centered on position x, y - int16_t x1, y1; // Return values of getTextBounds - uint16_t w, h; // Return values of getTextBounds - getdisplay().getTextBounds(ii, int(x), int(y), &x1, &y1, &w, &h); // Calc width of new string - getdisplay().setCursor(x-w/2, y+h/2); - if(i % 30 == 0){ - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().print(ii); - } - - // Draw sub scale with dots - float sinx = 0; - float cosx = 0; - if(i % 6 == 0){ - float x1c = 200 + rInstrument*sin(i/180.0*pi); - float y1c = 150 - rInstrument*cos(i/180.0*pi); - getdisplay().fillCircle((int)x1c, (int)y1c, 2, commonData->fgcolor); - sinx=sin(i/180.0*pi); - cosx=cos(i/180.0*pi); - } - - // Draw sub scale with lines (two triangles) - if(i % 30 == 0){ - float dx=2; // Line thickness = 2*dx+1 - float xx1 = -dx; - float xx2 = +dx; - float yy1 = -(rInstrument-10); - float yy2 = -(rInstrument+10); - getdisplay().fillTriangle(200+(int)(cosx*xx1-sinx*yy1),150+(int)(sinx*xx1+cosx*yy1), - 200+(int)(cosx*xx2-sinx*yy1),150+(int)(sinx*xx2+cosx*yy1), - 200+(int)(cosx*xx1-sinx*yy2),150+(int)(sinx*xx1+cosx*yy2),commonData->fgcolor); - getdisplay().fillTriangle(200+(int)(cosx*xx2-sinx*yy1),150+(int)(sinx*xx2+cosx*yy1), - 200+(int)(cosx*xx1-sinx*yy2),150+(int)(sinx*xx1+cosx*yy2), - 200+(int)(cosx*xx2-sinx*yy2),150+(int)(sinx*xx2+cosx*yy2),commonData->fgcolor); - } - } - - // Print Unit in clock - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(175, 110); - if(holdvalues == false){ - getdisplay().print(tz == 'L' ? "LOT" : "UTC"); - } - else{ - getdisplay().print(unit2old); // date unit - } - - getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(185, 190); - if (source == 'G') { - getdisplay().print("GPS"); - } else { - getdisplay().print("RTC"); - } - - // Clock values - double hour = 0; - double minute = 0; - if (source == 'R') { - if (tz == 'L') { - time_t tv = mktime(&commonData->data.rtcTime) + timezone * 3600; - struct tm *local_tm = localtime(&tv); - minute = local_tm->tm_min; - hour = local_tm->tm_hour; - } else { - minute = commonData->data.rtcTime.tm_min; - hour = commonData->data.rtcTime.tm_hour; - } - hour += minute / 60; - } else { - if (tz == 'L') { - value1 += timezone * 3600; - } - if (value1 > 86400) {value1 -= 86400;} - if (value1 < 0) {value1 += 86400;} - hour = (value1 / 3600.0); - // minute = (hour - int(hour)) * 3600.0 / 60.0; // Analog minute pointer smooth moving - minute = int((hour - int(hour)) * 3600.0 / 60.0); // Jumping minute pointer from minute to minute - } - if (hour > 12) { - hour -= 12.0; - } - LOG_DEBUG(GwLog::DEBUG,"... PageClock, value1: %f hour: %f minute:%f", value1, hour, minute); - - // Draw hour pointer - float startwidth = 8; // Start width of pointer - if(valid1 == true || (source == 'R' && commonData->data.rtcValid) || holdvalues == true || simulation == true){ - float sinx=sin(hour * 30.0 * pi / 180); // Hour - float cosx=cos(hour * 30.0 * pi / 180); - // Normal pointer - // Pointer as triangle with center base 2*width - float xx1 = -startwidth; - float xx2 = startwidth; - float yy1 = -startwidth; - float yy2 = -(rInstrument * 0.5); - getdisplay().fillTriangle(200+(int)(cosx*xx1-sinx*yy1),150+(int)(sinx*xx1+cosx*yy1), - 200+(int)(cosx*xx2-sinx*yy1),150+(int)(sinx*xx2+cosx*yy1), - 200+(int)(cosx*0-sinx*yy2),150+(int)(sinx*0+cosx*yy2),commonData->fgcolor); - // Inverted pointer - // Pointer as triangle with center base 2*width - float endwidth = 2; // End width of pointer - float ix1 = endwidth; - float ix2 = -endwidth; - float iy1 = -(rInstrument * 0.5); - float iy2 = -endwidth; - getdisplay().fillTriangle(200+(int)(cosx*ix1-sinx*iy1),150+(int)(sinx*ix1+cosx*iy1), - 200+(int)(cosx*ix2-sinx*iy1),150+(int)(sinx*ix2+cosx*iy1), - 200+(int)(cosx*0-sinx*iy2),150+(int)(sinx*0+cosx*iy2),commonData->fgcolor); - } - - // Draw minute pointer - startwidth = 8; // Start width of pointer - if(valid1 == true || (source == 'R' && commonData->data.rtcValid) || holdvalues == true || simulation == true){ - float sinx=sin(minute * 6.0 * pi / 180); // Minute - float cosx=cos(minute * 6.0 * pi / 180); - // Normal pointer - // Pointer as triangle with center base 2*width - float xx1 = -startwidth; - float xx2 = startwidth; - float yy1 = -startwidth; - float yy2 = -(rInstrument - 15); - getdisplay().fillTriangle(200+(int)(cosx*xx1-sinx*yy1),150+(int)(sinx*xx1+cosx*yy1), - 200+(int)(cosx*xx2-sinx*yy1),150+(int)(sinx*xx2+cosx*yy1), - 200+(int)(cosx*0-sinx*yy2),150+(int)(sinx*0+cosx*yy2),commonData->fgcolor); - // Inverted pointer - // Pointer as triangle with center base 2*width - float endwidth = 2; // End width of pointer - float ix1 = endwidth; - float ix2 = -endwidth; - float iy1 = -(rInstrument - 15); - float iy2 = -endwidth; - getdisplay().fillTriangle(200+(int)(cosx*ix1-sinx*iy1),150+(int)(sinx*ix1+cosx*iy1), - 200+(int)(cosx*ix2-sinx*iy1),150+(int)(sinx*ix2+cosx*iy1), - 200+(int)(cosx*0-sinx*iy2),150+(int)(sinx*0+cosx*iy2),commonData->fgcolor); - } - - // Center circle - getdisplay().fillCircle(200, 150, startwidth + 6, commonData->bgcolor); - getdisplay().fillCircle(200, 150, startwidth + 4, commonData->fgcolor); - - return PAGE_UPDATE; - }; -}; - -static Page *createPage(CommonData &common){ - return new PageClock(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 (0 here) - * and will will provide the names of the fixed values we need - */ -PageDescription registerPageClock( - "Clock", // Page name - createPage, // Action - 0, // Number of bus values depends on selection in Web configuration - {"GPST", "GPSD", "HDOP"}, // Bus values we need in the page - true // Show display header on/off -); - -#endif diff --git a/lib/obp60task/PageClock2.new b/lib/obp60task/PageClock2.new deleted file mode 100644 index 3e0af87..0000000 --- a/lib/obp60task/PageClock2.new +++ /dev/null @@ -1,548 +0,0 @@ -#if defined BOARD_OBP60S3 || defined BOARD_OBP40S3 - -#include "Pagedata.h" -#include "OBP60Extensions.h" - -/* - * Variant of PageClock with switchable analog / digital clock display. - * mode: (A)nalog | (D)igital | race (T)imer (T not implemented yet, falls back to analog) - */ - -class PageClock2 : public Page -{ - bool simulation = false; - int simtime; - bool keylock = false; -#ifdef BOARD_OBP60S3 - char source = 'G'; // time source (R)TC | (G)PS | (N)TP -#endif -#ifdef BOARD_OBP40S3 - char source = 'R'; // time source (R)TC | (G)PS | (N)TP -#endif - char mode = 'A'; // display mode (A)nalog | (D)igital | race (T)imer - char tz = 'L'; // time zone (L)ocal | (U)TC - double timezone = 0; // there are timezones with non int offsets, e.g. 5.5 or 5.75 - double homelat; - double homelon; - bool homevalid = false; // homelat and homelon are valid - -public: - PageClock2(CommonData& common) - { - commonData = &common; - common.logger->logDebug(GwLog::LOG, "Instantiate PageClock2"); - simulation = common.config->getBool(common.config->useSimuData); - timezone = common.config->getString(common.config->timeZone).toDouble(); - homelat = common.config->getString(common.config->homeLAT).toDouble(); - homelon = common.config->getString(common.config->homeLON).toDouble(); - homevalid = homelat >= -180.0 and homelat <= 180 and homelon >= -90.0 and homelon <= 90.0; - simtime = 38160; // time value 11:36 - } - - virtual void setupKeys() - { - Page::setupKeys(); - commonData->keydata[0].label = "SRC"; - commonData->keydata[1].label = "MODE"; - commonData->keydata[4].label = "TZ"; - } - - // Key functions - virtual int handleKey(int key) - { - // Time source - if (key == 1) { - switch (source) { - case 'G': - source = 'R'; - break; - case 'R': - source = 'G'; - break; - default: - source = 'G'; - break; - } - return 0; - } - if (key == 2) { - switch (mode) { - case 'A': - mode = 'D'; - break; - case 'D': - mode = 'T'; - break; - case 'T': - mode = 'A'; - break; - default: - mode = 'A'; - break; - } - return 0; - } - // Time zone: Local / UTC - if (key == 5) { - switch (tz) { - case 'L': - tz = 'U'; - break; - case 'U': - tz = 'L'; - break; - default: - tz = 'L'; - break; - } - return 0; - } - - // Keylock function - if (key == 11) { // Code for keylock - keylock = !keylock; // Toggle keylock - return 0; // Commit the key - } - return key; - } - - int displayPage(PageData& pageData) - { - GwConfigHandler* config = commonData->config; - - static String svalue1old = ""; - static String unit1old = ""; - static String svalue2old = ""; - static String unit2old = ""; - static String svalue3old = ""; - static String unit3old = ""; - - static String svalue5old = ""; - static String svalue6old = ""; - - double value1 = 0; - double value2 = 0; - double value3 = 0; - - // Get config data - String lengthformat = config->getString(config->lengthFormat); - String dateformat = config->getString(config->dateFormat); - bool holdvalues = config->getBool(config->holdvalues); - String flashLED = config->getString(config->flashLED); - String backlightMode = config->getString(config->backlight); - - // Get boat values for GPS time - GwApi::BoatValue* bvalue1 = pageData.values[0]; // First element in list (only one value by PageOneValue) - String name1 = bvalue1->getName().c_str(); // Value name - name1 = name1.substring(0, 6); // String length limit for value name - if (simulation == false) { - value1 = bvalue1->value; // Value as double in SI unit - } else { - value1 = simtime++; // Simulation data for time value 11:36 in seconds - } // Other simulation data see OBP60Formatter.cpp - bool valid1 = bvalue1->valid; // Valid information - String svalue1 = formatValue(bvalue1, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places - String unit1 = formatValue(bvalue1, *commonData).unit; // Unit of value - if (valid1 == true) { - svalue1old = svalue1; // Save old value - unit1old = unit1; // Save old unit - } - - // Get boat values for GPS date - GwApi::BoatValue* bvalue2 = pageData.values[1]; // Second element in list (only one value by PageOneValue) - String name2 = bvalue2->getName().c_str(); // Value name - name2 = name2.substring(0, 6); // String length limit for value name - value2 = bvalue2->value; // Value as double in SI unit - bool valid2 = bvalue2->valid; // Valid information - String svalue2 = formatValue(bvalue2, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places - String unit2 = formatValue(bvalue2, *commonData).unit; // Unit of value - if (valid2 == true) { - svalue2old = svalue2; // Save old value - unit2old = unit2; // Save old unit - } - - // Get boat values for HDOP date - GwApi::BoatValue* bvalue3 = pageData.values[2]; // Third element in list (only one value by PageOneValue) - String name3 = bvalue3->getName().c_str(); // Value name - name3 = name3.substring(0, 6); // String length limit for value name - value3 = bvalue3->value; // Value as double in SI unit - bool valid3 = bvalue3->valid; // Valid information - String svalue3 = formatValue(bvalue3, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places - String unit3 = formatValue(bvalue3, *commonData).unit; // Unit of value - if (valid3 == true) { - svalue3old = svalue3; // Save old value - unit3old = unit3; // Save old unit - } - - // Optical warning by limit violation (unused) - if (String(flashLED) == "Limit Violation") { - setBlinkingLED(false); - setFlashLED(false); - } - - // Logging boat values - if (bvalue1 == NULL) - return PAGE_OK; // WTF why this statement? - LOG_DEBUG(GwLog::LOG, "Drawing at PageClock2, %s:%f, %s:%f", name1.c_str(), value1, name2.c_str(), value2); - - // Draw page - //*********************************************************** - - // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update - - getdisplay().setTextColor(commonData->fgcolor); - - time_t tv = mktime(&commonData->data.rtcTime) + timezone * 3600; - struct tm* local_tm = localtime(&tv); - - // Show values GPS date - getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(10, 65); - if (holdvalues == false) { - if (source == 'G') { - // GPS value - getdisplay().print(svalue2); - } else if (commonData->data.rtcValid) { - // RTC value - if (tz == 'L') { - getdisplay().print(formatDate(dateformat, local_tm->tm_year + 1900, local_tm->tm_mon + 1, local_tm->tm_mday)); - } else { - getdisplay().print(formatDate(dateformat, commonData->data.rtcTime.tm_year + 1900, commonData->data.rtcTime.tm_mon + 1, commonData->data.rtcTime.tm_mday)); - } - } else { - getdisplay().print("---"); - } - } else { - getdisplay().print(svalue2old); - } - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(10, 95); - getdisplay().print("Date"); // Name - - // Horizintal separator left - getdisplay().fillRect(0, 149, 60, 3, commonData->fgcolor); - - // Show values GPS time (small text bottom left, same as original) - getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(10, 250); - if (holdvalues == false) { - if (source == 'G') { - getdisplay().print(svalue1); // Value - } else if (commonData->data.rtcValid) { - if (tz == 'L') { - getdisplay().print(formatTime('s', local_tm->tm_hour, local_tm->tm_min, local_tm->tm_sec)); - } else { - getdisplay().print(formatTime('s', commonData->data.rtcTime.tm_hour, commonData->data.rtcTime.tm_min, commonData->data.rtcTime.tm_sec)); - } - } else { - getdisplay().print("---"); - } - } else { - getdisplay().print(svalue1old); - } - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(10, 220); - getdisplay().print("Time"); // Name - - // Show values sunrise - String sunrise = "---"; - if ((valid1 and valid2 and valid3 == true) or (homevalid and commonData->data.rtcValid)) { - sunrise = String(commonData->sundata.sunriseHour) + ":" + String(commonData->sundata.sunriseMinute + 100).substring(1); - svalue5old = sunrise; - } else if (simulation) { - sunrise = String("06:42"); - } - - getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(335, 65); - if (holdvalues == false) - getdisplay().print(sunrise); // Value - else - getdisplay().print(svalue5old); - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(335, 95); - getdisplay().print("SunR"); // Name - - // Horizintal separator right - getdisplay().fillRect(340, 149, 80, 3, commonData->fgcolor); - - // Show values sunset - String sunset = "---"; - if ((valid1 and valid2 and valid3 == true) or (homevalid and commonData->data.rtcValid)) { - sunset = String(commonData->sundata.sunsetHour) + ":" + String(commonData->sundata.sunsetMinute + 100).substring(1); - svalue6old = sunset; - } else if (simulation) { - sunset = String("21:03"); - } - - getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(335, 250); - if (holdvalues == false) - getdisplay().print(sunset); // Value - else - getdisplay().print(svalue6old); - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(335, 220); - getdisplay().print("SunS"); // Name - - //******************************************************************************************* - - if (mode == 'D') { - // Digital clock mode: large 7-segment time centered on display - - // Determine current time in hours/minutes/seconds (24h) - int hour24 = 0; - int minute24 = 0; - int second24 = 0; - - if (source == 'R' && commonData->data.rtcValid) { - // RTC based - time_t tv2 = mktime(&commonData->data.rtcTime); - if (tz == 'L') { - tv2 += static_cast(timezone * 3600); - } - struct tm* tm2 = localtime(&tv2); - hour24 = tm2->tm_hour; - minute24 = tm2->tm_min; - second24 = tm2->tm_sec; - } else { - // GPS / simulation based - double t = value1; - if (tz == 'L') { - t += timezone * 3600; - } - if (t >= 86400) - t -= 86400; - if (t < 0) - t += 86400; - hour24 = static_cast(t / 3600.0); - int rest = static_cast(t) - hour24 * 3600; - minute24 = rest / 60; - second24 = rest % 60; - } - - char buf[9]; // "HH:MM:SS" + '\0' - snprintf(buf, sizeof(buf), "%02d:%02d:%02d", hour24, minute24, second24); - String timeStr = String(buf); - - // Clear central area and draw large digital time - getdisplay().fillRect(0, 110, getdisplay().width(), 80, commonData->bgcolor); - - getdisplay().setFont(&DSEG7Classic_BoldItalic60pt7b); - - int16_t x1b, y1b; - uint16_t wb, hb; - getdisplay().getTextBounds(timeStr, 0, 0, &x1b, &y1b, &wb, &hb); - - int16_t x = (static_cast(getdisplay().width()) - static_cast(wb)) / 2; - int16_t y = 150 + hb / 2; // vertically around center (y=150) - - getdisplay().setCursor(x, y); - getdisplay().print(timeStr); - - // Small indicators inside the digital area - getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(180, 110); - if (holdvalues == false) { - getdisplay().print(tz == 'L' ? "LOT" : "UTC"); - } else { - getdisplay().print(unit2old); // date unit - } - - getdisplay().setCursor(185, 190); - if (source == 'G') { - getdisplay().print("GPS"); - } else { - getdisplay().print("RTC"); - } - - } else { - // Analog clock mode (A/T) - original drawing - - int rInstrument = 110; // Radius of clock - float pi = 3.141592; - - getdisplay().fillCircle(200, 150, rInstrument + 10, commonData->fgcolor); // Outer circle - getdisplay().fillCircle(200, 150, rInstrument + 7, commonData->bgcolor); // Outer circle - - for (int i = 0; i < 360; i = i + 1) - { - // Scaling values - float x = 200 + (rInstrument - 30) * sin(i / 180.0 * pi); // x-coordinate dots - float y = 150 - (rInstrument - 30) * cos(i / 180.0 * pi); // y-coordinate dots - const char* ii = ""; - switch (i) - { - case 0: ii = "12"; break; - case 90: ii = "3"; break; - case 180: ii = "6"; break; - case 270: ii = "9"; break; - default: break; - } - - // Print text centered on position x, y - int16_t x1c, y1c; // Return values of getTextBounds - uint16_t wc, hc; // Return values of getTextBounds - getdisplay().getTextBounds(ii, int(x), int(y), &x1c, &y1c, &wc, &hc); // Calc width of new string - getdisplay().setCursor(x - wc / 2, y + hc / 2); - if (i % 90 == 0) { - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().print(ii); - } - - // Draw sub scale with dots - float sinx = 0; - float cosx = 0; - if (i % 6 == 0) { - float x1d = 200 + rInstrument * sin(i / 180.0 * pi); - float y1d = 150 - rInstrument * cos(i / 180.0 * pi); - getdisplay().fillCircle((int)x1d, (int)y1d, 2, commonData->fgcolor); - sinx = sin(i / 180.0 * pi); - cosx = cos(i / 180.0 * pi); - } - - // Draw sub scale with lines (two triangles) - if (i % 30 == 0) { - float dx = 2; // Line thickness = 2*dx+1 - float xx1 = -dx; - float xx2 = +dx; - float yy1 = -(rInstrument - 10); - float yy2 = -(rInstrument + 10); - getdisplay().fillTriangle(200 + (int)(cosx * xx1 - sinx * yy1), 150 + (int)(sinx * xx1 + cosx * yy1), - 200 + (int)(cosx * xx2 - sinx * yy1), 150 + (int)(sinx * xx2 + cosx * yy1), - 200 + (int)(cosx * xx1 - sinx * yy2), 150 + (int)(sinx * xx1 + cosx * yy2), commonData->fgcolor); - getdisplay().fillTriangle(200 + (int)(cosx * xx2 - sinx * yy1), 150 + (int)(sinx * xx2 + cosx * yy1), - 200 + (int)(cosx * xx1 - sinx * yy2), 150 + (int)(sinx * xx1 + cosx * yy2), - 200 + (int)(cosx * xx2 - sinx * yy2), 150 + (int)(sinx * xx2 + cosx * yy2), commonData->fgcolor); - } - } - - // Print Unit in clock - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(175, 110); - if (holdvalues == false) { - getdisplay().print(tz == 'L' ? "LOT" : "UTC"); - } else { - getdisplay().print(unit2old); // date unit - } - - getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(185, 190); - if (source == 'G') { - getdisplay().print("GPS"); - } else { - getdisplay().print("RTC"); - } - - // Clock values - double hour = 0; - double minute = 0; - if (source == 'R') { - if (tz == 'L') { - time_t tv2 = mktime(&commonData->data.rtcTime) + timezone * 3600; - struct tm* local_tm2 = localtime(&tv2); - minute = local_tm2->tm_min; - hour = local_tm2->tm_hour; - } else { - minute = commonData->data.rtcTime.tm_min; - hour = commonData->data.rtcTime.tm_hour; - } - hour += minute / 60; - } else { - if (tz == 'L') { - value1 += timezone * 3600; - } - if (value1 > 86400) { value1 -= 86400; } - if (value1 < 0) { value1 += 86400; } - hour = (value1 / 3600.0); - // minute = (hour - int(hour)) * 3600.0 / 60.0; // Analog minute pointer smooth moving - minute = int((hour - int(hour)) * 3600.0 / 60.0); // Jumping minute pointer from minute to minute - } - if (hour > 12) { - hour -= 12.0; - } - LOG_DEBUG(GwLog::DEBUG, "... PageClock2, value1: %f hour: %f minute:%f", value1, hour, minute); - - // Draw hour pointer - float startwidth = 8; // Start width of pointer - if (valid1 == true || (source == 'R' && commonData->data.rtcValid) || holdvalues == true || simulation == true) { - float sinx = sin(hour * 30.0 * pi / 180); // Hour - float cosx = cos(hour * 30.0 * pi / 180); - // Normal pointer - // Pointer as triangle with center base 2*width - float xx1 = -startwidth; - float xx2 = startwidth; - float yy1 = -startwidth; - float yy2 = -(rInstrument * 0.5); - getdisplay().fillTriangle(200 + (int)(cosx * xx1 - sinx * yy1), 150 + (int)(sinx * xx1 + cosx * yy1), - 200 + (int)(cosx * xx2 - sinx * yy1), 150 + (int)(sinx * xx2 + cosx * yy1), - 200 + (int)(cosx * 0 - sinx * yy2), 150 + (int)(sinx * 0 + cosx * yy2), commonData->fgcolor); - // Inverted pointer - // Pointer as triangle with center base 2*width - float endwidth = 2; // End width of pointer - float ix1 = endwidth; - float ix2 = -endwidth; - float iy1 = -(rInstrument * 0.5); - float iy2 = -endwidth; - getdisplay().fillTriangle(200 + (int)(cosx * ix1 - sinx * iy1), 150 + (int)(sinx * ix1 + cosx * iy1), - 200 + (int)(cosx * ix2 - sinx * iy1), 150 + (int)(sinx * ix2 + cosx * iy1), - 200 + (int)(cosx * 0 - sinx * iy2), 150 + (int)(sinx * 0 + cosx * iy2), commonData->fgcolor); - } - - // Draw minute pointer - startwidth = 8; // Start width of pointer - if (valid1 == true || (source == 'R' && commonData->data.rtcValid) || holdvalues == true || simulation == true) { - float sinx = sin(minute * 6.0 * pi / 180); // Minute - float cosx = cos(minute * 6.0 * pi / 180); - // Normal pointer - // Pointer as triangle with center base 2*width - float xx1 = -startwidth; - float xx2 = startwidth; - float yy1 = -startwidth; - float yy2 = -(rInstrument - 15); - getdisplay().fillTriangle(200 + (int)(cosx * xx1 - sinx * yy1), 150 + (int)(sinx * xx1 + cosx * yy1), - 200 + (int)(cosx * xx2 - sinx * yy1), 150 + (int)(sinx * xx2 + cosx * yy1), - 200 + (int)(cosx * 0 - sinx * yy2), 150 + (int)(sinx * 0 + cosx * yy2), commonData->fgcolor); - // Inverted pointer - // Pointer as triangle with center base 2*width - float endwidth = 2; // End width of pointer - float ix1 = endwidth; - float ix2 = -endwidth; - float iy1 = -(rInstrument - 15); - float iy2 = -endwidth; - getdisplay().fillTriangle(200 + (int)(cosx * ix1 - sinx * iy1), 150 + (int)(sinx * ix1 + cosx * iy1), - 200 + (int)(cosx * ix2 - sinx * iy1), 150 + (int)(sinx * ix2 + cosx * iy1), - 200 + (int)(cosx * 0 - sinx * iy2), 150 + (int)(sinx * 0 + cosx * iy2), commonData->fgcolor); - } - - // Center circle - getdisplay().fillCircle(200, 150, startwidth + 6, commonData->bgcolor); - getdisplay().fillCircle(200, 150, startwidth + 4, commonData->fgcolor); - } - - return PAGE_UPDATE; - }; -}; - -static Page* createPage(CommonData& common) -{ - return new PageClock2(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 - * we provide the number of user parameters we expect (0 here) - * and we provide the names of the fixed values we need - */ -PageDescription registerPageClock2( - "Clock2", // Page name - createPage, // Action - 0, // Number of bus values depends on selection in Web configuration - {"GPST", "GPSD", "HDOP"}, // Bus values we need in the page - true // Show display header on/off -); - -#endif - diff --git a/lib/obp60task/PageClock3.new b/lib/obp60task/PageClock3.new deleted file mode 100644 index 2814c8b..0000000 --- a/lib/obp60task/PageClock3.new +++ /dev/null @@ -1,777 +0,0 @@ -#if defined BOARD_OBP60S3 || defined BOARD_OBP40S3 - -#include "Pagedata.h" -#include "OBP60Extensions.h" - -/* - * PageClock3: Clock page with - * - Analog mode (mode == 'A') - * - Digital mode (mode == 'D') - * - Countdown timer mode (mode == 'T') - * - * Timer mode: - * - Format HH:MM:SS (24h, leading zeros) - * - Keys in timer mode: - * K1: MODE (A/D/T) - * K2: POS (select field: HH / MM / SS) - * K3: + (increment selected field) - * K4: - (decrement selected field) - * K5: RUN (start/stop countdown) - * - Selection marker: line under active field (width 2px, not wider than digits) - * - Editing only possible when timer is not running - * - When page is left, running timer continues in background using RTC time - * (on re-entry, remaining time is recalculated from RTC) - */ - -class PageClock3 : public Page -{ - bool simulation = false; - int simtime; - bool keylock = false; -#ifdef BOARD_OBP60S3 - char source = 'G'; // time source (R)TC | (G)PS | (N)TP -#endif -#ifdef BOARD_OBP40S3 - char source = 'R'; // time source (R)TC | (G)PS | (N)TP -#endif - char mode = 'A'; // display mode (A)nalog | (D)igital | race (T)imer - char tz = 'L'; // time zone (L)ocal | (U)TC - double timezone = 0; // there are timezones with non int offsets, e.g. 5.5 or 5.75 - double homelat; - double homelon; - bool homevalid = false; // homelat and homelon are valid - - // Timer state (static so it survives page switches) - static bool timerInitialized; - static bool timerRunning; - static int timerHours; - static int timerMinutes; - static int timerSeconds; - static int selectedField; // 0 = hours, 1 = minutes, 2 = seconds - static bool showSelectionMarker; - static time_t timerEndEpoch; // absolute end time based on RTC - - void setupTimerDefaults() - { - if (!timerInitialized) { - timerInitialized = true; - timerRunning = false; - timerHours = 0; - timerMinutes = 0; - timerSeconds = 0; - selectedField = 0; - showSelectionMarker = true; - timerEndEpoch = 0; - } - } - - static int clamp(int value, int minVal, int maxVal) - { - if (value < minVal) return minVal; - if (value > maxVal) return maxVal; - return value; - } - - void incrementSelected() - { - if (selectedField == 0) { - timerHours = clamp(timerHours + 1, 0, 23); - } else if (selectedField == 1) { - timerMinutes = clamp(timerMinutes + 1, 0, 59); - } else { - timerSeconds = clamp(timerSeconds + 1, 0, 59); - } - } - - void decrementSelected() - { - if (selectedField == 0) { - timerHours = clamp(timerHours - 1, 0, 23); - } else if (selectedField == 1) { - timerMinutes = clamp(timerMinutes - 1, 0, 59); - } else { - timerSeconds = clamp(timerSeconds - 1, 0, 59); - } - } - - int totalTimerSeconds() const - { - return timerHours * 3600 + timerMinutes * 60 + timerSeconds; - } - -public: - PageClock3(CommonData& common) - { - commonData = &common; - common.logger->logDebug(GwLog::LOG, "Instantiate PageClock3"); - simulation = common.config->getBool(common.config->useSimuData); - timezone = common.config->getString(common.config->timeZone).toDouble(); - homelat = common.config->getString(common.config->homeLAT).toDouble(); - homelon = common.config->getString(common.config->homeLON).toDouble(); - homevalid = homelat >= -180.0 and homelat <= 180 and homelon >= -90.0 and homelon <= 90.0; - simtime = 38160; // time value 11:36 - setupTimerDefaults(); - } - - virtual void setupKeys() - { - Page::setupKeys(); - - if (mode == 'T') { - // Timer mode: MODE, POS, +, -, RUN - commonData->keydata[0].label = "MODE"; - commonData->keydata[1].label = "POS"; - commonData->keydata[2].label = "+"; - commonData->keydata[3].label = "-"; - commonData->keydata[4].label = "RUN"; - } else { - // Clock modes: like original - commonData->keydata[0].label = "SRC"; - commonData->keydata[1].label = "MODE"; - commonData->keydata[4].label = "TZ"; - } - } - - // Key functions - virtual int handleKey(int key) - { - setupTimerDefaults(); - - // Keylock function - if (key == 11) { // Code for keylock - keylock = !keylock; // Toggle keylock - return 0; // Commit the key - } - - if (mode == 'T') { - // Timer mode key handling - - // MODE (K1): cycle display mode A/D/T - if (key == 1) { - switch (mode) { - case 'A': mode = 'D'; break; - case 'D': mode = 'T'; break; - case 'T': mode = 'A'; break; - default: mode = 'A'; break; - } - setupKeys(); - return 0; - } - - // POS (K2): select field HH / MM / SS (only if timer not running) - if (key == 2 && !timerRunning) { - selectedField = (selectedField + 1) % 3; - showSelectionMarker = true; - return 0; - } - - // + (K3): increment selected field (only if timer not running) - if (key == 3 && !timerRunning) { - incrementSelected(); - return 0; - } - - // - (K4): decrement selected field (only if timer not running) - if (key == 4 && !timerRunning) { - decrementSelected(); - return 0; - } - - // RUN (K5): start/stop timer - if (key == 5) { - if (!timerRunning) { - // Start timer if a non-zero duration is set - int total = totalTimerSeconds(); - if (total > 0 && commonData->data.rtcValid) { - struct tm rtcCopy = commonData->data.rtcTime; - time_t nowEpoch = mktime(&rtcCopy); - timerEndEpoch = nowEpoch + total; - timerRunning = true; - showSelectionMarker = false; - } - } else { - // Stop timer: compute remaining time and keep as new setting - if (commonData->data.rtcValid) { - struct tm rtcCopy = commonData->data.rtcTime; - time_t nowEpoch = mktime(&rtcCopy); - time_t remaining = timerEndEpoch - nowEpoch; - if (remaining < 0) remaining = 0; - int rem = static_cast(remaining); - timerHours = rem / 3600; - rem -= timerHours * 3600; - timerMinutes = rem / 60; - timerSeconds = rem % 60; - } - timerRunning = false; - // marker will become visible again only after POS press - } - return 0; - } - - // In timer mode, other keys are passed through - return key; - } - - // Clock (A/D) modes key handling – like original PageClock - - // Time source (K1) - if (key == 1) { - switch (source) { - case 'G': source = 'R'; break; - case 'R': source = 'G'; break; - default: source = 'G'; break; - } - return 0; - } - - // MODE (K2) - if (key == 2) { - switch (mode) { - case 'A': mode = 'D'; break; - case 'D': mode = 'T'; break; - case 'T': mode = 'A'; break; - default: mode = 'A'; break; - } - setupKeys(); - return 0; - } - - // Time zone: Local / UTC (K5) - if (key == 5) { - switch (tz) { - case 'L': tz = 'U'; break; - case 'U': tz = 'L'; break; - default: tz = 'L'; break; - } - return 0; - } - - return key; - } - - int displayPage(PageData& pageData) - { - GwConfigHandler* config = commonData->config; - GwLog* logger = commonData->logger; - - setupTimerDefaults(); - setupKeys(); // ensure correct key labels for current mode - - static String svalue1old = ""; - static String unit1old = ""; - static String svalue2old = ""; - static String unit2old = ""; - static String svalue3old = ""; - static String unit3old = ""; - - static String svalue5old = ""; - static String svalue6old = ""; - - double value1 = 0; - double value2 = 0; - double value3 = 0; - - // Get config data - String lengthformat = config->getString(config->lengthFormat); - String dateformat = config->getString(config->dateFormat); - bool holdvalues = config->getBool(config->holdvalues); - String flashLED = config->getString(config->flashLED); - String backlightMode = config->getString(config->backlight); - - // Get boat values for GPS time - GwApi::BoatValue* bvalue1 = pageData.values[0]; // First element in list - String name1 = bvalue1->getName().c_str(); // Value name - name1 = name1.substring(0, 6); // String length limit for value name - if (simulation == false) { - value1 = bvalue1->value; // Value as double in SI unit - } else { - value1 = simtime++; // Simulation data for time value 11:36 in seconds - } // Other simulation data see OBP60Formatter.cpp - bool valid1 = bvalue1->valid; // Valid information - String svalue1 = formatValue(bvalue1, *commonData).svalue; // Formatted value - String unit1 = formatValue(bvalue1, *commonData).unit; // Unit of value - if (valid1 == true) { - svalue1old = svalue1; // Save old value - unit1old = unit1; // Save old unit - } - - // Get boat values for GPS date - GwApi::BoatValue* bvalue2 = pageData.values[1]; // Second element in list - String name2 = bvalue2->getName().c_str(); // Value name - name2 = name2.substring(0, 6); // String length limit for value name - value2 = bvalue2->value; // Value as double in SI unit - bool valid2 = bvalue2->valid; // Valid information - String svalue2 = formatValue(bvalue2, *commonData).svalue; // Formatted value - String unit2 = formatValue(bvalue2, *commonData).unit; // Unit of value - if (valid2 == true) { - svalue2old = svalue2; // Save old value - unit2old = unit2; // Save old unit - } - - // Get boat values for HDOP - GwApi::BoatValue* bvalue3 = pageData.values[2]; // Third element in list - String name3 = bvalue3->getName().c_str(); // Value name - name3 = name3.substring(0, 6); // String length limit for value name - value3 = bvalue3->value; // Value as double in SI unit - bool valid3 = bvalue3->valid; // Valid information - String svalue3 = formatValue(bvalue3, *commonData).svalue; // Formatted value - String unit3 = formatValue(bvalue3, *commonData).unit; // Unit of value - if (valid3 == true) { - svalue3old = svalue3; // Save old value - unit3old = unit3; // Save old unit - } - - // Optical warning by limit violation (unused) - if (String(flashLED) == "Limit Violation") { - setBlinkingLED(false); - setFlashLED(false); - } - - // Logging boat values - if (bvalue1 == NULL) return PAGE_OK; - LOG_DEBUG(GwLog::LOG, "Drawing at PageClock3, %s:%f, %s:%f", name1.c_str(), value1, name2.c_str(), value2); - - // Draw page - //*********************************************************** - - // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update - - getdisplay().setTextColor(commonData->fgcolor); - - time_t tv = mktime(&commonData->data.rtcTime) + timezone * 3600; - struct tm* local_tm = localtime(&tv); - - // Show values GPS date - getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(10, 65); - if (holdvalues == false) { - if (source == 'G') { - // GPS value - getdisplay().print(svalue2); - } else if (commonData->data.rtcValid) { - // RTC value - if (tz == 'L') { - getdisplay().print(formatDate(dateformat, local_tm->tm_year + 1900, local_tm->tm_mon + 1, local_tm->tm_mday)); - } else { - getdisplay().print(formatDate(dateformat, commonData->data.rtcTime.tm_year + 1900, commonData->data.rtcTime.tm_mon + 1, commonData->data.rtcTime.tm_mday)); - } - } else { - getdisplay().print("---"); - } - } else { - getdisplay().print(svalue2old); - } - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(10, 95); - getdisplay().print("Date"); // Name - - // Horizontal separator left - getdisplay().fillRect(0, 149, 60, 3, commonData->fgcolor); - - // Show values GPS time (small text bottom left) - getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(10, 250); - if (holdvalues == false) { - if (source == 'G') { - getdisplay().print(svalue1); // Value - } else if (commonData->data.rtcValid) { - if (tz == 'L') { - getdisplay().print(formatTime('s', local_tm->tm_hour, local_tm->tm_min, local_tm->tm_sec)); - } else { - getdisplay().print(formatTime('s', commonData->data.rtcTime.tm_hour, commonData->data.rtcTime.tm_min, commonData->data.rtcTime.tm_sec)); - } - } else { - getdisplay().print("---"); - } - } else { - getdisplay().print(svalue1old); - } - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(10, 220); - getdisplay().print("Time"); // Name - - // Show values sunrise - String sunrise = "---"; - if ((valid1 and valid2 and valid3 == true) or (homevalid and commonData->data.rtcValid)) { - sunrise = String(commonData->sundata.sunriseHour) + ":" + String(commonData->sundata.sunriseMinute + 100).substring(1); - svalue5old = sunrise; - } else if (simulation) { - sunrise = String("06:42"); - } - - getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(335, 65); - if (holdvalues == false) getdisplay().print(sunrise); // Value - else getdisplay().print(svalue5old); - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(335, 95); - getdisplay().print("SunR"); // Name - - // Horizontal separator right - getdisplay().fillRect(340, 149, 80, 3, commonData->fgcolor); - - // Show values sunset - String sunset = "---"; - if ((valid1 and valid2 and valid3 == true) or (homevalid and commonData->data.rtcValid)) { - sunset = String(commonData->sundata.sunsetHour) + ":" + String(commonData->sundata.sunsetMinute + 100).substring(1); - svalue6old = sunset; - } else if (simulation) { - sunset = String("21:03"); - } - - getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(335, 250); - if (holdvalues == false) getdisplay().print(sunset); // Value - else getdisplay().print(svalue6old); - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(335, 220); - getdisplay().print("SunS"); // Name - - //******************************************************************************************* - - if (mode == 'T') { - // TIMER MODE: countdown timer HH:MM:SS in the center with 7-segment font - - int dispH = timerHours; - int dispM = timerMinutes; - int dispS = timerSeconds; - - // Update remaining time if timer is running (based on RTC) - if (timerRunning && commonData->data.rtcValid) { - struct tm rtcCopy = commonData->data.rtcTime; - time_t nowEpoch = mktime(&rtcCopy); - time_t remaining = timerEndEpoch - nowEpoch; - if (remaining <= 0) { - remaining = 0; - timerRunning = false; - } - int rem = static_cast(remaining); - dispH = rem / 3600; - rem -= dispH * 3600; - dispM = rem / 60; - dispS = rem % 60; - } - - char buf[9]; // "HH:MM:SS" - snprintf(buf, sizeof(buf), "%02d:%02d:%02d", dispH, dispM, dispS); - String timeStr = String(buf); - - // Clear central area and draw large digital time - getdisplay().fillRect(0, 110, getdisplay().width(), 80, commonData->bgcolor); - - getdisplay().setFont(&DSEG7Classic_BoldItalic60pt7b); - - // Determine widths for digits and colon to position selection underline exactly - int16_t x0, y0; - uint16_t wDigit, hDigit; - uint16_t wColon, hColon; - - getdisplay().getTextBounds("00", 0, 0, &x0, &y0, &wDigit, &hDigit); - getdisplay().getTextBounds(":", 0, 0, &x0, &y0, &wColon, &hColon); - - uint16_t totalWidth = 3 * wDigit + 2 * wColon; - - int16_t baseX = (static_cast(getdisplay().width()) - static_cast(totalWidth)) / 2; - int16_t centerY = 150; - - // Draw time string centered - int16_t x1b, y1b; - uint16_t wb, hb; - getdisplay().getTextBounds(timeStr, 0, 0, &x1b, &y1b, &wb, &hb); - int16_t textX = (static_cast(getdisplay().width()) - static_cast(wb)) / 2; - int16_t textY = centerY + hb / 2; - - getdisplay().setCursor(textX, textY); - getdisplay().print(timeStr); - - // Selection marker (only visible when not running and POS pressed) - if (!timerRunning && showSelectionMarker) { - int16_t selX = baseX; - if (selectedField == 1) { - selX = baseX + wDigit + wColon; // minutes start - } else if (selectedField == 2) { - selX = baseX + 2 * wDigit + 2 * wColon; // seconds start - } - - int16_t underlineY = centerY + hb / 2 + 2; - getdisplay().fillRect(selX, underlineY, wDigit, 2, commonData->fgcolor); - } - - // Small indicators: timezone and source - getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(180, 110); - if (holdvalues == false) { - getdisplay().print(tz == 'L' ? "LOT" : "UTC"); - } else { - getdisplay().print(unit2old); // date unit - } - - getdisplay().setCursor(185, 190); - if (source == 'G') { - getdisplay().print("GPS"); - } else { - getdisplay().print("RTC"); - } - - } else if (mode == 'D') { - // DIGITAL CLOCK MODE: large 7-segment time based on GPS/RTC - - int hour24 = 0; - int minute24 = 0; - int second24 = 0; - - if (source == 'R' && commonData->data.rtcValid) { - time_t tv2 = mktime(&commonData->data.rtcTime); - if (tz == 'L') { - tv2 += static_cast(timezone * 3600); - } - struct tm* tm2 = localtime(&tv2); - hour24 = tm2->tm_hour; - minute24 = tm2->tm_min; - second24 = tm2->tm_sec; - } else { - double t = value1; - if (tz == 'L') { - t += timezone * 3600; - } - if (t >= 86400) t -= 86400; - if (t < 0) t += 86400; - hour24 = static_cast(t / 3600.0); - int rest = static_cast(t) - hour24 * 3600; - minute24 = rest / 60; - second24 = rest % 60; - } - - char buf[9]; // "HH:MM:SS" - snprintf(buf, sizeof(buf), "%02d:%02d:%02d", hour24, minute24, second24); - String timeStr = String(buf); - - getdisplay().fillRect(0, 110, getdisplay().width(), 80, commonData->bgcolor); - - getdisplay().setFont(&DSEG7Classic_BoldItalic60pt7b); - - int16_t x1b, y1b; - uint16_t wb, hb; - getdisplay().getTextBounds(timeStr, 0, 0, &x1b, &y1b, &wb, &hb); - - int16_t x = (static_cast(getdisplay().width()) - static_cast(wb)) / 2; - int16_t y = 150 + hb / 2; - - getdisplay().setCursor(x, y); - getdisplay().print(timeStr); - - getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(180, 110); - if (holdvalues == false) { - getdisplay().print(tz == 'L' ? "LOT" : "UTC"); - } else { - getdisplay().print(unit2old); // date unit - } - - getdisplay().setCursor(185, 190); - if (source == 'G') { - getdisplay().print("GPS"); - } else { - getdisplay().print("RTC"); - } - - } else { - // ANALOG CLOCK MODE (mode == 'A') - - int rInstrument = 110; // Radius of clock - float pi = 3.141592; - - getdisplay().fillCircle(200, 150, rInstrument + 10, commonData->fgcolor); // Outer circle - getdisplay().fillCircle(200, 150, rInstrument + 7, commonData->bgcolor); // Outer circle - - for (int i = 0; i < 360; i = i + 1) - { - // Scaling values - float x = 200 + (rInstrument - 30) * sin(i / 180.0 * pi); // x-coordinate dots - float y = 150 - (rInstrument - 30) * cos(i / 180.0 * pi); // y-coordinate dots - const char* ii = ""; - switch (i) - { - case 0: ii = "12"; break; - case 90: ii = "3"; break; - case 180: ii = "6"; break; - case 270: ii = "9"; break; - default: break; - } - - // Print text centered on position x, y - int16_t x1c, y1c; // Return values of getTextBounds - uint16_t wc, hc; // Return values of getTextBounds - getdisplay().getTextBounds(ii, int(x), int(y), &x1c, &y1c, &wc, &hc); // Calc width of new string - getdisplay().setCursor(x - wc / 2, y + hc / 2); - if (i % 90 == 0) { - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().print(ii); - } - - // Draw sub scale with dots - float sinx = 0; - float cosx = 0; - if (i % 6 == 0) { - float x1d = 200 + rInstrument * sin(i / 180.0 * pi); - float y1d = 150 - rInstrument * cos(i / 180.0 * pi); - getdisplay().fillCircle((int)x1d, (int)y1d, 2, commonData->fgcolor); - sinx = sin(i / 180.0 * pi); - cosx = cos(i / 180.0 * pi); - } - - // Draw sub scale with lines (two triangles) - if (i % 30 == 0) { - float dx = 2; // Line thickness = 2*dx+1 - float xx1 = -dx; - float xx2 = +dx; - float yy1 = -(rInstrument - 10); - float yy2 = -(rInstrument + 10); - getdisplay().fillTriangle(200 + (int)(cosx * xx1 - sinx * yy1), 150 + (int)(sinx * xx1 + cosx * yy1), - 200 + (int)(cosx * xx2 - sinx * yy1), 150 + (int)(sinx * xx2 + cosx * yy1), - 200 + (int)(cosx * xx1 - sinx * yy2), 150 + (int)(sinx * xx1 + cosx * yy2), commonData->fgcolor); - getdisplay().fillTriangle(200 + (int)(cosx * xx2 - sinx * yy1), 150 + (int)(sinx * xx2 + cosx * yy1), - 200 + (int)(cosx * xx1 - sinx * yy2), 150 + (int)(sinx * xx1 + cosx * yy2), - 200 + (int)(cosx * xx2 - sinx * yy2), 150 + (int)(sinx * xx2 + cosx * yy2), commonData->fgcolor); - } - } - - // Print Unit in clock - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(175, 110); - if (holdvalues == false) { - getdisplay().print(tz == 'L' ? "LOT" : "UTC"); - } else { - getdisplay().print(unit2old); // date unit - } - - getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(185, 190); - if (source == 'G') { - getdisplay().print("GPS"); - } else { - getdisplay().print("RTC"); - } - - // Clock values - double hour = 0; - double minute = 0; - if (source == 'R') { - if (tz == 'L') { - time_t tv2 = mktime(&commonData->data.rtcTime) + timezone * 3600; - struct tm* local_tm2 = localtime(&tv2); - minute = local_tm2->tm_min; - hour = local_tm2->tm_hour; - } else { - minute = commonData->data.rtcTime.tm_min; - hour = commonData->data.rtcTime.tm_hour; - } - hour += minute / 60; - } else { - if (tz == 'L') { - value1 += timezone * 3600; - } - if (value1 > 86400) { value1 -= 86400; } - if (value1 < 0) { value1 += 86400; } - hour = (value1 / 3600.0); - // minute = (hour - int(hour)) * 3600.0 / 60.0; // Analog minute pointer smooth moving - minute = int((hour - int(hour)) * 3600.0 / 60.0); // Jumping minute pointer from minute to minute - } - if (hour > 12) { - hour -= 12.0; - } - LOG_DEBUG(GwLog::DEBUG, "... PageClock3, value1: %f hour: %f minute:%f", value1, hour, minute); - - // Draw hour pointer - float startwidth = 8; // Start width of pointer - if (valid1 == true || (source == 'R' && commonData->data.rtcValid) || holdvalues == true || simulation == true) { - float sinx = sin(hour * 30.0 * pi / 180); // Hour - float cosx = cos(hour * 30.0 * pi / 180); - // Normal pointer - // Pointer as triangle with center base 2*width - float xx1 = -startwidth; - float xx2 = startwidth; - float yy1 = -startwidth; - float yy2 = -(rInstrument * 0.5); - getdisplay().fillTriangle(200 + (int)(cosx * xx1 - sinx * yy1), 150 + (int)(sinx * xx1 + cosx * yy1), - 200 + (int)(cosx * xx2 - sinx * yy1), 150 + (int)(sinx * xx2 + cosx * yy1), - 200 + (int)(cosx * 0 - sinx * yy2), 150 + (int)(sinx * 0 + cosx * yy2), commonData->fgcolor); - // Inverted pointer - // Pointer as triangle with center base 2*width - float endwidth = 2; // End width of pointer - float ix1 = endwidth; - float ix2 = -endwidth; - float iy1 = -(rInstrument * 0.5); - float iy2 = -endwidth; - getdisplay().fillTriangle(200 + (int)(cosx * ix1 - sinx * iy1), 150 + (int)(sinx * ix1 + cosx * iy1), - 200 + (int)(cosx * ix2 - sinx * iy1), 150 + (int)(sinx * ix2 + cosx * iy1), - 200 + (int)(cosx * 0 - sinx * iy2), 150 + (int)(sinx * 0 + cosx * iy2), commonData->fgcolor); - } - - // Draw minute pointer - startwidth = 8; // Start width of pointer - if (valid1 == true || (source == 'R' && commonData->data.rtcValid) || holdvalues == true || simulation == true) { - float sinx = sin(minute * 6.0 * pi / 180); // Minute - float cosx = cos(minute * 6.0 * pi / 180); - // Normal pointer - // Pointer as triangle with center base 2*width - float xx1 = -startwidth; - float xx2 = startwidth; - float yy1 = -startwidth; - float yy2 = -(rInstrument - 15); - getdisplay().fillTriangle(200 + (int)(cosx * xx1 - sinx * yy1), 150 + (int)(sinx * xx1 + cosx * yy1), - 200 + (int)(cosx * xx2 - sinx * yy1), 150 + (int)(sinx * xx2 + cosx * yy1), - 200 + (int)(cosx * 0 - sinx * yy2), 150 + (int)(sinx * 0 + cosx * yy2), commonData->fgcolor); - // Inverted pointer - // Pointer as triangle with center base 2*width - float endwidth = 2; // End width of pointer - float ix1 = endwidth; - float ix2 = -endwidth; - float iy1 = -(rInstrument - 15); - float iy2 = -endwidth; - getdisplay().fillTriangle(200 + (int)(cosx * ix1 - sinx * iy1), 150 + (int)(sinx * ix1 + cosx * iy1), - 200 + (int)(cosx * ix2 - sinx * iy1), 150 + (int)(sinx * ix2 + cosx * iy1), - 200 + (int)(cosx * 0 - sinx * iy2), 150 + (int)(sinx * 0 + cosx * iy2), commonData->fgcolor); - } - - // Center circle - getdisplay().fillCircle(200, 150, startwidth + 6, commonData->bgcolor); - getdisplay().fillCircle(200, 150, startwidth + 4, commonData->fgcolor); - } - - return PAGE_UPDATE; - }; -}; - -// Static member definitions -bool PageClock3::timerInitialized = false; -bool PageClock3::timerRunning = false; -int PageClock3::timerHours = 0; -int PageClock3::timerMinutes = 0; -int PageClock3::timerSeconds = 0; -int PageClock3::selectedField = 0; -bool PageClock3::showSelectionMarker = true; -time_t PageClock3::timerEndEpoch = 0; - -static Page* createPage(CommonData& common) -{ - return new PageClock3(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 - * we provide the number of user parameters we expect (0 here) - * and we provide the names of the fixed values we need - */ -PageDescription registerPageClock3( - "Clock3", // Page name - createPage, // Action - 0, // Number of bus values depends on selection in Web configuration - {"GPST", "GPSD", "HDOP"}, // Bus values we need in the page - true // Show display header on/off -); - -#endif - diff --git a/lib/obp60task/PageClockDigital.new b/lib/obp60task/PageClockDigital.new deleted file mode 100644 index d4a3984..0000000 --- a/lib/obp60task/PageClockDigital.new +++ /dev/null @@ -1,224 +0,0 @@ -#if defined BOARD_OBP60S3 || defined BOARD_OBP40S3 - -#include "Pagedata.h" -#include "OBP60Extensions.h" - -/** - * Simple digital clock page. - * - * - Shows system time as large digital value in the center - * - Uses same data sources and configuration as PageClock (GPS / RTC, time zone) - * - Keys: - * K1: toggle time source (GPS / RTC) - * K5: toggle time zone (Local / UTC) - * K11: keylock - */ -class PageClockDigital : public Page -{ - bool simulation = false; - int simtime = 0; - char source; // time source (R)TC | (G)PS - char tz = 'L'; // time zone (L)ocal | (U)TC - double timezone = 0.0; - -public: - PageClockDigital(CommonData& common) - { - commonData = &common; - common.logger->logDebug(GwLog::LOG, "Instantiate PageClockDigital"); - - simulation = common.config->getBool(common.config->useSimuData); - timezone = common.config->getString(common.config->timeZone).toDouble(); - -#ifdef BOARD_OBP60S3 - source = 'G'; // default to GPS time on OBP60 -#endif -#ifdef BOARD_OBP40S3 - source = 'R'; // default to RTC time on OBP40 -#endif - simtime = 38160; // time value 11:36 for simulation (seconds) - } - - virtual void setupKeys() - { - Page::setupKeys(); - commonData->keydata[0].label = "SRC"; - commonData->keydata[4].label = "TZ"; - } - - // Key functions - virtual int handleKey(int key) - { - // Time source - if (key == 1) { - switch (source) { - case 'G': - source = 'R'; - break; - case 'R': - source = 'G'; - break; - default: - source = 'G'; - break; - } - return 0; - } - - // Time zone: Local / UTC - if (key == 5) { - switch (tz) { - case 'L': - tz = 'U'; - break; - case 'U': - tz = 'L'; - break; - default: - tz = 'L'; - break; - } - return 0; - } - - // Keylock function - if (key == 11) { // Code for keylock - commonData->keylock = !commonData->keylock; - return 0; // Commit the key - } - return key; - } - - int displayPage(PageData& pageData) - { - GwConfigHandler* config = commonData->config; - - static String svalueTimeOld = ""; - static String svalueDateOld = ""; - - // Get config data - bool holdvalues = config->getBool(config->holdvalues); - String flashLED = config->getString(config->flashLED); - - // Get boat values for GPS time and date (same as PageClock) - if (pageData.values.size() < 2) { - return PAGE_OK; - } - - GwApi::BoatValue* bvalueTime = pageData.values[0]; - GwApi::BoatValue* bvalueDate = pageData.values[1]; - - if (bvalueTime == nullptr || bvalueDate == nullptr) { - return PAGE_OK; - } - - double valueTime = 0; - if (!simulation) { - valueTime = bvalueTime->value; // Value as double in SI unit (seconds) - } else { - valueTime = simtime++; // Simulation data - } - bool validTime = bvalueTime->valid; - String svalueTime = formatValue(bvalueTime, *commonData).svalue; // formatted time string - if (validTime) { - svalueTimeOld = svalueTime; // Save old value - } - - bool validDate = bvalueDate->valid; - String svalueDate = formatValue(bvalueDate, *commonData).svalue; // formatted date string - if (validDate) { - svalueDateOld = svalueDate; // Save old value - } - - // Optical warning by limit violation (unused) - if (flashLED == "Limit Violation") { - setBlinkingLED(false); - setFlashLED(false); - } - - // Draw page - //*********************************************************** - - // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update - getdisplay().setTextColor(commonData->fgcolor); - - // Build time string depending on source and configuration - String timeStr = "---"; - - if (!holdvalues) { - if (source == 'G') { - // GPS value as formatted by formatter - timeStr = svalueTime; - } else if (commonData->data.rtcValid) { - // RTC value - time_t tv = mktime(&commonData->data.rtcTime); - if (tz == 'L') { - tv += static_cast(timezone * 3600); - } - struct tm* local_tm = localtime(&tv); - timeStr = formatTime('s', local_tm->tm_hour, local_tm->tm_min, local_tm->tm_sec); - } - } else { - timeStr = svalueTimeOld; - } - - // Clear central area and draw large digital time - getdisplay().fillRect(0, 80, getdisplay().width(), 140, commonData->bgcolor); - - getdisplay().setFont(&DSEG7Classic_BoldItalic60pt7b); - - int16_t x1, y1; - uint16_t w, h; - getdisplay().getTextBounds(timeStr, 0, 0, &x1, &y1, &w, &h); - - int16_t x = (static_cast(getdisplay().width()) - static_cast(w)) / 2; - int16_t y = (static_cast(getdisplay().height()) + static_cast(h)) / 2; - - getdisplay().setCursor(x, y); - getdisplay().print(timeStr); - - // Show date in the upper left corner - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(10, 40); - getdisplay().print("Date"); - - getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(10, 60); - if (!holdvalues) { - getdisplay().print(svalueDate); - } else { - getdisplay().print(svalueDateOld); - } - - // Show small labels for source and timezone in the lower right corner - getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(getdisplay().width() - 80, getdisplay().height() - 40); - getdisplay().print(source == 'G' ? "GPS" : "RTC"); - - getdisplay().setCursor(getdisplay().width() - 80, getdisplay().height() - 20); - getdisplay().print(tz == 'L' ? "LOC" : "UTC"); - - return PAGE_UPDATE; - }; -}; - -static Page* createPage(CommonData& common) -{ - return new PageClockDigital(common); -} - -/** - * Register page so it can be selected in the configuration. - * Uses the same fixed values as PageClock. - */ -PageDescription registerPageClockDigital( - "ClockDigital", // Page name - createPage, // Action - 0, // Number of user parameters - {"GPST", "GPSD", "HDOP"}, // Bus values we need in the page - true // Show display header on/off -); - -#endif - From 1d2ba2f71deaa8001cd530b1ed46cd12d50bdc65 Mon Sep 17 00:00:00 2001 From: Thomas Hooge Date: Sun, 8 Feb 2026 18:02:50 +0100 Subject: [PATCH 118/121] Make code compile for OBP60 v2.0 again --- lib/obp60task/OBP60Hardware.h | 2 +- lib/obp60task/OBP60Keypad.h | 2 +- lib/obp60task/obp60task.cpp | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/obp60task/OBP60Hardware.h b/lib/obp60task/OBP60Hardware.h index 8768e0f..6d038d3 100644 --- a/lib/obp60task/OBP60Hardware.h +++ b/lib/obp60task/OBP60Hardware.h @@ -1,7 +1,7 @@ // General hardware definitions // CAN and RS485 bus pin definitions see obp60task.h -#ifdef HARDWARE_V21 +#if defined HARDWARE_V20 || HARDWARE_V21 // Direction pin for RS485 NMEA0183 #define OBP_DIRECTION_PIN 18 // I2C diff --git a/lib/obp60task/OBP60Keypad.h b/lib/obp60task/OBP60Keypad.h index eafe3a2..d669ccb 100644 --- a/lib/obp60task/OBP60Keypad.h +++ b/lib/obp60task/OBP60Keypad.h @@ -58,7 +58,7 @@ void initKeys(CommonData &commonData) { commonData.keydata[5].h = height; } - #ifdef HARDWARE_V21 + #if defined HARDWARE_V20 || HARDWARE_V21 // Keypad functions for original OBP60 hardware int readKeypad(GwLog* logger, uint thSensitivity, bool use_syspage) { diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index cc825fa..84bc572 100644 --- a/lib/obp60task/obp60task.cpp +++ b/lib/obp60task/obp60task.cpp @@ -332,7 +332,7 @@ void OBP60Task(GwApi *api){ // return; GwLog *logger=api->getLogger(); GwConfigHandler *config=api->getConfig(); -#ifdef HARDWARE_V21 +#if defined HARDWARE_V20 || HARDWARE_V21 startLedTask(api); #endif PageList allPages; @@ -341,7 +341,7 @@ void OBP60Task(GwApi *api){ commonData.logger=logger; commonData.config=config; -#ifdef HARDWARE_V21 +#if defined HARDWARE_V20 || HARDWARE_V21 // Keyboard coordinates for page footer initKeys(commonData); #endif From 04dc09e44ab08e2ef477224ab7fcc2d6816b3979 Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Mon, 9 Feb 2026 15:25:27 +0100 Subject: [PATCH 119/121] Fix directory path for tool installation --- lib/obp60task/run_install_tools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/obp60task/run_install_tools b/lib/obp60task/run_install_tools index 9c4667e..c46c10c 100644 --- a/lib/obp60task/run_install_tools +++ b/lib/obp60task/run_install_tools @@ -8,6 +8,6 @@ # Install tools echo "Installing tools" -cd /workspace/esp32-nmea2000 +cd /workspaces/esp32-nmea2000 pip3 install -U esptool pip3 install platformio From fc5daaba3788382ba3f608420a76fd5d8c5750ab Mon Sep 17 00:00:00 2001 From: Ulrich Meine Date: Mon, 9 Feb 2026 22:31:07 +0100 Subject: [PATCH 120/121] - change control of key settings on PageOneValue + PageTwoValues - added taskYIELD() to chart loop to be nice to other tasks - fix typo in config_obp40.json - fine tune chart labels - disable debug messages --- lib/obp60task/OBPDataOperations.cpp | 22 +++++++++++----------- lib/obp60task/OBPcharts.cpp | 24 +++++++++++++----------- lib/obp60task/PageOneValue.cpp | 17 +++++++++++------ lib/obp60task/PageTwoValues.cpp | 21 +++++++++++++-------- lib/obp60task/PageWindPlot.cpp | 4 ++-- lib/obp60task/config_obp40.json | 20 ++++++++++---------- 6 files changed, 60 insertions(+), 48 deletions(-) diff --git a/lib/obp60task/OBPDataOperations.cpp b/lib/obp60task/OBPDataOperations.cpp index 562b1f6..c968e11 100644 --- a/lib/obp60task/OBPDataOperations.cpp +++ b/lib/obp60task/OBPDataOperations.cpp @@ -135,7 +135,7 @@ bool CalibrationData::calibrateInstance(GwApi::BoatValue* boatDataValue) double dataValue = 0; std::string format = ""; - // we test this earlier, but for safety reason ... + // we test this earlier, but for safety reasons ... if (calibrationMap.find(instance) == calibrationMap.end()) { LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s not in calibration list", instance.c_str()); return false; @@ -151,7 +151,7 @@ bool CalibrationData::calibrateInstance(GwApi::BoatValue* boatDataValue) slope = calibrationMap[instance].slope; dataValue = boatDataValue->value; format = boatDataValue->getFormat().c_str(); - LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s: value: %f, format: %s", instance.c_str(), dataValue, format.c_str()); + // LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s: value: %f, format: %s", instance.c_str(), dataValue, format.c_str()); if (format == "formatWind") { // instance is of type angle dataValue = (dataValue * slope) + offset; @@ -174,7 +174,7 @@ bool CalibrationData::calibrateInstance(GwApi::BoatValue* boatDataValue) calibrationMap[instance].value = dataValue; // store the calibrated value in the list calibrationMap[instance].isCalibrated = true; - LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s: Offset: %f, Slope: %f, Result: %f", instance.c_str(), offset, slope, calibrationMap[instance].value); + // LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s: Offset: %f, Slope: %f, Result: %f", instance.c_str(), offset, slope, calibrationMap[instance].value); return true; } @@ -189,7 +189,7 @@ bool CalibrationData::smoothInstance(GwApi::BoatValue* boatDataValue) // we test this earlier, but for safety reason ... if (calibrationMap.find(instance) == calibrationMap.end()) { - LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s not in calibration list", instance.c_str()); + // LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s not in calibration list", instance.c_str()); return false; } @@ -211,7 +211,7 @@ bool CalibrationData::smoothInstance(GwApi::BoatValue* boatDataValue) calibrationMap[instance].value = dataValue; // store the smoothed value in the list calibrationMap[instance].isCalibrated = true; - LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s: smooth: %f, oldValue: %f, result: %f", instance.c_str(), smoothFactor, oldValue, calibrationMap[instance].value); + // LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s: smooth: %f, oldValue: %f, result: %f", instance.c_str(), smoothFactor, oldValue, calibrationMap[instance].value); return true; } @@ -241,7 +241,7 @@ void HstryBuf::add(double value) { if (value >= hstryMin && value <= hstryMax) { hstryBuf.add(value); - LOG_DEBUG(GwLog::DEBUG, "HstryBuf::add: name: %s, value: %.3f", hstryBuf.getName(), value); + // LOG_DEBUG(GwLog::DEBUG, "HstryBuf::add: name: %s, value: %.3f", hstryBuf.getName(), value); } } @@ -405,7 +405,7 @@ void WindUtils::calcTwdSA(const double* AWA, const double* AWS, double stw = -*STW; addPolar(AWD, AWS, CTW, &stw, TWD, TWS); - // Normalize TWD and TWA to 0-360°/2PI + // Normalize TWD to [0..360°] (2PI) and TWA to [-180..180] (PI) *TWD = to2PI(*TWD); *TWA = toPI(*TWD - *HDT); } @@ -487,8 +487,8 @@ bool WindUtils::addWinds() double hdtVal = hdtBVal->valid ? hdtBVal->value : DBL_MAX; double hdmVal = hdmBVal->valid ? hdmBVal->value : DBL_MAX; double varVal = varBVal->valid ? varBVal->value : DBL_MAX; - LOG_DEBUG(GwLog::DEBUG, "WindUtils:addWinds: AWA %.1f, AWS %.1f, COG %.1f, STW %.1f, SOG %.2f, HDT %.1f, HDM %.1f, VAR %.1f", awaBVal->value * RAD_TO_DEG, awsBVal->value * 3.6 / 1.852, - cogBVal->value * RAD_TO_DEG, stwBVal->value * 3.6 / 1.852, sogBVal->value * 3.6 / 1.852, hdtBVal->value * RAD_TO_DEG, hdmBVal->value * RAD_TO_DEG, varBVal->value * RAD_TO_DEG); + //LOG_DEBUG(GwLog::DEBUG, "WindUtils:addWinds: AWA %.1f, AWS %.1f, COG %.1f, STW %.1f, SOG %.2f, HDT %.1f, HDM %.1f, VAR %.1f", awaBVal->value * RAD_TO_DEG, awsBVal->value * 3.6 / 1.852, + // cogBVal->value * RAD_TO_DEG, stwBVal->value * 3.6 / 1.852, sogBVal->value * 3.6 / 1.852, hdtBVal->value * RAD_TO_DEG, hdmBVal->value * RAD_TO_DEG, varBVal->value * RAD_TO_DEG); // Check if TWD can be calculated from TWA and HDT/HDM if (twaBVal->valid) { @@ -528,8 +528,8 @@ bool WindUtils::addWinds() } } } - LOG_DEBUG(GwLog::DEBUG, "WindUtils:addWinds: twCalculated %d, TWD %.1f, TWA %.1f, TWS %.2f kn, AWD: %.1f", twCalculated, twdBVal->value * RAD_TO_DEG, - twaBVal->value * RAD_TO_DEG, twsBVal->value * 3.6 / 1.852, awdBVal->value * RAD_TO_DEG); + // LOG_DEBUG(GwLog::DEBUG, "WindUtils:addWinds: twCalculated %d, TWD %.1f, TWA %.1f, TWS %.2f kn, AWD: %.1f", twCalculated, twdBVal->value * RAD_TO_DEG, + // twaBVal->value * RAD_TO_DEG, twsBVal->value * 3.6 / 1.852, awdBVal->value * RAD_TO_DEG); return twCalculated; } diff --git a/lib/obp60task/OBPcharts.cpp b/lib/obp60task/OBPcharts.cpp index e15fc83..8ae38ec 100644 --- a/lib/obp60task/OBPcharts.cpp +++ b/lib/obp60task/OBPcharts.cpp @@ -161,8 +161,8 @@ bool Chart::setChartDimensions(const char direction, const int8_t size) break; } } - LOG_DEBUG(GwLog::ERROR, "obp60:setChartDimensions %s: direction: %c, size: %d, dWidth: %d, dHeight: %d, timAxis: %d, valAxis: %d, cRoot{%d, %d}, top: %d, bottom: %d, hGap: %d, vGap: %d", - dataBuf.getName(), direction, size, dWidth, dHeight, timAxis, valAxis, cRoot.x, cRoot.y, top, bottom, hGap, vGap); + //LOG_DEBUG(GwLog::DEBUG, "obp60:setChartDimensions %s: direction: %c, size: %d, dWidth: %d, dHeight: %d, timAxis: %d, valAxis: %d, cRoot{%d, %d}, top: %d, bottom: %d, hGap: %d, vGap: %d", + // dataBuf.getName(), direction, size, dWidth, dHeight, timAxis, valAxis, cRoot.x, cRoot.y, top, bottom, hGap, vGap); return true; } @@ -176,7 +176,7 @@ void Chart::drawChrt(const char chrtDir, const int8_t chrtIntv, GwApi::BoatValue // LOG_DEBUG(GwLog::DEBUG, "Chart:drawChart: min: %.1f, mid: %.1f, max: %.1f, rng: %.1f", chrtMin, chrtMid, chrtMax, chrtRng); calcChrtBorders(chrtMin, chrtMid, chrtMax, chrtRng); chrtScale = double(valAxis) / chrtRng; // Chart scale: pixels per value step - LOG_DEBUG(GwLog::DEBUG, "Chart:drawChart: min: %.1f, mid: %.1f, max: %.1f, rng: %.1f", chrtMin, chrtMid, chrtMax, chrtRng); + // LOG_DEBUG(GwLog::DEBUG, "Chart:drawChart: min: %.1f, mid: %.1f, max: %.1f, rng: %.1f", chrtMin, chrtMid, chrtMax, chrtRng); // Do we have valid buffer data? if (dataBuf.getMax() == dbMAX_VAL) { // only values in buffer -> no valid wind data available @@ -261,8 +261,8 @@ void Chart::calcChrtBorders(double& rngMin, double& rngMid, double& rngMax, doub } recalcRngMid = false; // Reset flag for determination - LOG_DEBUG(GwLog::DEBUG, "calcChrtRange: rngMin: %.1f°, rngMid: %.1f°, rngMax: %.1f°, rng: %.1f°, rngStep: %.1f°", rngMin * RAD_TO_DEG, rngMid * RAD_TO_DEG, rngMax * RAD_TO_DEG, - rng * RAD_TO_DEG, rngStep * RAD_TO_DEG); + // LOG_DEBUG(GwLog::DEBUG, "calcChrtRange: rngMin: %.1f°, rngMid: %.1f°, rngMax: %.1f°, rng: %.1f°, rngStep: %.1f°", rngMin * RAD_TO_DEG, rngMid * RAD_TO_DEG, rngMax * RAD_TO_DEG, + // rng * RAD_TO_DEG, rngStep * RAD_TO_DEG); } } @@ -287,8 +287,8 @@ void Chart::calcChrtBorders(double& rngMin, double& rngMid, double& rngMax, doub rng = halfRng * 2.0; - LOG_DEBUG(GwLog::DEBUG, "calcChrtBorders: rngMin: %.1f°, rngMid: %.1f°, rngMax: %.1f°, tmpRng: %.1f°, rng: %.1f°, rngStep: %.1f°", rngMin * RAD_TO_DEG, rngMid * RAD_TO_DEG, rngMax * RAD_TO_DEG, - tmpRng * RAD_TO_DEG, rng * RAD_TO_DEG, rngStep * RAD_TO_DEG); + // LOG_DEBUG(GwLog::DEBUG, "calcChrtBorders: rngMin: %.1f°, rngMid: %.1f°, rngMax: %.1f°, tmpRng: %.1f°, rng: %.1f°, rngStep: %.1f°", rngMin * RAD_TO_DEG, rngMid * RAD_TO_DEG, rngMax * RAD_TO_DEG, + // tmpRng * RAD_TO_DEG, rng * RAD_TO_DEG, rngStep * RAD_TO_DEG); } else { // chart data is of any other type @@ -320,8 +320,8 @@ void Chart::calcChrtBorders(double& rngMin, double& rngMid, double& rngMax, doub rngMid = (rngMin + rngMax) / 2.0; rng = rngMax - rngMin; - LOG_DEBUG(GwLog::DEBUG, "calcChrtRange-end: currMinVal: %.1f, currMaxVal: %.1f, rngMin: %.1f, rngMid: %.1f, rngMax: %.1f, rng: %.1f, rngStep: %.1f, zeroValue: %.1f, dbMIN_VAL: %.1f", - currMinVal, currMaxVal, rngMin, rngMid, rngMax, rng, rngStep, zeroValue, dbMIN_VAL); + // LOG_DEBUG(GwLog::DEBUG, "calcChrtRange-end: currMinVal: %.1f, currMaxVal: %.1f, rngMin: %.1f, rngMid: %.1f, rngMax: %.1f, rng: %.1f, rngStep: %.1f, zeroValue: %.1f, dbMIN_VAL: %.1f", + // currMinVal, currMaxVal, rngMin, rngMid, rngMax, rng, rngStep, zeroValue, dbMIN_VAL); } } @@ -397,6 +397,8 @@ void Chart::drawChartLines(const char direction, const int8_t chrtIntv, const do } break; } + + taskYIELD(); // we run for 50-150ms; be polite to other tasks with same priority } } @@ -656,7 +658,7 @@ void Chart::prntHorizChartThreeValueAxisLabel(const GFXfont* font) if (font == &Ubuntu_Bold10pt8b) { xOffset = 39; - yOffset = 15; + yOffset = 16; } else if (font == &Ubuntu_Bold12pt8b) { xOffset = 51; yOffset = 18; @@ -718,7 +720,7 @@ void Chart::prntHorizChartMultiValueAxisLabel(const GFXfont* font) axSlots = valAxis / static_cast(VALAXIS_STEP); // number of axis labels (and we want to have a double calculation, no integer) axIntv = chrtRng / axSlots; axLabel = chrtMin + axIntv; - LOG_DEBUG(GwLog::DEBUG, "Chart::printHorizMultiValueAxisLabel: chrtRng: %.2f, th-chrtRng: %.2f, axSlots: %.2f, axIntv: %.2f, axLabel: %.2f, chrtMin: %.2f, chrtMid: %.2f, chrtMax: %.2f", chrtRng, this->chrtRng, axSlots, axIntv, axLabel, this->chrtMin, chrtMid, chrtMax); + // LOG_DEBUG(GwLog::DEBUG, "Chart::printHorizMultiValueAxisLabel: chrtRng: %.2f, th-chrtRng: %.2f, axSlots: %.2f, axIntv: %.2f, axLabel: %.2f, chrtMin: %.2f, chrtMid: %.2f, chrtMax: %.2f", chrtRng, this->chrtRng, axSlots, axIntv, axLabel, this->chrtMin, chrtMid, chrtMax); int loopStrt, loopEnd, loopStp; if (chrtDataFmt == SPEED || chrtDataFmt == TEMPERATURE || chrtDataFmt == OTHER) { diff --git a/lib/obp60task/PageOneValue.cpp b/lib/obp60task/PageOneValue.cpp index a5c0f9c..78cc6e6 100644 --- a/lib/obp60task/PageOneValue.cpp +++ b/lib/obp60task/PageOneValue.cpp @@ -162,9 +162,13 @@ public: constexpr int ZOOM_KEY = 1; #endif - if (dataHstryBuf) { // show "Mode" key only if chart supported boat data type is available + if (dataHstryBuf) { // show "Mode" key only if chart-supported boat data type is available commonData->keydata[0].label = "MODE"; - commonData->keydata[ZOOM_KEY].label = "ZOOM"; + if (pageMode != VALUE) { // show "ZOOM" key only if chart is visible + commonData->keydata[ZOOM_KEY].label = "ZOOM"; + } else { + commonData->keydata[ZOOM_KEY].label = ""; + } } else { commonData->keydata[0].label = ""; commonData->keydata[ZOOM_KEY].label = ""; @@ -189,14 +193,15 @@ public: pageMode = VALUE; break; } + setupKeys(); // Adjust key definition depending on and chart-supported boat data type return 0; // Commit the key } - // Set time frame to show for history chart + // Set time frame to show for chart #if defined BOARD_OBP60S3 - if (key == 5) { + if (key == 5 && pageMode != VALUE) { #elif defined BOARD_OBP40S3 - if (key == 2) { + if (key == 2 && pageMode != VALUE) { #endif if (dataIntv == 1) { dataIntv = 2; @@ -247,7 +252,7 @@ public: } } - setupKeys(); // adjust key depending on chart supported boat data type + setupKeys(); // Adjust key definition depending on and chart-supported boat data type } int displayPage(PageData& pageData) diff --git a/lib/obp60task/PageTwoValues.cpp b/lib/obp60task/PageTwoValues.cpp index 323cbcb..5990656 100644 --- a/lib/obp60task/PageTwoValues.cpp +++ b/lib/obp60task/PageTwoValues.cpp @@ -114,7 +114,7 @@ private: } if (numValues == 2 && mode == FULL) { // print line only, if we want to show 2 data values - getdisplay().fillRect(0, 145, width, 3, commonData->fgcolor); // Horizontal line 3 pix + getdisplay().fillRect(0, 145, width, 3, commonData->fgcolor); // Horizontal line 3 pix } } @@ -149,7 +149,11 @@ public: if (dataHstryBuf[0] || dataHstryBuf[1]) { // show "Mode" key only if at least 1 chart supported boat data type is available commonData->keydata[0].label = "MODE"; - commonData->keydata[ZOOM_KEY].label = "ZOOM"; + if (pageMode != VALUES) { // show "ZOOM" key only if chart is visible + commonData->keydata[ZOOM_KEY].label = "ZOOM"; + } else { + commonData->keydata[ZOOM_KEY].label = ""; + } } else { commonData->keydata[0].label = ""; commonData->keydata[ZOOM_KEY].label = ""; @@ -191,14 +195,15 @@ public: pageMode = VALUES; break; } + setupKeys(); // Adjust key definition depending on and chart-supported boat data type return 0; // Commit the key } - // Set time frame to show for history chart + // Set time frame to show for chart #if defined BOARD_OBP60S3 - if (key == 5) { + if (key == 5 && pageMode != VALUES) { #elif defined BOARD_OBP40S3 - if (key == 2) { + if (key == 2 && pageMode != VALUES) { #endif if (dataIntv == 1) { dataIntv = 2; @@ -251,7 +256,7 @@ public: } } - setupKeys(); // adjust key depending on chart supported boat data type + setupKeys(); // Adjust key definition depending on and chart-supported boat data type } int displayPage(PageData& pageData) @@ -285,13 +290,13 @@ public: showData(bValue, FULL); } else if (pageMode == VAL1_CHART) { // show data value 1 and chart - showData({bValue[0]}, HALF); + showData({ bValue[0] }, HALF); if (dataChart[0]) { dataChart[0]->showChrt(HORIZONTAL, HALF_SIZE_BOTTOM, dataIntv, NO_PRNT_NAME, NO_PRNT_VALUE, *bValue[0]); } } else if (pageMode == VAL2_CHART) { // show data value 2 and chart - showData({bValue[1]}, HALF); + showData({ bValue[1] }, HALF); if (dataChart[1]) { dataChart[1]->showChrt(HORIZONTAL, HALF_SIZE_BOTTOM, dataIntv, NO_PRNT_NAME, NO_PRNT_VALUE, *bValue[1]); } diff --git a/lib/obp60task/PageWindPlot.cpp b/lib/obp60task/PageWindPlot.cpp index f5745f7..da948c2 100644 --- a/lib/obp60task/PageWindPlot.cpp +++ b/lib/obp60task/PageWindPlot.cpp @@ -196,7 +196,7 @@ public: int displayPage(PageData& pageData) { LOG_DEBUG(GwLog::LOG, "Display PageWindPlot"); - ulong pageTime = millis(); + // ulong pageTime = millis(); if (showTruW != oldShowTruW) { @@ -243,7 +243,7 @@ public: } } - LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: page time %ldms", millis() - pageTime); + // LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: page time %ldms", millis() - pageTime); return PAGE_UPDATE; } }; diff --git a/lib/obp60task/config_obp40.json b/lib/obp60task/config_obp40.json index 54a5409..4e75006 100644 --- a/lib/obp60task/config_obp40.json +++ b/lib/obp60task/config_obp40.json @@ -1818,7 +1818,7 @@ "description": "Wind source for page 1: [true|apparent]", "list": [ "True wind", - "apparent wind" + "Apparent wind" ], "category": "OBP40 Page 1", "capabilities": { @@ -2140,7 +2140,7 @@ "description": "Wind source for page 2: [true|apparent]", "list": [ "True wind", - "apparent wind" + "Apparent wind" ], "category": "OBP40 Page 2", "capabilities": { @@ -2453,7 +2453,7 @@ "description": "Wind source for page 3: [true|apparent]", "list": [ "True wind", - "apparent wind" + "Apparent wind" ], "category": "OBP40 Page 3", "capabilities": { @@ -2757,7 +2757,7 @@ "description": "Wind source for page 4: [true|apparent]", "list": [ "True wind", - "apparent wind" + "Apparent wind" ], "category": "OBP40 Page 4", "capabilities": { @@ -3052,7 +3052,7 @@ "description": "Wind source for page 5: [true|apparent]", "list": [ "True wind", - "apparent wind" + "Apparent wind" ], "category": "OBP40 Page 5", "capabilities": { @@ -3338,7 +3338,7 @@ "description": "Wind source for page 6: [true|apparent]", "list": [ "True wind", - "apparent wind" + "Apparent wind" ], "category": "OBP40 Page 6", "capabilities": { @@ -3615,7 +3615,7 @@ "description": "Wind source for page 7: [true|apparent]", "list": [ "True wind", - "apparent wind" + "Apparent wind" ], "category": "OBP40 Page 7", "capabilities": { @@ -3883,7 +3883,7 @@ "description": "Wind source for page 8: [true|apparent]", "list": [ "True wind", - "apparent wind" + "Apparent wind" ], "category": "OBP40 Page 8", "capabilities": { @@ -4142,7 +4142,7 @@ "description": "Wind source for page 9: [true|apparent]", "list": [ "True wind", - "apparent wind" + "Apparent wind" ], "category": "OBP40 Page 9", "capabilities": { @@ -4392,7 +4392,7 @@ "description": "Wind source for page 10: [true|apparent]", "list": [ "True wind", - "apparent wind" + "Apparent wind" ], "category": "OBP40 Page 10", "capabilities": { From 0363ba4379d4eb2d11acca34b076fc9555f49911 Mon Sep 17 00:00:00 2001 From: Thomas Hooge Date: Sun, 15 Feb 2026 13:13:19 +0100 Subject: [PATCH 121/121] Add feature to optionally apply patches to gateway code --- lib/obp60task/extra_task.py | 30 ++++++++++++++++++++++++++++-- lib/obp60task/platformio.ini | 2 ++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/lib/obp60task/extra_task.py b/lib/obp60task/extra_task.py index 28e9980..66ee3b1 100644 --- a/lib/obp60task/extra_task.py +++ b/lib/obp60task/extra_task.py @@ -1,12 +1,21 @@ # PlatformIO extra script for obp60task +import subprocess + +patching = False + epdtype = "unknown" pcbvers = "unknown" for x in env["BUILD_FLAGS"]: - if x.startswith("-D HARDWARE_"): + if not x.startswith('-D'): + continue + opt = x[2:].strip() + if opt.startswith("HARDWARE_"): pcbvers = x.split('_')[1] - if x.startswith("-D DISPLAY_"): + elif opt.startswith("DISPLAY_"): epdtype = x.split('_')[1] + elif opt == 'ENABLE_PATCHES': + patching = True propfilename = os.path.join(env["PROJECT_LIBDEPS_DIR"], env["PIOENV"], "GxEPD2/library.properties") properties = {} @@ -28,3 +37,20 @@ except: env["CPPDEFINES"].extend([("BOARD", env["BOARD"]), ("EPDTYPE", epdtype), ("PCBVERS", pcbvers), ("GXEPD2VERS", gxepd2vers)]) print("added hardware info to CPPDEFINES") + +if patching: + # apply patches to gateway code + print("applying gateway patches") + patchdir = os.path.join(os.path.dirname(script), "patches") + if not os.path.isdir(patchdir): + print("patchdir not found, no patches applied") + else: + patchfiles = [f for f in os.listdir(patchdir)] + for p in patchfiles: + patch = os.path.join(patchdir, p) + print(f"applying {patch}") + res = subprocess.run(["git", "apply", patch], capture_output=True, text=True) + if res.returncode != 0: + print(res.stderr) + else: + print("no patches found") diff --git a/lib/obp60task/platformio.ini b/lib/obp60task/platformio.ini index 63468c3..03a5463 100644 --- a/lib/obp60task/platformio.ini +++ b/lib/obp60task/platformio.ini @@ -58,6 +58,7 @@ build_flags= # -D DISPLAY_GYE042A87 #alternativ E-Ink display from Genyo Optical, R10 2.2 ohm - medium # -D DISPLAY_SE0420NQ04 #alternativ E-Ink display from SID Technology, R10 2.2 ohm - bad (burn in effects) # -D DISPLAY_ZJY400300-042CAAMFGN #alternativ E-Ink display from ZZE Technology, R10 2.2 ohm - very good +# -D ENABLE_PATCHES #enable patching of gateway code ${env.build_flags} #CONFIG_ESP_TASK_WDT_TIMEOUT_S = 10 #Task Watchdog timeout period (seconds) [1...60] 5 default upload_port = /dev/ttyACM0 #OBP60 download via USB-C direct @@ -108,6 +109,7 @@ build_flags= #-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 ENABLE_PATCHES #enable patching of gateway code ${env.build_flags} upload_port = /dev/ttyUSB0 #OBP40 download via external USB/Serail converter upload_protocol = esptool #firmware upload via USB OTG seriell, by first upload need to set the ESP32-S3 in the upload mode with shortcut GND to Pin27