From 25422b77d68bb90b890285ac968cd9eb594b7177 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 20 Feb 2026 14:17:57 +0100 Subject: [PATCH 01/75] Fix NetworkCleint.cpp in case of bad connections --- lib/obp60task/NetworkClient.cpp | 153 +++++++++++++++++++++++++++++--- lib/obp60task/NetworkClient.h | 2 +- platformio.ini | 12 ++- 3 files changed, 152 insertions(+), 15 deletions(-) diff --git a/lib/obp60task/NetworkClient.cpp b/lib/obp60task/NetworkClient.cpp index c6a7962..4d091f5 100644 --- a/lib/obp60task/NetworkClient.cpp +++ b/lib/obp60task/NetworkClient.cpp @@ -51,14 +51,16 @@ 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; // Read limit for data (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 not with WiFi connectetd then return without any activities if (!gwWifi.clientConnected()) { if (DEBUGING) {Serial.println("No WiFi connection");} return false; } + // If frame buffer not correct allocated then return without any activities if (!buffer) { if (DEBUGING) {Serial.println("Malloc failed buffer");} return false; @@ -71,20 +73,51 @@ bool NetworkClient::httpGetGzip(const String& url, uint8_t*& outData, size_t& ou http.setTimeout(TCPREADTIMEOUT); // Read timeout in ms (can be adjusted in NetworkClient.h) http.begin(url); + + // NEW: force server to close the connection after the response (prevents "stuck" keep-alive reads) + http.addHeader("Connection", "close"); + + // NEW: request gzip, but we will only decompress if the server actually answers with gzip http.addHeader("Accept-Encoding", "gzip"); + // NEW: register headers BEFORE GET() (more reliable with Arduino HTTPClient) + if (DEBUGING) { + // We need follow key words + const char* keys[] = { + "Content-Encoding", + "Transfer-Encoding", + "Content-Length" + }; + // Read header + http.collectHeaders(keys, 3); + } + int code = http.GET(); if (code != HTTP_CODE_OK) { - Serial.printf("HTTP ERROR: %d\n", code); + Serial.printf("HTTP Client ERROR: %d (%s)\n", code, http.errorToString(code).c_str()); // Hard reset HTTP + socket WiFiClient* tmp = http.getStreamPtr(); if (tmp) tmp->stop(); // Force close TCP socket + http.end(); - free(buffer); return false; } + else{ + + if (DEBUGING) { + String ce = http.header("Content-Encoding"); + String te = http.header("Transfer-Encoding"); + String cl = http.header("Content-Length"); + + // Print header informations + Serial.printf("Content-Encoding=%s Transfer-Encoding=%s Content-Length=%s\n", + ce.c_str(), + te.c_str(), + cl.c_str()); + } + } WiFiClient* stream = http.getStreamPtr(); @@ -93,25 +126,120 @@ bool NetworkClient::httpGetGzip(const String& url, uint8_t*& outData, size_t& ou const uint32_t READ_TIMEOUT = READDATATIMEOUT; // Timeout for reading data (can be adjusted in NetworkClient.h) bool complete = false; + bool aborting = false; // NEW: remember if we must force-close socket - while (http.connected() && !complete) { + // NEW: detect if server really sent gzip + String ce = http.header("Content-Encoding"); + bool isGzip = ce.equalsIgnoreCase("gzip"); - size_t avail = stream->available(); + // NEW: if not gzip, we will not try to decompress (prevents false "Decompress OK" / random success) + // You can either handle plain JSON here or just fail-fast. + if (!isGzip) { + if (DEBUGING) { + Serial.println("Server response is NOT gzip (Content-Encoding != gzip)."); + Serial.println("Either disable Accept-Encoding: gzip or add plain-body handling here."); + } + + // --- Plain-body handling (recommended): read full body into outData as-is --- + // NEW: try to read Content-Length bytes if available (more robust) + int total = http.getSize(); // returns Content-Length, or -1 if unknown/chunked + if (total > 0 && (size_t)total > capacity) { + Serial.println("Plain response exceeds READLIMIT."); + aborting = true; + } else { + // Read until we have all bytes (Content-Length) or until connection closes + buffer drains + while ((http.connected() || (stream && stream->available())) && !aborting) { + size_t avail = stream ? stream->available() : 0; + if (avail == 0) { + if (millis() - lastData > READ_TIMEOUT) { + Serial.println("TIMEOUT waiting for data (plain)!"); + aborting = true; + break; + } + delay(1); + continue; + } + + if (len >= capacity) { + Serial.println("READLIMIT reached, aborting (plain)."); + aborting = true; + break; + } + + if (len + avail > capacity) + avail = capacity - len; + + int read = stream->readBytes(buffer + len, avail); + if (read > 0) { + len += (size_t)read; + lastData = millis(); + } + + if (total > 0 && (int)len >= total) { + break; // we got full body + } + } + } + + if (aborting) { + // --- Added: Force-close connection only if aborted to avoid TCP RST storms --- + if (stream) stream->stop(); // Force close TCP socket + http.end(); + free(buffer); + return false; + } + + // Return plain body to caller + outData = (uint8_t*)malloc(len); + if (!outData) { + Serial.println("Malloc failed outData (plain)."); + // --- Added: Force-close connection only if aborted to avoid TCP RST storms --- + if (stream) stream->stop(); // Force close TCP socket + http.end(); + free(buffer); + return false; + } + memcpy(outData, buffer, len); + outLen = len; + + http.end(); + free(buffer); + return true; + } + + // --- GZIP path (only if Content-Encoding is gzip) --- + while ((http.connected() || (stream && stream->available())) && !complete) { + + size_t avail = stream ? stream->available() : 0; if (avail == 0) { if (millis() - lastData > READ_TIMEOUT) { Serial.println("TIMEOUT waiting for data!"); + aborting = true; // NEW: mark abnormal exit break; } delay(1); continue; } + // NEW: safety check if buffer limit is reached + if (len >= capacity) { + Serial.println("READLIMIT reached, aborting."); + aborting = true; + break; + } + if (len + avail > capacity) avail = capacity - len; int read = stream->readBytes(buffer + len, avail); - len += read; + if (read <= 0) { + // NEW: avoid tight loop if read returns zero + delay(1); + continue; + } + + len += (size_t)read; lastData = millis(); if (DEBUGING) {Serial.printf("Read chunk: %d (total: %d)\n", read, (int)len);} @@ -124,7 +252,12 @@ bool NetworkClient::httpGetGzip(const String& url, uint8_t*& outData, size_t& ou unsigned long testLen = len * 8; // Dynamic expansion uint8_t* test = (uint8_t*)malloc(testLen); - if (!test) continue; + if (!test) { + // NEW: abort if allocation fails to prevent endless retry loop + Serial.println("Malloc failed test buffer, aborting."); + aborting = true; + break; + } unsigned long srcLen = len - headerOffset; @@ -132,7 +265,7 @@ bool NetworkClient::httpGetGzip(const String& url, uint8_t*& outData, size_t& ou if (res == 0) { if (DEBUGING) {Serial.printf("Decompress OK! Size: %lu bytes\n", testLen);} outData = test; - outLen = testLen; + outLen = (size_t)testLen; complete = true; break; } @@ -140,8 +273,8 @@ 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(); + // --- Added: Force-close connection only if aborted to avoid TCP RST storms --- + if (aborting && stream) stream->stop(); // NEW: stop() only on abnormal termination http.end(); free(buffer); diff --git a/lib/obp60task/NetworkClient.h b/lib/obp60task/NetworkClient.h index 03e7f83..5d3f448 100644 --- a/lib/obp60task/NetworkClient.h +++ b/lib/obp60task/NetworkClient.h @@ -3,7 +3,7 @@ #include #include -#define DEBUGING false // Debug flag for NetworkClient for more live information +#define DEBUGING true // 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 diff --git a/platformio.ini b/platformio.ini index 341083c..c32a227 100644 --- a/platformio.ini +++ b/platformio.ini @@ -21,8 +21,10 @@ lib_deps = ttlappalainen_NMEA2000=https://github.com/wellenvogel/NMEA2000.git#20251126 ttlappalainen/NMEA0183 @ 1.10.1 ArduinoJson @ 6.18.5 - AsyncTCP-esphome @ 2.0.1 - ottowinter/ESPAsyncWebServer-esphome@2.0.1 +; AsyncTCP-esphome @ 2.0.1 +; ottowinter/ESPAsyncWebServer-esphome@2.0.1 + AsyncTCP-esphome @ 2.1.1 + ottowinter/ESPAsyncWebServer-esphome@3.4.0 FS Preferences ESPmDNS @@ -34,8 +36,10 @@ lib_deps= ttlappalainen_NMEA2000=symlink://../NMEA2000 ttlappalainen/NMEA0183 @ 1.10.1 ArduinoJson @ 6.18.5 - AsyncTCP-esphome @ 2.0.1 - ottowinter/ESPAsyncWebServer-esphome@2.0.1 +; AsyncTCP-esphome @ 2.0.1 +; ottowinter/ESPAsyncWebServer-esphome@2.0.1 + AsyncTCP-esphome @ 2.1.1 + ottowinter/ESPAsyncWebServer-esphome@3.4.0 FS Preferences ESPmDNS From c647c2fe44a3925b429381e9a01b6fcd342b9be8 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 20 Feb 2026 14:54:48 +0100 Subject: [PATCH 02/75] Only decompress zip after a succsessfully contenet download --- lib/obp60task/NetworkClient.cpp | 153 +++++++++++++++++++------------- 1 file changed, 93 insertions(+), 60 deletions(-) diff --git a/lib/obp60task/NetworkClient.cpp b/lib/obp60task/NetworkClient.cpp index 4d091f5..53ac1c6 100644 --- a/lib/obp60task/NetworkClient.cpp +++ b/lib/obp60task/NetworkClient.cpp @@ -104,8 +104,7 @@ bool NetworkClient::httpGetGzip(const String& url, uint8_t*& outData, size_t& ou free(buffer); return false; } - else{ - + else{ if (DEBUGING) { String ce = http.header("Content-Encoding"); String te = http.header("Transfer-Encoding"); @@ -132,9 +131,18 @@ bool NetworkClient::httpGetGzip(const String& url, uint8_t*& outData, size_t& ou String ce = http.header("Content-Encoding"); bool isGzip = ce.equalsIgnoreCase("gzip"); + // NEW: read expected body size if provided by server (prevents waiting forever for missing bytes) + int total = http.getSize(); // returns Content-Length, or -1 if unknown/chunked + + // NEW: fail fast if server claims something larger than our buffer + if (total > 0 && (size_t)total > capacity) { + Serial.println("Response exceeds READLIMIT."); + aborting = true; + } + // NEW: if not gzip, we will not try to decompress (prevents false "Decompress OK" / random success) // You can either handle plain JSON here or just fail-fast. - if (!isGzip) { + if (!isGzip && !aborting) { if (DEBUGING) { Serial.println("Server response is NOT gzip (Content-Encoding != gzip)."); Serial.println("Either disable Accept-Encoding: gzip or add plain-body handling here."); @@ -142,7 +150,6 @@ bool NetworkClient::httpGetGzip(const String& url, uint8_t*& outData, size_t& ou // --- Plain-body handling (recommended): read full body into outData as-is --- // NEW: try to read Content-Length bytes if available (more robust) - int total = http.getSize(); // returns Content-Length, or -1 if unknown/chunked if (total > 0 && (size_t)total > capacity) { Serial.println("Plain response exceeds READLIMIT."); aborting = true; @@ -175,6 +182,7 @@ bool NetworkClient::httpGetGzip(const String& url, uint8_t*& outData, size_t& ou lastData = millis(); } + // NEW: stop reading as soon as we have the full response if (total > 0 && (int)len >= total) { break; // we got full body } @@ -208,69 +216,94 @@ bool NetworkClient::httpGetGzip(const String& url, uint8_t*& outData, size_t& ou } // --- GZIP path (only if Content-Encoding is gzip) --- - while ((http.connected() || (stream && stream->available())) && !complete) { + if (!aborting) { - size_t avail = stream ? stream->available() : 0; + // NEW: read exactly Content-Length bytes when available (prevents partial-body timeout loops) + while ((http.connected() || (stream && stream->available())) && !complete && !aborting) { - if (avail == 0) { - if (millis() - lastData > READ_TIMEOUT) { - Serial.println("TIMEOUT waiting for data!"); - aborting = true; // NEW: mark abnormal exit + size_t avail = stream ? stream->available() : 0; + + if (avail == 0) { + // NEW: if Content-Length is known and we already read it all, stop immediately + if (total > 0 && (int)len >= total) { + break; + } + + if (millis() - lastData > READ_TIMEOUT) { + if (DEBUGING) {Serial.println("TIMEOUT waiting for data!");} + aborting = true; // NEW: mark abnormal exit + break; + } + delay(1); + continue; + } + + // NEW: safety check if buffer limit is reached + if (len >= capacity) { + Serial.println("READLIMIT reached, aborting."); + aborting = true; + break; + } + + // NEW: if Content-Length is known, do not read beyond it + if (total > 0) { + size_t remaining = (size_t)total - len; + if (avail > remaining) avail = remaining; + } + + if (len + avail > capacity) + avail = capacity - len; + + int read = stream->readBytes(buffer + len, avail); + if (read <= 0) { + // NEW: avoid tight loop if read returns zero + delay(1); + continue; + } + + len += (size_t)read; + lastData = millis(); + + if (DEBUGING) {Serial.printf("Read chunk: %d (total: %d)\n", read, (int)len);} + + // NEW: if Content-Length is known and fully received, we can stop reading + if (total > 0 && (int)len >= total) { break; } - delay(1); - continue; } - // NEW: safety check if buffer limit is reached - if (len >= capacity) { - Serial.println("READLIMIT reached, aborting."); - aborting = true; - break; + // NEW: only attempt gzip parse/decompress after we have a complete body (when Content-Length is known) + // This avoids wasting heap with repeated malloc/free and reduces fragmentation over long runtimes. + if (!aborting) { + if (len < 20) { + aborting = true; + } else { + int headerOffset = skipGzipHeader(buffer, len); + if (headerOffset < 0) { + aborting = true; + } else { + unsigned long testLen = len * 8; // Dynamic expansion + uint8_t* test = (uint8_t*)malloc(testLen); + + if (!test) { + Serial.println("Malloc failed test buffer, aborting."); + aborting = true; + } else { + unsigned long srcLen = (unsigned long)(len - (size_t)headerOffset); + int res = puff(test, &testLen, buffer + headerOffset, &srcLen); + + if (res == 0) { + if (DEBUGING) {Serial.printf("Decompress OK! Size: %lu bytes\n", testLen);} + outData = test; + outLen = (size_t)testLen; + complete = true; + } else { + free(test); + } + } + } + } } - - if (len + avail > capacity) - avail = capacity - len; - - int read = stream->readBytes(buffer + len, avail); - if (read <= 0) { - // NEW: avoid tight loop if read returns zero - delay(1); - continue; - } - - len += (size_t)read; - lastData = millis(); - - if (DEBUGING) {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) { - // NEW: abort if allocation fails to prevent endless retry loop - Serial.println("Malloc failed test buffer, aborting."); - aborting = true; - break; - } - - unsigned long srcLen = len - headerOffset; - - int res = puff(test, &testLen, buffer + headerOffset, &srcLen); - if (res == 0) { - if (DEBUGING) {Serial.printf("Decompress OK! Size: %lu bytes\n", testLen);} - outData = test; - outLen = (size_t)testLen; - complete = true; - break; - } - - free(test); } // --- Added: Force-close connection only if aborted to avoid TCP RST storms --- From 2de3a1b084c90e4dab15903a3d762f60eeb459c0 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 20 Feb 2026 17:21:26 +0100 Subject: [PATCH 03/75] Add backoff in PageNavigation for better handling with bad connection --- lib/obp60task/NetworkClient.cpp | 2 +- lib/obp60task/PageNavigation.cpp | 33 ++++++++++++++++++++++++++++---- platformio.ini | 12 ++++-------- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/lib/obp60task/NetworkClient.cpp b/lib/obp60task/NetworkClient.cpp index 53ac1c6..b8ebaee 100644 --- a/lib/obp60task/NetworkClient.cpp +++ b/lib/obp60task/NetworkClient.cpp @@ -230,7 +230,7 @@ bool NetworkClient::httpGetGzip(const String& url, uint8_t*& outData, size_t& ou } if (millis() - lastData > READ_TIMEOUT) { - if (DEBUGING) {Serial.println("TIMEOUT waiting for data!");} + Serial.println("TIMEOUT waiting for data!"); aborting = true; // NEW: mark abnormal exit break; } diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp index bd4adf1..5652cad 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -374,8 +374,21 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update getdisplay().setTextColor(commonData->fgcolor); + // NEW: simple exponential backoff for 1 Hz polling (prevents connection-refused storms) + static uint32_t nextAllowedMs = 0; + static uint8_t failCount = 0; + + uint32_t now = millis(); + + // NEW: if we are in backoff window, skip network call and use backup immediately + bool allowFetch = ((int32_t)(now - nextAllowedMs) >= 0); + // If a network connection to URL then load the navigation map - if (net.fetchAndDecompressJson(url)) { + if (allowFetch && net.fetchAndDecompressJson(url)) { + + // NEW: reset backoff on success + failCount = 0; + nextAllowedMs = now + 1000; // keep 1 Hz on success auto& json = net.json(); // Extract JSON content int numPix = json["number_pixels"] | 0; // Read number of pixels @@ -397,7 +410,7 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map 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) { - LOG_DEBUG(GwLog::ERROR,"Error PageNavigation: PPSRAM alloc image buffer failed"); + LOG_DEBUG(GwLog::ERROR,"Error PageNavigation: PSRAM alloc image buffer failed"); free(b64); return PAGE_UPDATE; } @@ -426,12 +439,25 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map } // If no network connection then use backup navigation map else{ + + // NEW: update backoff only if we actually attempted a fetch (not when skipping due to backoff) + if (allowFetch) { + // NEW: exponential backoff: 1s,2s,4s,8s,16s,30s (capped) + if (failCount < 6) failCount++; + uint32_t backoffMs = 1000u << failCount; + if (backoffMs > 30000u) backoffMs = 30000u; + nextAllowedMs = now + backoffMs; + } else { + // NEW: we are currently backing off; do not increase failCount further + // nextAllowedMs stays unchanged + } + // Show backup image (backup navigation map) if (hasImageBackup) { getdisplay().drawBitmap(0, 25, imageBackupData, imageBackupWidth, imageBackupHeight, commonData->fgcolor); } - // Show info: Connection lost when 5 page refreshes has a connection lost to the map server + // Show connection lost info when 5 page refreshes has a connection lost to the map server // Short connection losts are uncritical if(lostCounter >= 5){ getdisplay().setFont(&Ubuntu_Bold12pt8b); @@ -444,7 +470,6 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map lostCounter++; // Increment lost counter } - // ############### Draw Values ################ getdisplay().setFont(&Ubuntu_Bold12pt8b); diff --git a/platformio.ini b/platformio.ini index c32a227..341083c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -21,10 +21,8 @@ lib_deps = ttlappalainen_NMEA2000=https://github.com/wellenvogel/NMEA2000.git#20251126 ttlappalainen/NMEA0183 @ 1.10.1 ArduinoJson @ 6.18.5 -; AsyncTCP-esphome @ 2.0.1 -; ottowinter/ESPAsyncWebServer-esphome@2.0.1 - AsyncTCP-esphome @ 2.1.1 - ottowinter/ESPAsyncWebServer-esphome@3.4.0 + AsyncTCP-esphome @ 2.0.1 + ottowinter/ESPAsyncWebServer-esphome@2.0.1 FS Preferences ESPmDNS @@ -36,10 +34,8 @@ lib_deps= ttlappalainen_NMEA2000=symlink://../NMEA2000 ttlappalainen/NMEA0183 @ 1.10.1 ArduinoJson @ 6.18.5 -; AsyncTCP-esphome @ 2.0.1 -; ottowinter/ESPAsyncWebServer-esphome@2.0.1 - AsyncTCP-esphome @ 2.1.1 - ottowinter/ESPAsyncWebServer-esphome@3.4.0 + AsyncTCP-esphome @ 2.0.1 + ottowinter/ESPAsyncWebServer-esphome@2.0.1 FS Preferences ESPmDNS From bcb618fc7b46594dc843feedd4165c58708796e7 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 20 Feb 2026 17:42:00 +0100 Subject: [PATCH 04/75] Change AsyncTCP and AsyncWebServer to newer versions --- platformio.ini | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/platformio.ini b/platformio.ini index 341083c..2ecdd94 100644 --- a/platformio.ini +++ b/platformio.ini @@ -21,8 +21,8 @@ lib_deps = ttlappalainen_NMEA2000=https://github.com/wellenvogel/NMEA2000.git#20251126 ttlappalainen/NMEA0183 @ 1.10.1 ArduinoJson @ 6.18.5 - AsyncTCP-esphome @ 2.0.1 - ottowinter/ESPAsyncWebServer-esphome@2.0.1 + AsyncTCP-esphome @ 2.1.1 + ottowinter/ESPAsyncWebServer-esphome@3.4.0 FS Preferences ESPmDNS @@ -34,8 +34,8 @@ lib_deps= ttlappalainen_NMEA2000=symlink://../NMEA2000 ttlappalainen/NMEA0183 @ 1.10.1 ArduinoJson @ 6.18.5 - AsyncTCP-esphome @ 2.0.1 - ottowinter/ESPAsyncWebServer-esphome@2.0.1 + AsyncTCP-esphome @ 2.1.1 + ottowinter/ESPAsyncWebServer-esphome@3.4.0 FS Preferences ESPmDNS From 6a0811f42f50f3e2c95256139fe597b52067144f Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 20 Feb 2026 17:56:30 +0100 Subject: [PATCH 05/75] Add diff comments to original version --- lib/obp60task/Changes_to_original.txt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 lib/obp60task/Changes_to_original.txt diff --git a/lib/obp60task/Changes_to_original.txt b/lib/obp60task/Changes_to_original.txt new file mode 100644 index 0000000..9f656bd --- /dev/null +++ b/lib/obp60task/Changes_to_original.txt @@ -0,0 +1,6 @@ +Changes to original project (ellenvogel) + +* esp32-nmea2000-obp60/gwwifi/GwWifi.CPPDEFINES + - any fixes for reconnect handling +* esp32-nmea2000-obp60/platformio.ini + - change to newer versions for AsyncTCP and AsyncWebServer (better handling for bad WiFi connections) \ No newline at end of file From 94f29eed068a527c801db2c970edc67ef3e2ba2d Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 20 Feb 2026 17:58:14 +0100 Subject: [PATCH 06/75] Typo --- lib/obp60task/Changes_to_original.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/obp60task/Changes_to_original.txt b/lib/obp60task/Changes_to_original.txt index 9f656bd..4cb1112 100644 --- a/lib/obp60task/Changes_to_original.txt +++ b/lib/obp60task/Changes_to_original.txt @@ -1,4 +1,4 @@ -Changes to original project (ellenvogel) +Changes to original project (wellenvogel) * esp32-nmea2000-obp60/gwwifi/GwWifi.CPPDEFINES - any fixes for reconnect handling From 501b9f7023ef372a52c6259438fd368e2a7edb5e Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 20 Feb 2026 18:09:12 +0100 Subject: [PATCH 07/75] Add more infos --- lib/obp60task/Changes_to_original.txt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/obp60task/Changes_to_original.txt b/lib/obp60task/Changes_to_original.txt index 4cb1112..052d644 100644 --- a/lib/obp60task/Changes_to_original.txt +++ b/lib/obp60task/Changes_to_original.txt @@ -3,4 +3,10 @@ Changes to original project (wellenvogel) * esp32-nmea2000-obp60/gwwifi/GwWifi.CPPDEFINES - any fixes for reconnect handling * esp32-nmea2000-obp60/platformio.ini - - change to newer versions for AsyncTCP and AsyncWebServer (better handling for bad WiFi connections) \ No newline at end of file + - change to newer versions for AsyncTCP and AsyncWebServer (better handling for bad WiFi connections) + + From AsyncTCP-esphome @ 2.0.1 + ottowinter/ESPAsyncWebServer-esphome@2.0.1 + + To AsyncTCP-esphome @ 2.1.1 + ottowinter/ESPAsyncWebServer-esphome@3.4.0 \ No newline at end of file From a6fd3ef599a2859594864823e158474b0815a346 Mon Sep 17 00:00:00 2001 From: Thomas Hooge Date: Mon, 23 Feb 2026 15:34:10 +0100 Subject: [PATCH 08/75] System page with N2K device list by improved patching feature and patch --- lib/obp60task/PageSystem.cpp | 102 ++++++++++++++++++++--- lib/obp60task/extra_task.py | 22 +++-- lib/obp60task/patches/01-nmea2000.patch | 103 ++++++++++++++++++++++++ 3 files changed, 210 insertions(+), 17 deletions(-) create mode 100644 lib/obp60task/patches/01-nmea2000.patch diff --git a/lib/obp60task/PageSystem.cpp b/lib/obp60task/PageSystem.cpp index 92af7b2..a8b3ee4 100644 --- a/lib/obp60task/PageSystem.cpp +++ b/lib/obp60task/PageSystem.cpp @@ -15,6 +15,7 @@ #include "images/logo64.xbm" #include #include "qrcode.h" +#include #ifdef BOARD_OBP40S3 #include "dirent.h" @@ -40,6 +41,7 @@ private: String rtc_module; String gps_module; String env_module; + String flashLED; String batt_sensor; String solar_sensor; @@ -50,6 +52,17 @@ private: char mode = 'N'; // (N)ormal, (S)ettings, (D)evice list, (C)ard +#ifdef PATCH_N2K + struct device { + uint64_t NAME; + uint8_t id; + char hex_name[17]; + uint16_t manuf_code; + const char *model; + }; + std::vector devicelist; +#endif + public: PageSystem(CommonData &common){ commonData = &common; @@ -76,6 +89,7 @@ public: rot_sensor = common.config->getString(common.config->useRotSensor); homelat = common.config->getString(common.config->homeLAT).toDouble(); homelon = common.config->getString(common.config->homeLON).toDouble(); + flashLED = common.config->getString(common.config->flashLED); } void setupKeys() { @@ -168,19 +182,43 @@ public: } } + void displayNew(PageData &pageData) { +#ifdef BOARD_OBP60S3 + // Clear optical warning + if (flashLED == "Limit Violation") { + setBlinkingLED(false); + setFlashLED(false); + } +#endif + +#ifdef PATCH_N2K + // load current device list + tN2kDeviceList *pDevList = pageData.api->getN2kDeviceList(); + // TODO check if changed + if (pDevList->ReadResetIsListUpdated()) { + // only reload if changed + devicelist.clear(); + for (uint8_t i = 0; i <= 252; i++) { + const tNMEA2000::tDevice *d = pDevList->FindDeviceBySource(i); + if (d == nullptr) { + continue; + } + device dev; + dev.id = i; + dev.NAME = d->GetName(); + snprintf(dev.hex_name, sizeof(dev.hex_name), "%08X%08X", (uint32_t)(dev.NAME >> 32), (uint32_t)(dev.NAME & 0xFFFFFFFF)); + dev.manuf_code = d->GetManufacturerCode(); + dev.model = d->GetModelID(); + devicelist.push_back(dev); + } + } +#endif + }; + int displayPage(PageData &pageData){ GwConfigHandler *config = commonData->config; GwLog *logger = commonData->logger; - // Get config data - String flashLED = config->getString(config->flashLED); - - // Optical warning by limit violation (unused) - if(String(flashLED) == "Limit Violation"){ - setBlinkingLED(false); - setFlashLED(false); - } - // Logging boat values logger->logDebug(GwLog::LOG, "Drawing at PageSystem, Mode=%c", mode); @@ -461,12 +499,54 @@ public: getdisplay().print("NMEA2000 device list"); getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(20, 80); + getdisplay().setCursor(20, 70); getdisplay().print("RxD: "); getdisplay().print(String(commonData->status.n2kRx)); - getdisplay().setCursor(20, 100); + getdisplay().setCursor(120, 70); getdisplay().print("TxD: "); getdisplay().print(String(commonData->status.n2kTx)); + +#ifdef PATCH_N2K + x0 = 20; + y0 = 100; + + getdisplay().setFont(&Ubuntu_Bold10pt8b); + getdisplay().setCursor(x0, y0); + getdisplay().print("ID"); + getdisplay().setCursor(x0 + 50, y0); + getdisplay().print("Model"); + getdisplay().setCursor(x0 + 250, y0); + getdisplay().print("Manuf."); + getdisplay().drawLine(18, y0 + 4, 360 , y0 + 4 , commonData->fgcolor); + + getdisplay().setFont(&Ubuntu_Bold8pt8b); + y0 = 120; + uint8_t n_dev = 0; + for (const device& item : devicelist) { + if (n_dev > 8) { + break; + } + getdisplay().setCursor(x0, y0 + n_dev * 20); + getdisplay().print(item.id); + getdisplay().setCursor(x0 + 50, y0 + n_dev * 20); + getdisplay().print(item.model); + getdisplay().setCursor(x0 + 250, y0 + n_dev * 20); + getdisplay().print(item.manuf_code); + n_dev++; + } + getdisplay().setCursor(x0, y0 + (n_dev + 1) * 20); + if (n_dev == 0) { + getdisplay().printf("no devices found on bus"); + + } else { + getdisplay().drawLine(18, y0 + n_dev * 20, 360 , y0 + n_dev * 20, commonData->fgcolor); + getdisplay().printf("%d devices of %d in total", n_dev, devicelist.size()); + } +#else + getdisplay().setCursor(20, 100); + getdisplay().print("NMEA2000 not exposed to obp60 task"); +#endif + } // Update display diff --git a/lib/obp60task/extra_task.py b/lib/obp60task/extra_task.py index 66ee3b1..dfc8257 100644 --- a/lib/obp60task/extra_task.py +++ b/lib/obp60task/extra_task.py @@ -2,6 +2,14 @@ import subprocess +def cleanup_patches(source, target, env): + for p in patchfiles: + patch = os.path.join(patchdir, p) + print(f"removing {patch}") + res = subprocess.run(["git", "apply", "-R", patch], capture_output=True, text=True) + if res.returncode != 0: + print(res.stderr) + patching = False epdtype = "unknown" @@ -46,11 +54,13 @@ if patching: 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) + if len(patchfiles) > 0: + 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) + env.AddPostAction("$PROGPATH", cleanup_patches) else: print("no patches found") diff --git a/lib/obp60task/patches/01-nmea2000.patch b/lib/obp60task/patches/01-nmea2000.patch new file mode 100644 index 0000000..3d50bea --- /dev/null +++ b/lib/obp60task/patches/01-nmea2000.patch @@ -0,0 +1,103 @@ +diff --git a/lib/api/GwApi.h b/lib/api/GwApi.h +index 88f9690..9663a65 100644 +--- a/lib/api/GwApi.h ++++ b/lib/api/GwApi.h +@@ -2,6 +2,8 @@ + #define _GWAPI_H + #include "GwMessage.h" + #include "N2kMsg.h" ++#include "Nmea2kTwai.h" ++#include "N2kDeviceList.h" + #include "NMEA0183Msg.h" + #include "GWConfig.h" + #include "GwBoatData.h" +@@ -222,6 +224,8 @@ class GwApi{ + * accessing boat data must only be executed from within the main thread + * you need to use the request pattern as shown in GwExampleTask.cpp + */ ++ virtual Nmea2kTwai *getNMEA2000()=0; ++ virtual tN2kDeviceList *getN2kDeviceList()=0; + virtual GwBoatData *getBoatData()=0; + virtual ~GwApi(){} + }; +diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h +index 604c356..2fe4496 100644 +--- a/lib/obp60task/OBP60Extensions.h ++++ b/lib/obp60task/OBP60Extensions.h +@@ -15,6 +15,9 @@ + #define MOUNT_POINT "/sdcard" + #endif + ++// Patches to apply to gateway code ++#define PATCH_N2K ++ + // FRAM address reservations 32kB: 0x0000 - 0x7FFF + // 0x0000 - 0x03ff: single variables + #define FRAM_PAGE_NO 0x0002 +diff --git a/lib/usercode/GwUserCode.cpp b/lib/usercode/GwUserCode.cpp +index 1b007f8..90087d4 100644 +--- a/lib/usercode/GwUserCode.cpp ++++ b/lib/usercode/GwUserCode.cpp +@@ -216,6 +216,14 @@ public: + { + return api->getLogger(); + } ++ virtual Nmea2kTwai *getNMEA2000() ++ { ++ return api->getNMEA2000(); ++ } ++ virtual tN2kDeviceList *getN2kDeviceList() ++ { ++ return api->getN2kDeviceList(); ++ } + virtual GwBoatData *getBoatData() + { + return api->getBoatData(); +@@ -428,4 +436,4 @@ void GwUserCode::handleWebRequest(const String &url,AsyncWebServerRequest *req){ + } + LOG_DEBUG(GwLog::DEBUG,"no task found for web request %s[%s]",url.c_str(),tname.c_str()); + req->send(404, "text/plain", "not found"); +-} +\ No newline at end of file ++} +diff --git a/src/main.cpp b/src/main.cpp +index 44c715f..fdb0366 100644 +--- a/src/main.cpp ++++ b/src/main.cpp +@@ -100,6 +100,7 @@ GwLog logger(LOGLEVEL,NULL); + GwConfigHandler config(&logger); + + #include "Nmea2kTwai.h" ++#include + static const unsigned long CAN_RECOVERY_PERIOD=3000; //ms + static const unsigned long NMEA2000_HEARTBEAT_INTERVAL=5000; + class Nmea2kTwaiLog : public Nmea2kTwai{ +@@ -126,6 +127,7 @@ class Nmea2kTwaiLog : public Nmea2kTwai{ + #endif + + Nmea2kTwai &NMEA2000=*(new Nmea2kTwaiLog((gpio_num_t)ESP32_CAN_TX_PIN,(gpio_num_t)ESP32_CAN_RX_PIN,CAN_RECOVERY_PERIOD,&logger)); ++tN2kDeviceList *pN2kDeviceList; + + #ifdef GWBUTTON_PIN + bool fixedApPass=false; +@@ -333,6 +335,12 @@ public: + status.n2kTx=countNMEA2KOut.getGlobal(); + channels.fillStatus(status); + } ++ virtual Nmea2kTwai *getNMEA2000(){ ++ return &NMEA2000; ++ } ++ virtual tN2kDeviceList *getN2kDeviceList(){ ++ return pN2kDeviceList; ++ } + virtual GwBoatData *getBoatData(){ + return &boatData; + } +@@ -935,6 +943,7 @@ void setup() { + NMEA2000.SetMsgHandler([](const tN2kMsg &n2kMsg){ + handleN2kMessage(n2kMsg,N2K_CHANNEL_ID); + }); ++ pN2kDeviceList = new tN2kDeviceList(&NMEA2000); + NMEA2000.Open(); + logger.logDebug(GwLog::LOG,"starting addon tasks"); + logger.flush(); From 97fcebdcb7e50264a4ceb4df4e92521edf980d8e Mon Sep 17 00:00:00 2001 From: Thomas Hooge Date: Mon, 23 Feb 2026 18:56:20 +0100 Subject: [PATCH 09/75] Added format "formatXdr:A:rd" to formatter (see #159) --- lib/obp60task/OBP60Formatter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/obp60task/OBP60Formatter.cpp b/lib/obp60task/OBP60Formatter.cpp index 9b42102..ad40372 100644 --- a/lib/obp60task/OBP60Formatter.cpp +++ b/lib/obp60task/OBP60Formatter.cpp @@ -835,7 +835,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool result.cvalue = dplace; } //######################################################## - else if (value->getFormat() == "formatXdr:A:D"){ + else if ((value->getFormat() == "formatXdr:A:D") || ((value->getFormat() == "formatXdr:A:rd"))){ double angle = 0; if (usesimudata == false) { angle = value->value; From e33f908187382ef2edf33c0e3a536d58d27d8ecb Mon Sep 17 00:00:00 2001 From: Thomas Hooge Date: Wed, 25 Feb 2026 20:03:54 +0100 Subject: [PATCH 10/75] Page system rework and improvements --- lib/obp60task/PageSystem.cpp | 795 ++++++++++++++++++++--------------- lib/obp60task/obp60task.cpp | 8 +- 2 files changed, 455 insertions(+), 348 deletions(-) diff --git a/lib/obp60task/PageSystem.cpp b/lib/obp60task/PageSystem.cpp index a8b3ee4..23baa42 100644 --- a/lib/obp60task/PageSystem.cpp +++ b/lib/obp60task/PageSystem.cpp @@ -38,6 +38,7 @@ private: String buzzer_mode; uint8_t buzzer_power; String cpuspeed; + String powermode; String rtc_module; String gps_module; String env_module; @@ -50,7 +51,7 @@ private: double homelat; double homelon; - char mode = 'N'; // (N)ormal, (S)ettings, (D)evice list, (C)ard + char mode = 'N'; // (N)ormal, (S)ettings, (C)onfiguration, (D)evice list, c(A)rd #ifdef PATCH_N2K struct device { @@ -63,13 +64,432 @@ private: std::vector devicelist; #endif + void incMode() { + if (mode == 'N') { // Normal + mode = 'S'; + } else if (mode == 'S') { // Settings + mode = 'C'; + } else if (mode == 'C') { // Config + mode = 'D'; + } else if (mode == 'D') { // Device list + if (use_sdcard) { + mode = 'A'; // SD-Card + } else { + mode = 'N'; + } + } else { + mode = 'N'; + } + } + + void decMode() { + if (mode == 'N') { + if (use_sdcard) { + mode = 'A'; // SD-Card + } else { + mode = 'D'; // Device list + } + } else if (mode == 'S') { // Settings + mode = 'N'; + } else if (mode == 'C') { // Config + mode = 'S'; + } else if (mode == 'D') { // Device list + mode = 'C'; + } else { + mode = 'D'; + } + } + + void displayModeNormal() { + // Default system page view + + uint16_t y0 = 155; + + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().setCursor(8, 48); + getdisplay().print("System Information"); + + getdisplay().drawXBitmap(320, 25, logo64_bits, logo64_width, logo64_height, commonData->fgcolor); + + getdisplay().setFont(&Ubuntu_Bold8pt8b); + + char ssid[13]; + snprintf(ssid, 13, "%04X%08X", (uint16_t)(chipid >> 32), (uint32_t)chipid); + displayBarcode(String(ssid), 320, 200, 2); + getdisplay().setCursor(8, 70); + getdisplay().print(String("MCUDEVICE-") + String(ssid)); + + getdisplay().setCursor(8, 95); + getdisplay().print("Firmware version: "); + getdisplay().setCursor(150, 95); + getdisplay().print(VERSINFO); + + getdisplay().setCursor(8, 113); + getdisplay().print("Board version: "); + getdisplay().setCursor(150, 113); + getdisplay().print(BOARDINFO); + getdisplay().print(String(" HW ") + String(PCBINFO)); + + getdisplay().setCursor(8, 131); + getdisplay().print("Display version: "); + getdisplay().setCursor(150, 131); + getdisplay().print(DISPLAYINFO); + getdisplay().print("; GxEPD2 v"); + getdisplay().print(GXEPD2INFO); + + getdisplay().setCursor(8, 265); +#ifdef BOARD_OBP60S3 + getdisplay().print("Press STBY to enter deep sleep mode"); +#endif +#ifdef BOARD_OBP40S3 + getdisplay().print("Press wheel to enter deep sleep mode"); +#endif + + // Flash memory size + uint32_t flash_size = ESP.getFlashChipSize(); + getdisplay().setCursor(8, y0); + getdisplay().print("FLASH:"); + getdisplay().setCursor(90, y0); + getdisplay().print(String(flash_size / 1024) + String(" kB")); + + // PSRAM memory size + uint32_t psram_size = ESP.getPsramSize(); + getdisplay().setCursor(8, y0 + 16); + getdisplay().print("PSRAM:"); + getdisplay().setCursor(90, y0 + 16); + getdisplay().print(String(psram_size / 1024) + String(" kB")); + + // FRAM available / status + getdisplay().setCursor(8, y0 + 32); + getdisplay().print("FRAM:"); + getdisplay().setCursor(90, y0 + 32); + getdisplay().print(hasFRAM ? "available" : "not found"); + +#ifdef BOARD_OBP40S3 + // SD-Card + getdisplay().setCursor(8, y0 + 48); + getdisplay().print("SD-Card:"); + getdisplay().setCursor(90, y0 + 48); + if (hasSDCard) { + uint64_t cardsize = ((uint64_t) sdcard->csd.capacity) * sdcard->csd.sector_size / (1024 * 1024); + getdisplay().printf("%llu MB", cardsize); + } else { + getdisplay().print("off"); + } +#endif + + // Uptime + int64_t uptime = esp_timer_get_time() / 1000000; + String uptime_unit; + if (uptime < 120) { + uptime_unit = " seconds"; + } else { + if (uptime < 2 * 3600) { + uptime /= 60; + uptime_unit = " minutes"; + } else if (uptime < 2 * 3600 * 24) { + uptime /= 3600; + uptime_unit = " hours"; + } else { + uptime /= 86400; + uptime_unit = " days"; + } + } + getdisplay().setCursor(8, y0 + 80); + getdisplay().print("Uptime:"); + getdisplay().setCursor(90, y0 + 80); + getdisplay().print(uptime); + getdisplay().print(uptime_unit); + + // CPU speed config / active + getdisplay().setCursor(202, y0); + getdisplay().print("CPU speed:"); + getdisplay().setCursor(300, y0); + getdisplay().print(cpuspeed); + getdisplay().print(" / "); + int cpu_freq = esp_clk_cpu_freq() / 1000000; + getdisplay().print(String(cpu_freq)); + + // total RAM free + int Heap_free = esp_get_free_heap_size(); + getdisplay().setCursor(202, y0 + 16); + getdisplay().print("Total free:"); + getdisplay().setCursor(300, y0 + 16); + getdisplay().print(String(Heap_free)); + + // RAM free for task + int RAM_free = uxTaskGetStackHighWaterMark(NULL); + getdisplay().setCursor(202, y0 + 32); + getdisplay().print("Task free:"); + getdisplay().setCursor(300, y0 + 32); + getdisplay().print(String(RAM_free)); + + } + + void displayModeConfig() { + // Configuration interface + + uint16_t x0 = 16; + uint16_t y0 = 80; + uint16_t dy = 20; + + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().setCursor(8, 48); + getdisplay().print("System configuration"); + + getdisplay().setFont(&Ubuntu_Bold8pt8b); + + getdisplay().setCursor(x0, y0); + getdisplay().print("CPU speed: 80 | 160 | 240"); + getdisplay().setCursor(x0, y0 + 1 * dy); + getdisplay().print("Power mode: Max | 5V | Min"); + getdisplay().setCursor(x0, y0 + 2 * dy); + getdisplay().print("Accesspoint: On | Off"); + + // TODO Change NVRAM-preferences settings here + getdisplay().setCursor(x0, y0 + 4 * dy); + getdisplay().print("Simulation: On | Off"); + + } + + void displayModeSettings() { + // View some of the current settings + + const uint16_t x0 = 8; + const uint16_t y0 = 72; + + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().setCursor(x0, 48); + getdisplay().print("System settings"); + + getdisplay().setFont(&Ubuntu_Bold8pt8b); + + // left column + getdisplay().setCursor(x0, y0); + getdisplay().print("Simulation:"); + getdisplay().setCursor(120, y0); + getdisplay().print(simulation ? "on" : "off"); + + getdisplay().setCursor(x0, y0 + 16); + getdisplay().print("Environment:"); + getdisplay().setCursor(120, y0 + 16); + getdisplay().print(env_module); + + getdisplay().setCursor(x0, y0 + 32); + getdisplay().print("Buzzer:"); + getdisplay().setCursor(120, y0 + 32); + getdisplay().print(buzzer_mode); + + getdisplay().setCursor(x0, y0 + 64); + getdisplay().print("GPS:"); + getdisplay().setCursor(120, y0 + 64); + getdisplay().print(gps_module); + + getdisplay().setCursor(x0, y0 + 80); + getdisplay().print("RTC:"); + getdisplay().setCursor(120, y0 + 80); + getdisplay().print(rtc_module); + + getdisplay().setCursor(x0, y0 + 96); + getdisplay().print("Wifi:"); + getdisplay().setCursor(120, y0 + 96); + getdisplay().print(commonData->status.wifiApOn ? "on" : "off"); + + // Home location + getdisplay().setCursor(x0, y0 + 128); + getdisplay().print("Home Lat.:"); + getdisplay().setCursor(120, y0 + 128); + getdisplay().print(formatLatitude(homelat)); + getdisplay().setCursor(x0, y0 + 144); + getdisplay().print("Home Lon.:"); + getdisplay().setCursor(120, y0 + 144); + getdisplay().print(formatLongitude(homelon)); + + // Power + getdisplay().setCursor(x0, y0 + 176); + getdisplay().print("Power mode:"); + getdisplay().setCursor(120, y0 + 176); + getdisplay().print(powermode); + + // right column + getdisplay().setCursor(202, y0); + getdisplay().print("Batt. sensor:"); + getdisplay().setCursor(320, y0); + getdisplay().print(batt_sensor); + + // Solar sensor + getdisplay().setCursor(202, y0 + 16); + getdisplay().print("Solar sensor:"); + getdisplay().setCursor(320, y0 + 16); + getdisplay().print(solar_sensor); + + // Generator sensor + getdisplay().setCursor(202, y0 + 32); + getdisplay().print("Gen. sensor:"); + getdisplay().setCursor(320, y0 + 32); + getdisplay().print(gen_sensor); + + // TODO + // Gyro sensor (rotation) + getdisplay().setCursor(202, y0 + 48); + getdisplay().print("Rot. sensor:"); + getdisplay().setCursor(320, y0 + 48); + getdisplay().print(rot_sensor); + + // Temp.-sensor + // Power Mode + +#ifdef BOARD_OBP60S3 + // Backlight infos + getdisplay().setCursor(202, y0 + 64); + getdisplay().print("Backlight:"); + getdisplay().setCursor(320, y0 + 64); + getdisplay().printf("%d%%", commonData->backlight.brightness); + // TODO test function with OBP60 device + getdisplay().setCursor(202, y0 + 80); + getdisplay().print("Bl color:"); + getdisplay().setCursor(320, y0 + 80); + getdisplay().print(commonData->backlight.color.toName()); + getdisplay().setCursor(202, y0 + 96); + getdisplay().print("Bl mode:"); + getdisplay().setCursor(320, y0 + 96); + getdisplay().print(commonData->backlight.mode); + // TODO Buzzer mode and power +#endif + } + + void displayModeSDCard() { + + // SD Card info + uint16_t x0 = 20; + uint16_t y0 = 72; + + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().setCursor(8, 48); + getdisplay().print("SD Card info"); + + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(x0, y0); +#ifdef BOARD_OBP60S3 + // This mode should not be callable by devices without card hardware + // In case of accidential reaching this, display a friendly message + getdisplay().print("This mode is not indended to be reached!\n"); + getdisplay().print("There's nothing to see here. Move on."); +#endif +#ifdef BOARD_OBP40S3 + getdisplay().print("Work in progress..."); + + /* TODO + this code should go somewhere else. only for testing purposes here + identify card as OBP-Card: + magic.dat + version.dat + readme.txt + IMAGES/ + CHARTS/ + LOGS/ + DATA/ + hint: file access with fopen, fgets, fread, fclose + */ + + // Simple test for magic file in root + getdisplay().setCursor(x0, y0 + 32); + String file_magic = MOUNT_POINT "/magic.dat"; + commonData->logger->logDebug(GwLog::LOG, "Test magicfile: %s", file_magic.c_str()); + struct stat st; + if (stat(file_magic.c_str(), &st) == 0) { + getdisplay().printf("File %s exists", file_magic.c_str()); + } else { + getdisplay().printf("File %s not found", file_magic.c_str()); + } + + // Root directory check + DIR* dir = opendir(MOUNT_POINT); + int dy = 0; + if (dir != NULL) { + commonData->logger->logDebug(GwLog::LOG, "Root directory: %s", MOUNT_POINT); + struct dirent* entry; + while (((entry = readdir(dir)) != NULL) and (dy < 140)) { + getdisplay().setCursor(x0, y0 + 64 + dy); + getdisplay().print(entry->d_name); + // type 1 is file, type 2 is dir + if (entry->d_type == 2) { + getdisplay().print("/"); + } + dy += 20; + commonData->logger->logDebug(GwLog::DEBUG, " %s type %d", entry->d_name, entry->d_type); + } + closedir(dir); + } else { + commonData->logger->logDebug(GwLog::LOG, "Failed to open root directory"); + } + +#endif + } + + void displayModeDevicelist() { + // NMEA2000 device list + getdisplay().setFont(&Ubuntu_Bold12pt8b); + getdisplay().setCursor(8, 48); + getdisplay().print("NMEA2000 device list"); + + getdisplay().setFont(&Ubuntu_Bold8pt8b); + getdisplay().setCursor(20, 70); + getdisplay().print("RxD: "); + getdisplay().print(String(commonData->status.n2kRx)); + getdisplay().setCursor(120, 70); + getdisplay().print("TxD: "); + getdisplay().print(String(commonData->status.n2kTx)); + +#ifdef PATCH_N2K + uint16_t x0 = 20; + uint16_t y0 = 100; + + getdisplay().setFont(&Ubuntu_Bold10pt8b); + getdisplay().setCursor(x0, y0); + getdisplay().print("ID"); + getdisplay().setCursor(x0 + 50, y0); + getdisplay().print("Model"); + getdisplay().setCursor(x0 + 250, y0); + getdisplay().print("Manuf."); + getdisplay().drawLine(18, y0 + 4, 360 , y0 + 4 , commonData->fgcolor); + + getdisplay().setFont(&Ubuntu_Bold8pt8b); + y0 = 120; + uint8_t n_dev = 0; + for (const device& item : devicelist) { + if (n_dev > 8) { + break; + } + getdisplay().setCursor(x0, y0 + n_dev * 20); + getdisplay().print(item.id); + getdisplay().setCursor(x0 + 50, y0 + n_dev * 20); + getdisplay().print(item.model); + getdisplay().setCursor(x0 + 250, y0 + n_dev * 20); + getdisplay().print(item.manuf_code); + n_dev++; + } + getdisplay().setCursor(x0, y0 + (n_dev + 1) * 20); + if (n_dev == 0) { + getdisplay().printf("no devices found on bus"); + + } else { + getdisplay().drawLine(18, y0 + n_dev * 20, 360 , y0 + n_dev * 20, commonData->fgcolor); + getdisplay().printf("%d devices of %d in total", n_dev, devicelist.size()); + } +#else + getdisplay().setCursor(20, 100); + getdisplay().print("NMEA2000 not exposed to obp60 task"); +#endif + } + public: PageSystem(CommonData &common){ commonData = &common; - common.logger->logDebug(GwLog::LOG,"Instantiate PageSystem"); + commonData->logger->logDebug(GwLog::LOG,"Instantiate PageSystem"); if (hasFRAM) { mode = fram.read(FRAM_SYSTEM_MODE); - common.logger->logDebug(GwLog::DEBUG, "Loaded mode '%c' from FRAM", mode); + commonData->logger->logDebug(GwLog::DEBUG, "Loaded mode '%c' from FRAM", mode); } chipid = ESP.getEfuseMac(); simulation = common.config->getBool(common.config->useSimuData); @@ -80,6 +500,7 @@ public: buzzer_mode.toLowerCase(); buzzer_power = common.config->getInt(common.config->buzzerPower); cpuspeed = common.config->getString(common.config->cpuSpeed); + powermode = common.config->getString(common.config->powerMode); env_module = common.config->getString(common.config->useEnvSensor); rtc_module = common.config->getString(common.config->useRTC); gps_module = common.config->getString(common.config->useGPS); @@ -106,19 +527,7 @@ public: // Switch display mode commonData->logger->logDebug(GwLog::LOG, "System keyboard handler"); if (key == 2) { - if (mode == 'N') { - mode = 'S'; - } else if (mode == 'S') { - mode = 'D'; - } else if (mode == 'D') { - if (hasSDCard) { - mode = 'C'; - } else { - mode = 'N'; - } - } else { - mode = 'N'; - } + incMode(); if (hasFRAM) fram.write(FRAM_SYSTEM_MODE, mode); return 0; } @@ -143,8 +552,13 @@ public: } #endif #ifdef BOARD_OBP40S3 - // grab cursor keys to disable page navigation - if (key == 9 or key == 10) { + // use cursor keys for local mode navigation + if (key == 9) { + incMode(); + return 0; + } + if (key == 10) { + decMode(); return 0; } // standby / deep sleep @@ -216,337 +630,30 @@ public: }; int displayPage(PageData &pageData){ - GwConfigHandler *config = commonData->config; - GwLog *logger = commonData->logger; - // Logging boat values - logger->logDebug(GwLog::LOG, "Drawing at PageSystem, Mode=%c", mode); - - // Draw page - //*********************************************************** - - uint16_t x0 = 8; // left column - uint16_t y0 = 48; // data table starts here + // Logging page information + commonData->logger->logDebug(GwLog::LOG, "Drawing at PageSystem, Mode=%c", mode); // Set display in partial refresh mode getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update - if (mode == 'N') { - - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(8, 48); - getdisplay().print("System Information"); - - getdisplay().drawXBitmap(320, 25, logo64_bits, logo64_width, logo64_height, commonData->fgcolor); - - getdisplay().setFont(&Ubuntu_Bold8pt8b); - y0 = 155; - - char ssid[13]; - snprintf(ssid, 13, "%04X%08X", (uint16_t)(chipid >> 32), (uint32_t)chipid); - displayBarcode(String(ssid), 320, 200, 2); - getdisplay().setCursor(8, 70); - getdisplay().print(String("MCUDEVICE-") + String(ssid)); - - getdisplay().setCursor(8, 95); - getdisplay().print("Firmware version: "); - getdisplay().setCursor(150, 95); - getdisplay().print(VERSINFO); - - getdisplay().setCursor(8, 113); - getdisplay().print("Board version: "); - getdisplay().setCursor(150, 113); - getdisplay().print(BOARDINFO); - getdisplay().print(String(" HW ") + String(PCBINFO)); - - getdisplay().setCursor(8, 131); - getdisplay().print("Display version: "); - getdisplay().setCursor(150, 131); - getdisplay().print(DISPLAYINFO); - getdisplay().print("; GxEPD2 v"); - getdisplay().print(GXEPD2INFO); - - getdisplay().setCursor(8, 265); -#ifdef BOARD_OBP60S3 - getdisplay().print("Press STBY to enter deep sleep mode"); -#endif -#ifdef BOARD_OBP40S3 - getdisplay().print("Press wheel to enter deep sleep mode"); -#endif - - // Flash memory size - uint32_t flash_size = ESP.getFlashChipSize(); - getdisplay().setCursor(8, y0); - getdisplay().print("FLASH:"); - getdisplay().setCursor(90, y0); - getdisplay().print(String(flash_size / 1024) + String(" kB")); - - // PSRAM memory size - uint32_t psram_size = ESP.getPsramSize(); - getdisplay().setCursor(8, y0 + 16); - getdisplay().print("PSRAM:"); - getdisplay().setCursor(90, y0 + 16); - getdisplay().print(String(psram_size / 1024) + String(" kB")); - - // FRAM available / status - getdisplay().setCursor(8, y0 + 32); - getdisplay().print("FRAM:"); - getdisplay().setCursor(90, y0 + 32); - getdisplay().print(hasFRAM ? "available" : "not found"); - -#ifdef BOARD_OBP40S3 - // SD-Card - getdisplay().setCursor(8, y0 + 48); - getdisplay().print("SD-Card:"); - getdisplay().setCursor(90, y0 + 48); - if (hasSDCard) { - uint64_t cardsize = ((uint64_t) sdcard->csd.capacity) * sdcard->csd.sector_size / (1024 * 1024); - getdisplay().printf("%llu MB", cardsize); - } else { - getdisplay().print("off"); - } -#endif - - // Uptime - int64_t uptime = esp_timer_get_time() / 1000000; - String uptime_unit; - if (uptime < 120) { - uptime_unit = " seconds"; - } else { - if (uptime < 2 * 3600) { - uptime /= 60; - uptime_unit = " minutes"; - } else if (uptime < 2 * 3600 * 24) { - uptime /= 3600; - uptime_unit = " hours"; - } else { - uptime /= 86400; - uptime_unit = " days"; - } - } - getdisplay().setCursor(8, y0 + 80); - getdisplay().print("Uptime:"); - getdisplay().setCursor(90, y0 + 80); - getdisplay().print(uptime); - getdisplay().print(uptime_unit); - - // CPU speed config / active - getdisplay().setCursor(202, y0); - getdisplay().print("CPU speed:"); - getdisplay().setCursor(300, y0); - getdisplay().print(cpuspeed); - getdisplay().print(" / "); - int cpu_freq = esp_clk_cpu_freq() / 1000000; - getdisplay().print(String(cpu_freq)); - - // total RAM free - int Heap_free = esp_get_free_heap_size(); - getdisplay().setCursor(202, y0 + 16); - getdisplay().print("Total free:"); - getdisplay().setCursor(300, y0 + 16); - getdisplay().print(String(Heap_free)); - - // RAM free for task - int RAM_free = uxTaskGetStackHighWaterMark(NULL); - getdisplay().setCursor(202, y0 + 32); - getdisplay().print("Task free:"); - getdisplay().setCursor(300, y0 + 32); - getdisplay().print(String(RAM_free)); - - } else if (mode == 'S') { - // Settings - - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(x0, 48); - getdisplay().print("System settings"); - - getdisplay().setFont(&Ubuntu_Bold8pt8b); - x0 = 8; - y0 = 72; - - // left column - getdisplay().setCursor(x0, y0); - getdisplay().print("Simulation:"); - getdisplay().setCursor(120, y0); - getdisplay().print(simulation ? "on" : "off"); - - getdisplay().setCursor(x0, y0 + 16); - getdisplay().print("Environment:"); - getdisplay().setCursor(120, y0 + 16); - getdisplay().print(env_module); - - getdisplay().setCursor(x0, y0 + 32); - getdisplay().print("Buzzer:"); - getdisplay().setCursor(120, y0 + 32); - getdisplay().print(buzzer_mode); - - getdisplay().setCursor(x0, y0 + 64); - getdisplay().print("GPS:"); - getdisplay().setCursor(120, y0 + 64); - getdisplay().print(gps_module); - - getdisplay().setCursor(x0, y0 + 80); - getdisplay().print("RTC:"); - getdisplay().setCursor(120, y0 + 80); - getdisplay().print(rtc_module); - - getdisplay().setCursor(x0, y0 + 96); - getdisplay().print("Wifi:"); - getdisplay().setCursor(120, y0 + 96); - getdisplay().print(commonData->status.wifiApOn ? "on" : "off"); - - // Home location - getdisplay().setCursor(x0, y0 + 128); - getdisplay().print("Home Lat.:"); - getdisplay().setCursor(120, y0 + 128); - getdisplay().print(formatLatitude(homelat)); - getdisplay().setCursor(x0, y0 + 144); - getdisplay().print("Home Lon.:"); - getdisplay().setCursor(120, y0 + 144); - getdisplay().print(formatLongitude(homelon)); - - // right column - getdisplay().setCursor(202, y0); - getdisplay().print("Batt. sensor:"); - getdisplay().setCursor(320, y0); - getdisplay().print(batt_sensor); - - // Solar sensor - getdisplay().setCursor(202, y0 + 16); - getdisplay().print("Solar sensor:"); - getdisplay().setCursor(320, y0 + 16); - getdisplay().print(solar_sensor); - - // Generator sensor - getdisplay().setCursor(202, y0 + 32); - getdisplay().print("Gen. sensor:"); - getdisplay().setCursor(320, y0 + 32); - getdisplay().print(gen_sensor); - - // Gyro sensor - - } else if (mode == 'C') { - // Card info - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(8, 48); - getdisplay().print("SD Card info"); - - getdisplay().setFont(&Ubuntu_Bold8pt8b); - - x0 = 20; - y0 = 72; - getdisplay().setCursor(x0, y0); -#ifdef BOARD_OBP60S3 - // This mode should not be callable by devices without card hardware - // In case of accidential reaching this, display a friendly message - getdisplay().print("This mode is not indended to be reached!\n"); - getdisplay().print("There's nothing to see here. Move on."); -#endif -#ifdef BOARD_OBP40S3 - getdisplay().print("Work in progress..."); - - /* TODO - this code should go somewhere else. only for testing purposes here - identify card as OBP-Card: - magic.dat - version.dat - readme.txt - IMAGES/ - CHARTS/ - LOGS/ - DATA/ - hint: file access with fopen, fgets, fread, fclose - */ - - // Simple test for magic file in root - getdisplay().setCursor(x0, y0 + 32); - String file_magic = MOUNT_POINT "/magic.dat"; - logger->logDebug(GwLog::LOG, "Test magicfile: %s", file_magic.c_str()); - struct stat st; - if (stat(file_magic.c_str(), &st) == 0) { - getdisplay().printf("File %s exists", file_magic.c_str()); - } else { - getdisplay().printf("File %s not found", file_magic.c_str()); - } - - // Root directory check - DIR* dir = opendir(MOUNT_POINT); - int dy = 0; - if (dir != NULL) { - logger->logDebug(GwLog::LOG, "Root directory: %s", MOUNT_POINT); - struct dirent* entry; - while (((entry = readdir(dir)) != NULL) and (dy < 140)) { - getdisplay().setCursor(x0, y0 + 64 + dy); - getdisplay().print(entry->d_name); - // type 1 is file, type 2 is dir - if (entry->d_type == 2) { - getdisplay().print("/"); - } - dy += 20; - logger->logDebug(GwLog::DEBUG, " %s type %d", entry->d_name, entry->d_type); - } - closedir(dir); - } else { - logger->logDebug(GwLog::LOG, "Failed to open root directory"); - } - -#endif - - } else { - // NMEA2000 device list - getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().setCursor(8, 48); - getdisplay().print("NMEA2000 device list"); - - getdisplay().setFont(&Ubuntu_Bold8pt8b); - getdisplay().setCursor(20, 70); - getdisplay().print("RxD: "); - getdisplay().print(String(commonData->status.n2kRx)); - getdisplay().setCursor(120, 70); - getdisplay().print("TxD: "); - getdisplay().print(String(commonData->status.n2kTx)); - -#ifdef PATCH_N2K - x0 = 20; - y0 = 100; - - getdisplay().setFont(&Ubuntu_Bold10pt8b); - getdisplay().setCursor(x0, y0); - getdisplay().print("ID"); - getdisplay().setCursor(x0 + 50, y0); - getdisplay().print("Model"); - getdisplay().setCursor(x0 + 250, y0); - getdisplay().print("Manuf."); - getdisplay().drawLine(18, y0 + 4, 360 , y0 + 4 , commonData->fgcolor); - - getdisplay().setFont(&Ubuntu_Bold8pt8b); - y0 = 120; - uint8_t n_dev = 0; - for (const device& item : devicelist) { - if (n_dev > 8) { - break; - } - getdisplay().setCursor(x0, y0 + n_dev * 20); - getdisplay().print(item.id); - getdisplay().setCursor(x0 + 50, y0 + n_dev * 20); - getdisplay().print(item.model); - getdisplay().setCursor(x0 + 250, y0 + n_dev * 20); - getdisplay().print(item.manuf_code); - n_dev++; - } - getdisplay().setCursor(x0, y0 + (n_dev + 1) * 20); - if (n_dev == 0) { - getdisplay().printf("no devices found on bus"); - - } else { - getdisplay().drawLine(18, y0 + n_dev * 20, 360 , y0 + n_dev * 20, commonData->fgcolor); - getdisplay().printf("%d devices of %d in total", n_dev, devicelist.size()); - } -#else - getdisplay().setCursor(20, 100); - getdisplay().print("NMEA2000 not exposed to obp60 task"); -#endif - + // call current system page + switch (mode) { + case 'N': + displayModeNormal(); + break; + case 'S': + displayModeSettings(); + break; + case 'C': + displayModeConfig(); + break; + case 'A': + displayModeSDCard(); + break; + case 'D': + displayModeDevicelist(); + break; } // Update display diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index 84bc572..8c63fe4 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=-1; // initialize with an impiossible value, so we can detect wether we are during startup and no page has been displayed yet + int lastPage=-1; // initialize with an impossible 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 HstryBuffers hstryBufferList(1920, &boatValues, logger); // Create empty list of boat data history buffers (1.920 values = seconds = 32 min.) @@ -729,7 +729,7 @@ void OBP60Task(GwApi *api){ else{ getdisplay().fillScreen(commonData.fgcolor); // Clear display #ifdef DISPLAY_GDEY042T81 - getdisplay().hibernate(); // Set display in hybenate mode + getdisplay().hibernate(); // Set display in hibenate mode getdisplay().init(115200, true, 2, false); // Init for Waveshare boards with "clever" reset circuit, 2ms reset pulse #else getdisplay().init(115200); // Init for normal displays @@ -757,7 +757,7 @@ void OBP60Task(GwApi *api){ else{ getdisplay().fillScreen(commonData.fgcolor); // Clear display #ifdef DISPLAY_GDEY042T81 - getdisplay().hibernate(); // Set display in hybenate mode + getdisplay().hibernate(); // Set display in hibernate mode getdisplay().init(115200, true, 2, false); // Init for Waveshare boards with "clever" reset circuit, 2ms reset pulse #else getdisplay().init(115200); // Init for normal displays @@ -782,7 +782,7 @@ void OBP60Task(GwApi *api){ else{ getdisplay().fillScreen(commonData.fgcolor); // Clear display #ifdef DISPLAY_GDEY042T81 - getdisplay().hibernate(); // Set display in hybenate mode + getdisplay().hibernate(); // Set display in hibernate mode getdisplay().init(115200, true, 2, false); // Init for Waveshare boards with "clever" reset circuit, 2ms reset pulse #else getdisplay().init(115200); // Init for normal displays From aea401589d793e0e4919195f09340ad071951d68 Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Fri, 27 Feb 2026 21:33:23 +0000 Subject: [PATCH 11/75] Add 4 inch TFT display ST7796 --- lib/obp60task/OBP60Extensions.cpp | 48 ++++++++++- lib/obp60task/OBP60Extensions.h | 117 +++++++++++++++++++++++++++ lib/obp60task/OBP60QRWiFi.h | 2 +- lib/obp60task/OBPcharts.cpp | 10 ++- lib/obp60task/PageAutopilot.cpp | 2 +- lib/obp60task/PageBME280.cpp | 2 +- lib/obp60task/PageBattery.cpp | 2 +- lib/obp60task/PageBattery2.cpp | 2 +- lib/obp60task/PageClock.cpp | 12 +-- lib/obp60task/PageCompass.cpp | 2 +- lib/obp60task/PageDST810.cpp | 2 +- lib/obp60task/PageDigitalOut.cpp | 2 +- lib/obp60task/PageFluid.cpp | 2 +- lib/obp60task/PageFourValues.cpp | 2 +- lib/obp60task/PageFourValues2.cpp | 2 +- lib/obp60task/PageGenerator.cpp | 2 +- lib/obp60task/PageKeelPosition.cpp | 4 +- lib/obp60task/PageNavigation.cpp | 2 +- lib/obp60task/PageOneValue.cpp | 2 +- lib/obp60task/PageRollPitch.cpp | 4 +- lib/obp60task/PageRudderPosition.cpp | 4 +- lib/obp60task/PageSixValues.cpp | 2 +- lib/obp60task/PageSkyView.cpp | 10 +-- lib/obp60task/PageSolar.cpp | 2 +- lib/obp60task/PageSystem.cpp | 4 +- lib/obp60task/PageThreeValues.cpp | 2 +- lib/obp60task/PageTwoValues.cpp | 2 +- lib/obp60task/PageVoltage.cpp | 2 +- lib/obp60task/PageWhite.cpp | 2 +- lib/obp60task/PageWind.cpp | 2 +- lib/obp60task/PageWindPlot.cpp | 2 +- lib/obp60task/PageWindRose.cpp | 4 +- lib/obp60task/PageWindRoseFlex.cpp | 4 +- lib/obp60task/PageXTETrack.cpp | 12 +-- lib/obp60task/obp60task.cpp | 72 +++++++++++------ lib/obp60task/platformio.ini | 8 +- 36 files changed, 272 insertions(+), 85 deletions(-) diff --git a/lib/obp60task/OBP60Extensions.cpp b/lib/obp60task/OBP60Extensions.cpp index 3b3ee88..6dbcf05 100644 --- a/lib/obp60task/OBP60Extensions.cpp +++ b/lib/obp60task/OBP60Extensions.cpp @@ -56,6 +56,12 @@ GxEPD2_BW display(GxEPD2_4 GxEPD2_BW & getdisplay(){return display;} #endif +#ifdef DISPLAY_ST7796 +// only instantiate; class defined in header +static LGFX display; +LGFX & getdisplay(){return display;} +#endif + // Horter I2C moduls PCF8574 pcf8574_Modul1(PCF8574_I2C_ADDR1); // First digital IO modul PCF8574 from Horter @@ -251,8 +257,10 @@ void deepSleep(CommonData &common){ getdisplay().setFont(&Ubuntu_Bold8pt8b); getdisplay().setCursor(65, 175); getdisplay().print("To wake up press key and wait 5s"); - getdisplay().nextPage(); // Update display contents + displayNextPage(); // Update display contents + #ifndef DISPLAY_ST7796 getdisplay().powerOff(); // Display power off + #endif setPortPin(OBP_POWER_50, false); // Power off ePaper display // Stop system esp_deep_sleep_start(); // Deep Sleep with weakup via touch pin @@ -276,8 +284,10 @@ void deepSleep(CommonData &common){ getdisplay().setFont(&Ubuntu_Bold8pt8b); getdisplay().setCursor(65, 175); getdisplay().print("To wake up press wheel and wait 5s"); - getdisplay().nextPage(); // Partial update + displayNextPage(); // Partial update + #ifndef DISPLAY_ST7796 getdisplay().powerOff(); // Display power off + #endif setPortPin(OBP_POWER_EPD, false); // Power off ePaper display setPortPin(OBP_POWER_SD, false); // Power off SD card // Stop system @@ -479,7 +489,13 @@ std::vector wordwrap(String &line, uint16_t maxwidth) { void drawTextCenter(int16_t cx, int16_t cy, String text) { int16_t x1, y1; uint16_t w, h; +#ifdef DISPLAY_ST7796 + // LovyanGFX doesn't expose getTextBounds; use width/height helpers + w = getdisplay().textWidth(text); + h = getdisplay().fontHeight(); +#else getdisplay().getTextBounds(text, 0, 150, &x1, &y1, &w, &h); +#endif getdisplay().setCursor(cx - w / 2, cy + h / 2); getdisplay().print(text); } @@ -490,7 +506,12 @@ void drawButtonCenter(int16_t cx, int16_t cy, int8_t sx, int8_t sy, String text, uint16_t w, h; uint16_t color; +#ifdef DISPLAY_ST7796 + w = getdisplay().textWidth(text); + h = getdisplay().fontHeight(); +#else getdisplay().getTextBounds(text, cx, cy, &x1, &y1, &w, &h); // Find text center +#endif getdisplay().setCursor(cx - w/2, cy + h/2); // Set cursor to center //getdisplay().drawPixel(cx, cy, fg); // Debug pixel for center position if (inverted) { @@ -509,7 +530,12 @@ void drawButtonCenter(int16_t cx, int16_t cy, int8_t sx, int8_t sy, String text, void drawTextRalign(int16_t x, int16_t y, String text) { int16_t x1, y1; uint16_t w, h; +#ifdef DISPLAY_ST7796 + w = getdisplay().textWidth(text); + h = getdisplay().fontHeight(); +#else getdisplay().getTextBounds(text, 0, 150, &x1, &y1, &w, &h); +#endif getdisplay().setCursor(x - w - 1, y); // '-1' required since some strings wrap around w/o it getdisplay().print(text); } @@ -1000,7 +1026,14 @@ void displayRudderPosition(int rudderPosition, uint8_t rangeDeg, uint16_t cx, ui String lbl = String(angle); int16_t bx, by; uint16_t bw, bh; - getdisplay().getTextBounds(lbl, 0, 0, &bx, &by, &bw, &bh); + #ifdef DISPLAY_ST7796 + // LovyanGFX: compute width/height manually + bw = getdisplay().textWidth(lbl); + bh = getdisplay().fontHeight(); + bx = 0; by = 0; + #else + getdisplay().getTextBounds(lbl, 0, 0, &bx, &by, &bw, &bh); + #endif int16_t tx = xpos - bw/2; int16_t ty = top + h + bh + 5; // A little spacing getdisplay().setCursor(tx, ty); @@ -1019,9 +1052,16 @@ void doImageRequest(GwApi *api, int *pageno, const PageStruct pages[MAX_PAGE_NUM logger->logDebug(GwLog::LOG,"handle image request [%s]: %s", imgformat, filename); - uint8_t *fb = getdisplay().getBuffer(); // EPD framebuffer + uint8_t *fb = nullptr; // EPD framebuffer std::vector imageBuffer; // image in webserver transferbuffer String mimetype; + #ifndef DISPLAY_ST7796 + fb = getdisplay().getBuffer(); // available only for EPD + #endif + if (!fb) { + request->send(500, "text/plain", "screenshot not available"); + return; + } if (imgformat == "gif") { // GIF is commpressed with LZW, so small diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index 41e3717..47767f3 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -9,6 +9,10 @@ #include // I2C FRAM #include +#ifdef DISPLAY_ST7796 +#include // TFT LCD lib for ST7796 +#endif + #ifdef BOARD_OBP40S3 #include "esp_vfs_fat.h" #include "sdmmc_cmd.h" @@ -74,11 +78,124 @@ GxEPD2_BW & getdisplay(); GxEPD2_BW & getdisplay(); #endif +#ifdef DISPLAY_ST7796 +// LovyanGFX based display wrapper for ST7796 +class LGFX : public lgfx::LGFX_Device { +public: + lgfx::Bus_SPI _bus_instance; + lgfx::Light_PWM _light_instance; + + LGFX(void) { + { + auto cfg = _bus_instance.config(); + cfg.spi_host = SPI2_HOST; + cfg.spi_mode = 0; + cfg.freq_write = 80000000; + cfg.freq_read = 16000000; + cfg.pin_sclk = OBP_SPI_CLK; + cfg.pin_mosi = OBP_SPI_DIN; + cfg.pin_miso = -1; + cfg.pin_dc = OBP_SPI_DC; + _bus_instance.config(cfg); + _panel_instance.setBus(&_bus_instance); + } + { + auto cfg = _panel_instance.config(); + cfg.pin_cs = OBP_SPI_CS; + cfg.pin_rst = OBP_SPI_RST; + cfg.pin_busy = -1; + cfg.panel_width = 480; + cfg.panel_height = 320; + cfg.offset_x = 0; + cfg.offset_y = 0; + cfg.offset_rotation = 0; + cfg.dummy_read_pixel = 8; + cfg.dummy_read_bits = 1; + cfg.memory_width = 480; + cfg.memory_height = 320; + // cfg.pwm_control not available in this LovyanGFX version + cfg.invert = false; + cfg.rgb_order = false; + cfg.dlen_16bit = false; + cfg.bus_shared = true; + _panel_instance.config(cfg); + } + { + auto cfg = _light_instance.config(); + cfg.pin_bl = -1; + _light_instance.config(cfg); + _panel_instance.setLight(&_light_instance); + } + setPanel(&_panel_instance); + } + + // compatibility helpers -------------------------------------------------- + // Adafruit GFX fonts support: ignore on TFT, use base on E-Ink + void setFont(const GFXfont *font) { (void)font; } + // E-Ink interface compatibility + void setFullWindow() { /* no-op on TFT */ } + +private: + lgfx::Panel_ST7796 _panel_instance; +}; + +LGFX & getdisplay(); +#endif + // Page display return values #define PAGE_OK 0 // all ok, do nothing #define PAGE_UPDATE 1 // page wants display to update #define PAGE_HIBERNATE 2 // page wants displey to hibernate +// Display wrapper functions for E-Ink/TFT compatibility +inline void displayFirstPage() { + #ifdef DISPLAY_ST7796 + // TFT LCD doesn't need firstPage() + #else + getdisplay().firstPage(); + #endif +} + +inline void displayNextPage() { + #ifdef DISPLAY_ST7796 + // TFT LCD doesn't need nextPage() for refresh + #else + getdisplay().nextPage(); + #endif +} + +inline void displaySetPartialWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h) { + #ifdef DISPLAY_ST7796 + // TFT LCD doesn't use partial windows + (void)x; (void)y; (void)w; (void)h; + #else + getdisplay().setPartialWindow(x, y, w, h); + #endif +} + +inline void displaySetFullWindow() { + #ifdef DISPLAY_ST7796 + // TFT LCD doesn't need setFullWindow() + #else + getdisplay().setFullWindow(); + #endif +} + +// replacement for getTextBounds that works with both EPD and ST7796 +inline void displayGetTextBounds(const String &txt, int16_t x, int16_t y, + int16_t *x0, int16_t *y0, + uint16_t *w, uint16_t *h) { +#ifdef DISPLAY_ST7796 + // LovyanGFX doesn't expose getTextBounds; compute via helpers + *w = getdisplay().textWidth(txt); + *h = getdisplay().fontHeight(); + if (x0) *x0 = 0; + if (y0) *y0 = 0; +#else + getdisplay().getTextBounds(txt, x, y, x0, y0, w, h); +#endif +} + void fillPoly4(const std::vector& p4, uint16_t color); void drawPoly(const std::vector& points, uint16_t color); diff --git a/lib/obp60task/OBP60QRWiFi.h b/lib/obp60task/OBP60QRWiFi.h index d3f5248..d2139ed 100644 --- a/lib/obp60task/OBP60QRWiFi.h +++ b/lib/obp60task/OBP60QRWiFi.h @@ -39,7 +39,7 @@ void qrWiFi(String ssid, String passwd, uint16_t fgcolor, uint16_t bgcolor){ getdisplay().setTextColor(fgcolor); getdisplay().setCursor(140, 285); getdisplay().print("WiFi"); - getdisplay().nextPage(); // Full Refresh + displayNextPage(); // Full Refresh } #endif diff --git a/lib/obp60task/OBPcharts.cpp b/lib/obp60task/OBPcharts.cpp index 8ae38ec..b245972 100644 --- a/lib/obp60task/OBPcharts.cpp +++ b/lib/obp60task/OBPcharts.cpp @@ -28,8 +28,14 @@ Chart::Chart(RingBuffer& dataBuf, double dfltRng, CommonData& common, fgColor = commonData->fgcolor; bgColor = commonData->bgcolor; - dWidth = getdisplay().width(); - dHeight = getdisplay().height(); + // display dimensions (avoid calling width()/height() on incomplete LGFX type) + #ifdef DISPLAY_ST7796 + dWidth = 480; + dHeight = 320; + #else + dWidth = getdisplay().width(); + dHeight = getdisplay().height(); + #endif dataBuf.getMetaData(dbName, dbFormat); dbMIN_VAL = dataBuf.getMinVal(); diff --git a/lib/obp60task/PageAutopilot.cpp b/lib/obp60task/PageAutopilot.cpp index ecfc62e..b57c159 100644 --- a/lib/obp60task/PageAutopilot.cpp +++ b/lib/obp60task/PageAutopilot.cpp @@ -117,7 +117,7 @@ class PageAutopilot : public Page //*********************************************************** // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + displaySetPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update getdisplay().setTextColor(commonData->fgcolor); /* // Horizontal line 2 pix top & bottom diff --git a/lib/obp60task/PageBME280.cpp b/lib/obp60task/PageBME280.cpp index 540d5c5..e54b629 100644 --- a/lib/obp60task/PageBME280.cpp +++ b/lib/obp60task/PageBME280.cpp @@ -105,7 +105,7 @@ class PageBME280 : public Page //*********************************************************** // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + displaySetPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update getdisplay().setTextColor(commonData->fgcolor); diff --git a/lib/obp60task/PageBattery.cpp b/lib/obp60task/PageBattery.cpp index 463b848..e53f12f 100644 --- a/lib/obp60task/PageBattery.cpp +++ b/lib/obp60task/PageBattery.cpp @@ -158,7 +158,7 @@ class PageBattery : public Page //*********************************************************** // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + displaySetPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update // Show average settings getdisplay().setTextColor(commonData->fgcolor); diff --git a/lib/obp60task/PageBattery2.cpp b/lib/obp60task/PageBattery2.cpp index b0b712b..05ae52e 100644 --- a/lib/obp60task/PageBattery2.cpp +++ b/lib/obp60task/PageBattery2.cpp @@ -184,7 +184,7 @@ public: //*********************************************************** // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + displaySetPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update getdisplay().setTextColor(commonData->fgcolor); diff --git a/lib/obp60task/PageClock.cpp b/lib/obp60task/PageClock.cpp index 3c5a63f..4ff6783 100644 --- a/lib/obp60task/PageClock.cpp +++ b/lib/obp60task/PageClock.cpp @@ -375,7 +375,7 @@ public: //*********************************************************** // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + displaySetPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update getdisplay().setTextColor(commonData->fgcolor); @@ -442,8 +442,8 @@ public: 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); +displayGetTextBounds("00", 0, 0, &x0, &y0, &wDigit, &hDigit); +displayGetTextBounds(":", 0, 0, &x0, &y0, &wColon, &hColon); uint16_t totalWidth = 3 * wDigit + 2 * wColon; @@ -453,7 +453,7 @@ public: // Draw time string centered int16_t x1b, y1b; uint16_t wb, hb; - getdisplay().getTextBounds(timeStr, 0, 0, &x1b, &y1b, &wb, &hb); + displayGetTextBounds(timeStr, 0, 0, &x1b, &y1b, &wb, &hb); int16_t textX = (static_cast(getdisplay().width()) - static_cast(wb)) / 2; int16_t textY = centerY + hb / 2; @@ -520,7 +520,7 @@ public: int16_t x1b, y1b; uint16_t wb, hb; - getdisplay().getTextBounds(timeStr, 0, 0, &x1b, &y1b, &wb, &hb); + displayGetTextBounds(timeStr, 0, 0, &x1b, &y1b, &wb, &hb); int16_t x = (static_cast(getdisplay().width()) - static_cast(wb)) / 2; int16_t y = 150 + hb / 2; @@ -665,7 +665,7 @@ public: // 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 + displayGetTextBounds(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); diff --git a/lib/obp60task/PageCompass.cpp b/lib/obp60task/PageCompass.cpp index 12281e9..f5e14b5 100644 --- a/lib/obp60task/PageCompass.cpp +++ b/lib/obp60task/PageCompass.cpp @@ -109,7 +109,7 @@ class PageCompass : public Page //*********************************************************** // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + displaySetPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update getdisplay().setTextColor(commonData->fgcolor); // Horizontal line 2 pix top & bottom diff --git a/lib/obp60task/PageDST810.cpp b/lib/obp60task/PageDST810.cpp index 2ac7494..93021c2 100644 --- a/lib/obp60task/PageDST810.cpp +++ b/lib/obp60task/PageDST810.cpp @@ -91,7 +91,7 @@ public: //*********************************************************** // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + displaySetPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update getdisplay().setTextColor(commonData->fgcolor); diff --git a/lib/obp60task/PageDigitalOut.cpp b/lib/obp60task/PageDigitalOut.cpp index 464a069..1919785 100644 --- a/lib/obp60task/PageDigitalOut.cpp +++ b/lib/obp60task/PageDigitalOut.cpp @@ -106,7 +106,7 @@ bool button5 = false; //*********************************************************** // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + displaySetPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update getdisplay().setTextColor(commonData->fgcolor); getdisplay().setFont(&Ubuntu_Bold12pt8b); // Write text diff --git a/lib/obp60task/PageFluid.cpp b/lib/obp60task/PageFluid.cpp index f844d87..f17038a 100644 --- a/lib/obp60task/PageFluid.cpp +++ b/lib/obp60task/PageFluid.cpp @@ -138,7 +138,7 @@ class PageFluid : public Page //*********************************************************** // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); + displaySetPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); getdisplay().setTextColor(commonData->fgcolor); diff --git a/lib/obp60task/PageFourValues.cpp b/lib/obp60task/PageFourValues.cpp index cb9de68..5797f55 100644 --- a/lib/obp60task/PageFourValues.cpp +++ b/lib/obp60task/PageFourValues.cpp @@ -91,7 +91,7 @@ class PageFourValues : public Page //*********************************************************** // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + displaySetPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update getdisplay().setTextColor(commonData->fgcolor); diff --git a/lib/obp60task/PageFourValues2.cpp b/lib/obp60task/PageFourValues2.cpp index 730e14b..63b0045 100644 --- a/lib/obp60task/PageFourValues2.cpp +++ b/lib/obp60task/PageFourValues2.cpp @@ -91,7 +91,7 @@ class PageFourValues2 : public Page //*********************************************************** // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + displaySetPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update getdisplay().setTextColor(commonData->fgcolor); diff --git a/lib/obp60task/PageGenerator.cpp b/lib/obp60task/PageGenerator.cpp index 201b647..05550cb 100644 --- a/lib/obp60task/PageGenerator.cpp +++ b/lib/obp60task/PageGenerator.cpp @@ -83,7 +83,7 @@ public: //*********************************************************** // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + displaySetPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update getdisplay().setTextColor(commonData->fgcolor); diff --git a/lib/obp60task/PageKeelPosition.cpp b/lib/obp60task/PageKeelPosition.cpp index dd86611..ddb47aa 100644 --- a/lib/obp60task/PageKeelPosition.cpp +++ b/lib/obp60task/PageKeelPosition.cpp @@ -68,7 +68,7 @@ public: //*********************************************************** // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + displaySetPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update //******************************************************************************************* @@ -105,7 +105,7 @@ public: // 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 + displayGetTextBounds(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_Bold8pt8b); diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp index bd4adf1..92d6789 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -371,7 +371,7 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map // ############### Draw Navigation Map ################ // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + displaySetPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update getdisplay().setTextColor(commonData->fgcolor); // If a network connection to URL then load the navigation map diff --git a/lib/obp60task/PageOneValue.cpp b/lib/obp60task/PageOneValue.cpp index 78cc6e6..dd3eb1b 100644 --- a/lib/obp60task/PageOneValue.cpp +++ b/lib/obp60task/PageOneValue.cpp @@ -276,7 +276,7 @@ public: // Draw page //*********************************************************** - getdisplay().setPartialWindow(0, 0, width, height); // Set partial update + displaySetPartialWindow(0, 0, width, height); // Set partial update if (pageMode == VALUE || dataHstryBuf == nullptr) { // show only data value; ignore other pageMode options if no chart supported boat data history buffer is available diff --git a/lib/obp60task/PageRollPitch.cpp b/lib/obp60task/PageRollPitch.cpp index 3cc9277..59c79dc 100644 --- a/lib/obp60task/PageRollPitch.cpp +++ b/lib/obp60task/PageRollPitch.cpp @@ -116,7 +116,7 @@ public: //*********************************************************** // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + displaySetPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update getdisplay().setTextColor(commonData->fgcolor); @@ -194,7 +194,7 @@ public: // 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 + displayGetTextBounds(ii, int(x), int(y), &x1, &y1, &w, &h); // Calc width of new string getdisplay().setCursor(x-w/2, y+h/2); if(i % 20 == 0){ getdisplay().setFont(&Ubuntu_Bold8pt8b); diff --git a/lib/obp60task/PageRudderPosition.cpp b/lib/obp60task/PageRudderPosition.cpp index 6a8695f..a09f4e6 100644 --- a/lib/obp60task/PageRudderPosition.cpp +++ b/lib/obp60task/PageRudderPosition.cpp @@ -72,7 +72,7 @@ public: //*********************************************************** // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + displaySetPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update //******************************************************************************************* @@ -110,7 +110,7 @@ public: // 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 + displayGetTextBounds(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_Bold8pt8b); diff --git a/lib/obp60task/PageSixValues.cpp b/lib/obp60task/PageSixValues.cpp index b75f307..8d89167 100644 --- a/lib/obp60task/PageSixValues.cpp +++ b/lib/obp60task/PageSixValues.cpp @@ -75,7 +75,7 @@ class PageSixValues : public Page //*********************************************************** // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + displaySetPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update getdisplay().setTextColor(commonData->fgcolor); for (int i = 0; i < ( HowManyValues / 2 ); i++){ diff --git a/lib/obp60task/PageSkyView.cpp b/lib/obp60task/PageSkyView.cpp index 9d80a85..a9bf8ef 100644 --- a/lib/obp60task/PageSkyView.cpp +++ b/lib/obp60task/PageSkyView.cpp @@ -73,7 +73,7 @@ public: //*********************************************************** // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + displaySetPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update // current position getdisplay().setFont(&Ubuntu_Bold8pt8b); @@ -105,19 +105,19 @@ public: uint16_t w, h; getdisplay().setFont(&Ubuntu_Bold12pt8b); - getdisplay().getTextBounds("N", 0, 150, &x1, &y1, &w, &h); + displayGetTextBounds("N", 0, 150, &x1, &y1, &w, &h); getdisplay().setCursor(c.x - w / 2, c.y - r + h + 3); getdisplay().print("N"); - getdisplay().getTextBounds("S", 0, 150, &x1, &y1, &w, &h); + displayGetTextBounds("S", 0, 150, &x1, &y1, &w, &h); getdisplay().setCursor(c.x - w / 2, c.y + r - 3); getdisplay().print("S"); - getdisplay().getTextBounds("E", 0, 150, &x1, &y1, &w, &h); + displayGetTextBounds("E", 0, 150, &x1, &y1, &w, &h); getdisplay().setCursor(c.x + r - w - 3, c.y + h / 2); getdisplay().print("E"); - getdisplay().getTextBounds("W", 0, 150, &x1, &y1, &w, &h); + displayGetTextBounds("W", 0, 150, &x1, &y1, &w, &h); getdisplay().setCursor(c.x - r + 3 , c.y + h / 2); getdisplay().print("W"); diff --git a/lib/obp60task/PageSolar.cpp b/lib/obp60task/PageSolar.cpp index 2a19bb9..e06b95e 100644 --- a/lib/obp60task/PageSolar.cpp +++ b/lib/obp60task/PageSolar.cpp @@ -82,7 +82,7 @@ public: //*********************************************************** // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + displaySetPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update getdisplay().setTextColor(commonData->fgcolor); diff --git a/lib/obp60task/PageSystem.cpp b/lib/obp60task/PageSystem.cpp index a8b3ee4..223bf81 100644 --- a/lib/obp60task/PageSystem.cpp +++ b/lib/obp60task/PageSystem.cpp @@ -229,7 +229,7 @@ public: uint16_t y0 = 48; // data table starts here // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + displaySetPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update if (mode == 'N') { @@ -550,7 +550,7 @@ public: } // Update display - getdisplay().nextPage(); // Partial update (fast) + displayNextPage(); // Partial update (fast) return PAGE_OK; }; }; diff --git a/lib/obp60task/PageThreeValues.cpp b/lib/obp60task/PageThreeValues.cpp index 7c5324f..404015b 100644 --- a/lib/obp60task/PageThreeValues.cpp +++ b/lib/obp60task/PageThreeValues.cpp @@ -80,7 +80,7 @@ class PageThreeValues : public Page //*********************************************************** /// Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + displaySetPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update // ############### Value 1 ################ diff --git a/lib/obp60task/PageTwoValues.cpp b/lib/obp60task/PageTwoValues.cpp index 5990656..4c10c00 100644 --- a/lib/obp60task/PageTwoValues.cpp +++ b/lib/obp60task/PageTwoValues.cpp @@ -283,7 +283,7 @@ public: // Draw page //*********************************************************** - getdisplay().setPartialWindow(0, 0, width, height); // Set partial update + displaySetPartialWindow(0, 0, width, height); // Set partial update 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 diff --git a/lib/obp60task/PageVoltage.cpp b/lib/obp60task/PageVoltage.cpp index 6681b5e..adf3d28 100644 --- a/lib/obp60task/PageVoltage.cpp +++ b/lib/obp60task/PageVoltage.cpp @@ -193,7 +193,7 @@ public: //*********************************************************** // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + displaySetPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update if (mode == 'D') { // Display mode digital diff --git a/lib/obp60task/PageWhite.cpp b/lib/obp60task/PageWhite.cpp index 4791400..bfdb9d8 100644 --- a/lib/obp60task/PageWhite.cpp +++ b/lib/obp60task/PageWhite.cpp @@ -63,7 +63,7 @@ public: if (mode == 'W') { getdisplay().setFullWindow(); } else { - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + displaySetPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update } if (mode == 'L') { diff --git a/lib/obp60task/PageWind.cpp b/lib/obp60task/PageWind.cpp index 242a365..084651b 100644 --- a/lib/obp60task/PageWind.cpp +++ b/lib/obp60task/PageWind.cpp @@ -358,7 +358,7 @@ public: //*********************************************************** // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + displaySetPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update getdisplay().setTextColor(commonData->fgcolor); diff --git a/lib/obp60task/PageWindPlot.cpp b/lib/obp60task/PageWindPlot.cpp index da948c2..c9ff5d5 100644 --- a/lib/obp60task/PageWindPlot.cpp +++ b/lib/obp60task/PageWindPlot.cpp @@ -221,7 +221,7 @@ public: //*********************************************************** // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, width, height); // Set partial update + displaySetPartialWindow(0, 0, width, height); // Set partial update getdisplay().setTextColor(commonData->fgcolor); if (chrtMode == DIRECTION) { diff --git a/lib/obp60task/PageWindRose.cpp b/lib/obp60task/PageWindRose.cpp index 427e64b..75f9ab1 100644 --- a/lib/obp60task/PageWindRose.cpp +++ b/lib/obp60task/PageWindRose.cpp @@ -140,7 +140,7 @@ public: //*********************************************************** // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + displaySetPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update getdisplay().setTextColor(commonData->fgcolor); @@ -260,7 +260,7 @@ public: // 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 + displayGetTextBounds(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_Bold8pt8b); diff --git a/lib/obp60task/PageWindRoseFlex.cpp b/lib/obp60task/PageWindRoseFlex.cpp index d3526e0..2b71328 100644 --- a/lib/obp60task/PageWindRoseFlex.cpp +++ b/lib/obp60task/PageWindRoseFlex.cpp @@ -200,7 +200,7 @@ public: //*********************************************************** // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + displaySetPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update getdisplay().setTextColor(commonData->fgcolor); @@ -314,7 +314,7 @@ public: // 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 + displayGetTextBounds(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_Bold8pt8b); diff --git a/lib/obp60task/PageXTETrack.cpp b/lib/obp60task/PageXTETrack.cpp index 0b513df..ce404a9 100644 --- a/lib/obp60task/PageXTETrack.cpp +++ b/lib/obp60task/PageXTETrack.cpp @@ -89,7 +89,7 @@ class PageXTETrack : public Page //*********************************************************** // Set display in partial refresh mode - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + displaySetPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update getdisplay().setTextColor(commonData->fgcolor); @@ -112,25 +112,25 @@ class PageXTETrack : public Page GwApi::BoatValue *bv_xte = pageData.values[0]; // XTE String sval_xte = formatValue(bv_xte, *commonData).svalue; - getdisplay().getTextBounds(sval_xte, 0, 0, &x, &y, &w, &h); + displayGetTextBounds(sval_xte, 0, 0, &x, &y, &w, &h); getdisplay().setCursor(160-w, 170); getdisplay().print(sval_xte); GwApi::BoatValue *bv_cog = pageData.values[1]; // COG String sval_cog = formatValue(bv_cog, *commonData).svalue; - getdisplay().getTextBounds(sval_cog, 0, 0, &x, &y, &w, &h); + displayGetTextBounds(sval_cog, 0, 0, &x, &y, &w, &h); getdisplay().setCursor(360-w, 170); getdisplay().print(sval_cog); GwApi::BoatValue *bv_dtw = pageData.values[2]; // DTW String sval_dtw = formatValue(bv_dtw, *commonData).svalue; - getdisplay().getTextBounds(sval_dtw, 0, 0, &x, &y, &w, &h); + displayGetTextBounds(sval_dtw, 0, 0, &x, &y, &w, &h); getdisplay().setCursor(160-w, 257); getdisplay().print(sval_dtw); GwApi::BoatValue *bv_btw = pageData.values[3]; // BTW String sval_btw = formatValue(bv_btw, *commonData).svalue; - getdisplay().getTextBounds(sval_btw, 0, 0, &x, &y, &w, &h); + displayGetTextBounds(sval_btw, 0, 0, &x, &y, &w, &h); getdisplay().setCursor(360-w, 257); getdisplay().print(sval_btw); @@ -149,7 +149,7 @@ class PageXTETrack : public Page } getdisplay().setFont(&Ubuntu_Bold10pt8b); - getdisplay().getTextBounds(sval_wpname, 0, 150, &x, &y, &w, &h); + displayGetTextBounds(sval_wpname, 0, 150, &x, &y, &w, &h); // TODO if text don't fix use smaller font size. // if smallest size does not fit use 2 lines // last resort: clip with ellipsis diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index 84bc572..93fbf9b 100644 --- a/lib/obp60task/obp60task.cpp +++ b/lib/obp60task/obp60task.cpp @@ -284,8 +284,10 @@ void underVoltageError(CommonData &common) { getdisplay().setFont(&Ubuntu_Bold8pt8b); getdisplay().setCursor(65, 175); getdisplay().print("Charge battery and restart system"); - getdisplay().nextPage(); // Partial update + displayNextPage(); // Partial update + #ifndef DISPLAY_ST7796 getdisplay().powerOff(); // Display power off + #endif setPortPin(OBP_POWER_EPD, false); // Power off ePaper display setPortPin(OBP_POWER_SD, false); // Power off SD card #else @@ -295,7 +297,7 @@ void underVoltageError(CommonData &common) { buzzer(TONE4, 20); // Buzzer tone 4kHz 20ms setPortPin(OBP_POWER_50, false); // Power rail 5.0V Off // Shutdown EInk display - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + displaySetPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update getdisplay().fillScreen(common.bgcolor);// Clear screen getdisplay().setTextColor(common.fgcolor); getdisplay().setFont(&Ubuntu_Bold20pt8b); @@ -304,8 +306,10 @@ void underVoltageError(CommonData &common) { getdisplay().setFont(&Ubuntu_Bold8pt8b); getdisplay().setCursor(65, 175); getdisplay().print("To wake up repower system"); - getdisplay().nextPage(); // Partial update + displayNextPage(); // Partial update + #ifndef DISPLAY_ST7796 getdisplay().powerOff(); // Display power off + #endif #endif while (true) { esp_deep_sleep_start(); // Deep Sleep without wakeup. Wakeup only after power cycle (restart). @@ -375,36 +379,38 @@ void OBP60Task(GwApi *api){ #ifdef DISPLAY_GDEY042T81 getdisplay().init(115200, true, 2, false); // Init for Waveshare boards with "clever" reset circuit, 2ms reset pulse + #elif defined DISPLAY_ST7796 + getdisplay().init(); // Init for ST7796 TFT LCD #else getdisplay().init(115200); // Init for normal displays #endif getdisplay().setRotation(0); // Set display orientation (horizontal) - getdisplay().setFullWindow(); // Set full Refresh - getdisplay().firstPage(); // set first page + displaySetFullWindow(); // Set full Refresh (E-Ink only) + displayFirstPage(); // set first page getdisplay().fillScreen(commonData.bgcolor); getdisplay().setTextColor(commonData.fgcolor); - getdisplay().nextPage(); // Full Refresh - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + displayNextPage(); // Full Refresh + displaySetPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update (E-Ink only) getdisplay().fillScreen(commonData.bgcolor); - getdisplay().nextPage(); // Fast Refresh - getdisplay().nextPage(); // Fast Refresh + displayNextPage(); // Fast Refresh + displayNextPage(); // Fast Refresh if(String(displaymode) == "Logo + QR Code" || String(displaymode) == "Logo"){ getdisplay().fillScreen(commonData.bgcolor); getdisplay().drawBitmap(0, 0, gImage_Logo_OBP_400x300_sw, getdisplay().width(), getdisplay().height(), commonData.fgcolor); // Draw start logo - getdisplay().nextPage(); // Fast Refresh - getdisplay().nextPage(); // Fast Refresh + displayNextPage(); // Fast Refresh + displayNextPage(); // Fast Refresh delay(SHOW_TIME); // Logo show time if(String(displaymode) == "Logo + QR Code"){ getdisplay().fillScreen(commonData.bgcolor); qrWiFi(systemname, wifipass, commonData.fgcolor, commonData.bgcolor); // Show QR code for WiFi connection - getdisplay().nextPage(); // Fast Refresh - getdisplay().nextPage(); // Fast Refresh + displayNextPage(); // Fast Refresh + displayNextPage(); // Fast Refresh delay(SHOW_TIME); // QR code show time } getdisplay().fillScreen(commonData.bgcolor); - getdisplay().nextPage(); // Fast Refresh - getdisplay().nextPage(); // Fast Refresh + displayNextPage(); // Fast Refresh + displayNextPage(); // Fast Refresh } // Init pages @@ -722,9 +728,12 @@ void OBP60Task(GwApi *api){ if(millis() > starttime4 + 8000 && delayedDisplayUpdate == true){ starttime1 = millis(); starttime2 = millis(); - getdisplay().setFullWindow(); // Set full update + displaySetFullWindow(); // Set full update + #ifdef DISPLAY_ST7796 + // TFT LCD doesn't need refresh operations + #else if(fastrefresh == "true"){ - getdisplay().nextPage(); // Full update + displayNextPage(); // Full update } else{ getdisplay().fillScreen(commonData.fgcolor); // Clear display @@ -734,13 +743,14 @@ void OBP60Task(GwApi *api){ #else getdisplay().init(115200); // Init for normal displays #endif - getdisplay().firstPage(); // Full update - getdisplay().nextPage(); // Full update + displayFirstPage(); // Full update + displayNextPage(); // Full update // getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update // getdisplay().fillScreen(commonData.bgcolor); // Clear display // getdisplay().nextPage(); // Partial update // getdisplay().nextPage(); // Partial update } + #endif delayedDisplayUpdate = false; } @@ -750,9 +760,12 @@ void OBP60Task(GwApi *api){ starttime1 = millis(); starttime2 = millis(); LOG_DEBUG(GwLog::DEBUG,"E-Ink full refresh first 5 min"); - getdisplay().setFullWindow(); // Set full update + displaySetFullWindow(); // Set full update + #ifdef DISPLAY_ST7796 + // TFT LCD doesn't need refresh operations + #else if(fastrefresh == "true"){ - getdisplay().nextPage(); // Full update + displayNextPage(); // Full update } else{ getdisplay().fillScreen(commonData.fgcolor); // Clear display @@ -762,19 +775,23 @@ void OBP60Task(GwApi *api){ #else getdisplay().init(115200); // Init for normal displays #endif - getdisplay().firstPage(); // Full update - getdisplay().nextPage(); // Full update + displayFirstPage(); // Full update + displayNextPage(); // Full update // getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update // getdisplay().fillScreen(commonData.bgcolor); // Clear display // getdisplay().nextPage(); // Partial update // getdisplay().nextPage(); // Partial update } + #endif } // Subtask E-Ink full refresh if(millis() > starttime2 + fullrefreshtime * 60 * 1000){ starttime2 = millis(); LOG_DEBUG(GwLog::DEBUG,"E-Ink full refresh"); + #ifdef DISPLAY_ST7796 + // TFT LCD: no special refresh + #else getdisplay().setFullWindow(); // Set full update if(fastrefresh == "true"){ getdisplay().nextPage(); // Full update @@ -794,6 +811,7 @@ void OBP60Task(GwApi *api){ // getdisplay().nextPage(); // Partial update // getdisplay().nextPage(); // Partial update } + #endif } // Refresh display data, default all 1s @@ -842,13 +860,13 @@ void OBP60Task(GwApi *api){ if (currentPage == NULL){ LOG_DEBUG(GwLog::ERROR,"page number %d not found", pageNumber); // Error handling for missing page - getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + displaySetPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update getdisplay().fillScreen(commonData.bgcolor); // Clear display getdisplay().drawXBitmap(200 - unknown_width / 2, 150 - unknown_height / 2, unknown_bits, unknown_width, unknown_height, commonData.fgcolor); getdisplay().setCursor(140, 250); getdisplay().setFont(&Atari16px); getdisplay().print("Here be dragons!"); - getdisplay().nextPage(); // Partial update (fast) + displayNextPage(); // Partial update (fast) } else{ if (lastPage != pageNumber){ @@ -871,10 +889,12 @@ void OBP60Task(GwApi *api){ displayAlarm(commonData); } if (ret & PAGE_UPDATE) { - getdisplay().nextPage(); // Partial update (fast) + displayNextPage(); // Partial update (fast) } if (ret & PAGE_HIBERNATE) { + #ifndef DISPLAY_ST7796 getdisplay().hibernate(); + #endif } } diff --git a/lib/obp60task/platformio.ini b/lib/obp60task/platformio.ini index 03a5463..970869f 100644 --- a/lib/obp60task/platformio.ini +++ b/lib/obp60task/platformio.ini @@ -45,6 +45,7 @@ lib_deps = milesburton/DallasTemperature@3.11.0 signetica/SunRise@2.0.2 adafruit/Adafruit FRAM I2C@2.0.3 + lovyan03/LovyanGFX@^1.2.19 build_flags= #https://thingpulse.com/usb-settings-for-logging-with-the-esp32-s3-in-platformio/?srsltid=AfmBOopGskbkr4GoeVkNlFaZXe_zXkLceKF6Rn-tmoXABCeAR2vWsdHL # -D CORE_DEBUG_LEVEL=1 #Debug level for CPU core via CDC (serial device) @@ -54,9 +55,10 @@ build_flags= # -D HARDWARE_V20 #OBP60 hardware revision V2.0 -D HARDWARE_V21 #OBP60 hardware revision V2.1 # -D DISPLAY_GDEW042T2 #old E-Ink display from GoodDisplay (Waveshare), R10 0.47 ohm - very good - -D DISPLAY_GDEY042T81 #new E-Ink display from GoodDisplay (Waveshare), R10 2.2 ohm - good (contast lost by shunshine) +# -D DISPLAY_GDEY042T81 #new E-Ink display from GoodDisplay (Waveshare), R10 2.2 ohm - good (contast lost by shunshine) # -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_ST7796 #ST7796 TFT LCD display (480x320 color display) # -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} @@ -101,11 +103,13 @@ lib_deps = milesburton/DallasTemperature@3.11.0 signetica/SunRise@2.0.2 adafruit/Adafruit FRAM I2C@2.0.3 + lovyan03/LovyanGFX@^1.2.19 build_flags= -D DISABLE_DIAGNOSTIC_OUTPUT #Disable diagnostic output for GxEPD2 lib -D BOARD_OBP40S3 #Board OBP40 with ESP32S3 -D HARDWARE_V10 #OBP40 hardware revision V1.0 SKU:DIE07300S V1.1 (CrowPanel 4.2) - -D DISPLAY_GDEY042T81 #new E-Ink display from Good Display (Waveshare), R10 2.2 ohm - good (contast lost by shunshine) +# -D DISPLAY_GDEY042T81 #new E-Ink display from Good Display (Waveshare), R10 2.2 ohm - good (contast lost by shunshine) + -D DISPLAY_ST7796 #ST7796 TFT LCD display (480x320 color display) #-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 From 8a9f87a8f4941a8fb7f73d4bba1203b002bfc3cc Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Sat, 28 Feb 2026 00:05:54 +0100 Subject: [PATCH 12/75] Fix platformio.ini --- lib/obp60task/platformio.ini | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/obp60task/platformio.ini b/lib/obp60task/platformio.ini index 970869f..1abd66b 100644 --- a/lib/obp60task/platformio.ini +++ b/lib/obp60task/platformio.ini @@ -55,10 +55,10 @@ build_flags= # -D HARDWARE_V20 #OBP60 hardware revision V2.0 -D HARDWARE_V21 #OBP60 hardware revision V2.1 # -D DISPLAY_GDEW042T2 #old E-Ink display from GoodDisplay (Waveshare), R10 0.47 ohm - very good -# -D DISPLAY_GDEY042T81 #new E-Ink display from GoodDisplay (Waveshare), R10 2.2 ohm - good (contast lost by shunshine) + -D DISPLAY_GDEY042T81 #new E-Ink display from GoodDisplay (Waveshare), R10 2.2 ohm - good (contast lost by shunshine) # -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_ST7796 #ST7796 TFT LCD display (480x320 color display) +# -D DISPLAY_ST7796 #ST7796 TFT LCD display (480x320 color display) # -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} @@ -103,13 +103,11 @@ lib_deps = milesburton/DallasTemperature@3.11.0 signetica/SunRise@2.0.2 adafruit/Adafruit FRAM I2C@2.0.3 - lovyan03/LovyanGFX@^1.2.19 build_flags= -D DISABLE_DIAGNOSTIC_OUTPUT #Disable diagnostic output for GxEPD2 lib -D BOARD_OBP40S3 #Board OBP40 with ESP32S3 -D HARDWARE_V10 #OBP40 hardware revision V1.0 SKU:DIE07300S V1.1 (CrowPanel 4.2) -# -D DISPLAY_GDEY042T81 #new E-Ink display from Good Display (Waveshare), R10 2.2 ohm - good (contast lost by shunshine) - -D DISPLAY_ST7796 #ST7796 TFT LCD display (480x320 color display) + -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 From 9ac0b4a2bbe165f2c44ad32ddd7afe70e1ceede5 Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sat, 28 Feb 2026 14:28:09 +0000 Subject: [PATCH 13/75] Rotate display an showthe content in the middle of creen --- lib/obp60task/OBP60Extensions.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index 47767f3..9eab0f9 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -106,9 +106,9 @@ public: cfg.pin_busy = -1; cfg.panel_width = 480; cfg.panel_height = 320; - cfg.offset_x = 0; - cfg.offset_y = 0; - cfg.offset_rotation = 0; + cfg.offset_x = 40; + cfg.offset_y = 10; + cfg.offset_rotation = 3; cfg.dummy_read_pixel = 8; cfg.dummy_read_bits = 1; cfg.memory_width = 480; From 415c155949c41686dda2d4340fc407555ddb532e Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Sat, 28 Feb 2026 16:18:14 +0100 Subject: [PATCH 14/75] Fix for display output to mechanical center adjusted --- lib/obp60task/OBP60Extensions.h | 14 +++++++------- lib/obp60task/OBP60Hardware.h | 2 +- lib/obp60task/platformio.ini | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index 9eab0f9..4599aa3 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -104,15 +104,15 @@ public: cfg.pin_cs = OBP_SPI_CS; cfg.pin_rst = OBP_SPI_RST; cfg.pin_busy = -1; - cfg.panel_width = 480; - cfg.panel_height = 320; - cfg.offset_x = 40; - cfg.offset_y = 10; - cfg.offset_rotation = 3; + cfg.panel_width = 320; // Native width resolution + cfg.panel_height = 480; // Native hight resolution + cfg.offset_x = 10; // Display output 400x300 pix to housing center adjusted + cfg.offset_y = -20; // Display output 400x300 pix to housing center adjusted + cfg.offset_rotation = 3; // Rotate display content conter clock wise 90 deg cfg.dummy_read_pixel = 8; cfg.dummy_read_bits = 1; - cfg.memory_width = 480; - cfg.memory_height = 320; + cfg.memory_width = 320; + cfg.memory_height = 480; // cfg.pwm_control not available in this LovyanGFX version cfg.invert = false; cfg.rgb_order = false; diff --git a/lib/obp60task/OBP60Hardware.h b/lib/obp60task/OBP60Hardware.h index 6d038d3..5aec5ac 100644 --- a/lib/obp60task/OBP60Hardware.h +++ b/lib/obp60task/OBP60Hardware.h @@ -34,7 +34,7 @@ #define PCF8574_I2C_ADDR1 0x20 // First digital out module // FRAM (e.g. MB85RC256V) #define FRAM_I2C_ADDR 0x50 - // SPI (E-Ink display, Extern Bus) + // SPI (E-paper display, TFT display Extern Bus) #define OBP_SPI_CS 39 #define OBP_SPI_DC 40 #define OBP_SPI_RST 41 diff --git a/lib/obp60task/platformio.ini b/lib/obp60task/platformio.ini index 1abd66b..71fbcbc 100644 --- a/lib/obp60task/platformio.ini +++ b/lib/obp60task/platformio.ini @@ -55,10 +55,10 @@ build_flags= # -D HARDWARE_V20 #OBP60 hardware revision V2.0 -D HARDWARE_V21 #OBP60 hardware revision V2.1 # -D DISPLAY_GDEW042T2 #old E-Ink display from GoodDisplay (Waveshare), R10 0.47 ohm - very good - -D DISPLAY_GDEY042T81 #new E-Ink display from GoodDisplay (Waveshare), R10 2.2 ohm - good (contast lost by shunshine) +# -D DISPLAY_GDEY042T81 #new E-Ink display from GoodDisplay (Waveshare), R10 2.2 ohm - good (contast lost by shunshine) # -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_ST7796 #ST7796 TFT LCD display (480x320 color display) + -D DISPLAY_ST7796 #ST7796 TFT LCD display (480x320 color display) # -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} From 2d6127912b0d4c312c03bc2a7bd2dc95c2f1bbca Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sat, 28 Feb 2026 15:27:39 +0000 Subject: [PATCH 15/75] Fix image problem with 1Bit images --- lib/obp60task/OBP60Extensions.h | 28 ++++++++++++++++++++++++++++ lib/obp60task/obp60task.cpp | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index 4599aa3..559c79c 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -147,6 +147,34 @@ LGFX & getdisplay(); #define PAGE_UPDATE 1 // page wants display to update #define PAGE_HIBERNATE 2 // page wants displey to hibernate +// Draw monochrome bitmap on both E-Ink and TFT displays +// Konvertiert 1-Bit Bilder automatisch zu RGB565 für Farbdisplays +inline void drawMonochromeBitmap( + int16_t x, int16_t y, + const uint8_t *bitmap, + int16_t w, int16_t h, + uint16_t color) { + + #ifdef DISPLAY_ST7796 + // Für RGB565 TFT: Konvertierung von 1-Bit zu Pixel-Zeichnung + for (int row = 0; row < h; row++) { + for (int col = 0; col < w; col++) { + int byteIdx = (row * ((w + 7) / 8)) + (col / 8); + int bitIdx = col % 8; + uint8_t byte = bitmap[byteIdx]; + + // LSB-first format (Adafruit standard) + if (byte & (1 << bitIdx)) { + getdisplay().drawPixel(x + col, y + row, color); + } + } + } + #else + // Für E-Ink Displays: direkt drawBitmap verwenden + getdisplay().drawBitmap(x, y, bitmap, w, h, color); + #endif +} + // Display wrapper functions for E-Ink/TFT compatibility inline void displayFirstPage() { #ifdef DISPLAY_ST7796 diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index 93fbf9b..2293865 100644 --- a/lib/obp60task/obp60task.cpp +++ b/lib/obp60task/obp60task.cpp @@ -397,7 +397,7 @@ void OBP60Task(GwApi *api){ displayNextPage(); // Fast Refresh if(String(displaymode) == "Logo + QR Code" || String(displaymode) == "Logo"){ getdisplay().fillScreen(commonData.bgcolor); - getdisplay().drawBitmap(0, 0, gImage_Logo_OBP_400x300_sw, getdisplay().width(), getdisplay().height(), commonData.fgcolor); // Draw start logo + drawMonochromeBitmap(0, 0, gImage_Logo_OBP_400x300_sw, getdisplay().width(), getdisplay().height(), commonData.fgcolor); // Draw start logo displayNextPage(); // Fast Refresh displayNextPage(); // Fast Refresh delay(SHOW_TIME); // Logo show time From 7434601382f634fdb688f1122567d7ba9c7146ec Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sat, 28 Feb 2026 15:55:52 +0000 Subject: [PATCH 16/75] Fix image converter --- lib/obp60task/OBP60Extensions.h | 47 +++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index 559c79c..43465a1 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -148,30 +148,43 @@ LGFX & getdisplay(); #define PAGE_HIBERNATE 2 // page wants displey to hibernate // Draw monochrome bitmap on both E-Ink and TFT displays -// Konvertiert 1-Bit Bilder automatisch zu RGB565 für Farbdisplays +// supports various packing and bit orders; optional runtime conversion for TFT inline void drawMonochromeBitmap( - int16_t x, int16_t y, - const uint8_t *bitmap, + int16_t x, int16_t y, + const uint8_t *bmp, int16_t w, int16_t h, - uint16_t color) { - + uint16_t color, + bool vertical=false, // true: bytes run vertically (each byte 8 pixels down) + bool lsbFirst=false) // true: least significant bit = left/top pixel +{ #ifdef DISPLAY_ST7796 - // Für RGB565 TFT: Konvertierung von 1-Bit zu Pixel-Zeichnung - for (int row = 0; row < h; row++) { - for (int col = 0; col < w; col++) { - int byteIdx = (row * ((w + 7) / 8)) + (col / 8); - int bitIdx = col % 8; - uint8_t byte = bitmap[byteIdx]; - - // LSB-first format (Adafruit standard) - if (byte & (1 << bitIdx)) { - getdisplay().drawPixel(x + col, y + row, color); + // TFT converts per‑pixel + for (int yy = 0; yy < h; yy++) { + for (int xx = 0; xx < w; xx++) { + int byteIdx; + int bitIdx; + if (vertical) { + byteIdx = xx * ((h + 7) / 8) + (yy / 8); + bitIdx = yy % 8; + } else { + byteIdx = yy * ((w + 7) / 8) + (xx / 8); + bitIdx = xx % 8; + } + uint8_t b = bmp[byteIdx]; + bool pix; + if (lsbFirst) { + pix = b & (1 << bitIdx); + } else { + pix = b & (1 << (7 - bitIdx)); + } + if (pix) { + getdisplay().drawPixel(x + xx, y + yy, color); } } } #else - // Für E-Ink Displays: direkt drawBitmap verwenden - getdisplay().drawBitmap(x, y, bitmap, w, h, color); + // E‑Paper: just hand over to driver (expects MSB‑first horizontal) + getdisplay().drawBitmap(x, y, bmp, w, h, color); #endif } From 42c147ac575b686d6415be246a617c65b82eafe7 Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sat, 28 Feb 2026 16:13:15 +0000 Subject: [PATCH 17/75] Fix image converter --- lib/obp60task/OBP60Extensions.h | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index 43465a1..17a2a2c 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -155,20 +155,26 @@ inline void drawMonochromeBitmap( int16_t w, int16_t h, uint16_t color, bool vertical=false, // true: bytes run vertically (each byte 8 pixels down) - bool lsbFirst=false) // true: least significant bit = left/top pixel + bool lsbFirst=false, // true: least significant bit = left/top pixel + bool mirrorX=false) // true: bytes run right-to-left within each row { #ifdef DISPLAY_ST7796 // TFT converts per‑pixel + int bytesPerRow = (w + 7) / 8; for (int yy = 0; yy < h; yy++) { for (int xx = 0; xx < w; xx++) { int byteIdx; int bitIdx; if (vertical) { - byteIdx = xx * ((h + 7) / 8) + (yy / 8); + // vertical packing: column-major bytes + int col = mirrorX ? (w - 1 - xx) : xx; + byteIdx = col * ((h + 7) / 8) + (yy / 8); bitIdx = yy % 8; } else { - byteIdx = yy * ((w + 7) / 8) + (xx / 8); - bitIdx = xx % 8; + // horizontal packing: row-major bytes + int col = mirrorX ? (w - 1 - xx) : xx; + byteIdx = yy * bytesPerRow + (col / 8); + bitIdx = col % 8; } uint8_t b = bmp[byteIdx]; bool pix; From 0b3b5f387bd30ec1ad0de77ceeecf0b74dacaca9 Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sat, 28 Feb 2026 16:23:43 +0000 Subject: [PATCH 18/75] Fix image converter --- lib/obp60task/obp60task.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index 2293865..77f31ba 100644 --- a/lib/obp60task/obp60task.cpp +++ b/lib/obp60task/obp60task.cpp @@ -397,7 +397,16 @@ void OBP60Task(GwApi *api){ displayNextPage(); // Fast Refresh if(String(displaymode) == "Logo + QR Code" || String(displaymode) == "Logo"){ getdisplay().fillScreen(commonData.bgcolor); - drawMonochromeBitmap(0, 0, gImage_Logo_OBP_400x300_sw, getdisplay().width(), getdisplay().height(), commonData.fgcolor); // Draw start logo + // logo is 400×300 pixels regardless of physical display resolution + const int LOGO_W = 400; + const int LOGO_H = 300; + // bitmap is stored row‑wise LSB‑oriented (adfruit generator), no vertical packing + drawMonochromeBitmap(0, 0, gImage_Logo_OBP_400x300_sw, + LOGO_W, LOGO_H, + commonData.fgcolor, + /*vertical=*/false, + /*lsbFirst=*/false, + /*mirrorX=*/false); displayNextPage(); // Fast Refresh displayNextPage(); // Fast Refresh delay(SHOW_TIME); // Logo show time From 7e35c250cd13b426c1e8533856319c89d2d343d6 Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sat, 28 Feb 2026 16:42:37 +0000 Subject: [PATCH 19/75] Fix image converter --- lib/obp60task/OBP60Extensions.h | 27 +++++++++++++++++++++++++++ lib/obp60task/obp60task.cpp | 13 +++---------- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index 17a2a2c..76a47df 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -194,7 +194,34 @@ inline void drawMonochromeBitmap( #endif } +// helper for built-in OBP logo (400×300 mono bitmap) +inline void drawOBPLogo(int16_t x, int16_t y, uint16_t color) { + constexpr int LOGO_W = 400; + constexpr int LOGO_H = 300; + drawMonochromeBitmap(x, y, + gImage_Logo_OBP_400x300_sw, + LOGO_W, LOGO_H, + color, + /*vertical=*/false, + /*lsbFirst=*/false, + /*mirrorX=*/false); +} + // Display wrapper functions for E-Ink/TFT compatibility + +// generic bitmap draw that accepts 1‑bit data; TFT version +// forwards to drawMonochromeBitmap whereas EPD uses native drawBitmap +inline void displayDrawBitmap(int16_t x, int16_t y, + const uint8_t *bmp, + int16_t w, int16_t h, + uint16_t color) { + #ifdef DISPLAY_ST7796 + drawMonochromeBitmap(x, y, bmp, w, h, color); + #else + getdisplay().drawBitmap(x, y, bmp, w, h, color); + #endif +} + inline void displayFirstPage() { #ifdef DISPLAY_ST7796 // TFT LCD doesn't need firstPage() diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index 77f31ba..fba82ac 100644 --- a/lib/obp60task/obp60task.cpp +++ b/lib/obp60task/obp60task.cpp @@ -397,16 +397,9 @@ void OBP60Task(GwApi *api){ displayNextPage(); // Fast Refresh if(String(displaymode) == "Logo + QR Code" || String(displaymode) == "Logo"){ getdisplay().fillScreen(commonData.bgcolor); - // logo is 400×300 pixels regardless of physical display resolution - const int LOGO_W = 400; - const int LOGO_H = 300; - // bitmap is stored row‑wise LSB‑oriented (adfruit generator), no vertical packing - drawMonochromeBitmap(0, 0, gImage_Logo_OBP_400x300_sw, - LOGO_W, LOGO_H, - commonData.fgcolor, - /*vertical=*/false, - /*lsbFirst=*/false, - /*mirrorX=*/false); + // draw the fixed-size logo via generic display wrapper + displayDrawBitmap(0, 0, gImage_Logo_OBP_400x300_sw, + 400, 300, commonData.fgcolor); displayNextPage(); // Fast Refresh displayNextPage(); // Fast Refresh delay(SHOW_TIME); // Logo show time From 1aac8fdcd94356a316552b2d36698314de9ec521 Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sat, 28 Feb 2026 16:56:00 +0000 Subject: [PATCH 20/75] Fix image converter --- lib/obp60task/OBP60Extensions.h | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index 76a47df..3a91b0a 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -194,18 +194,6 @@ inline void drawMonochromeBitmap( #endif } -// helper for built-in OBP logo (400×300 mono bitmap) -inline void drawOBPLogo(int16_t x, int16_t y, uint16_t color) { - constexpr int LOGO_W = 400; - constexpr int LOGO_H = 300; - drawMonochromeBitmap(x, y, - gImage_Logo_OBP_400x300_sw, - LOGO_W, LOGO_H, - color, - /*vertical=*/false, - /*lsbFirst=*/false, - /*mirrorX=*/false); -} // Display wrapper functions for E-Ink/TFT compatibility From 253aaa5b2eb40bf88d9ff544d9d1400f8cb2c27d Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sat, 28 Feb 2026 17:04:31 +0000 Subject: [PATCH 21/75] Using new wrepper funktion in PageNavigation --- 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 6a4bf71..03729e1 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -431,7 +431,7 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map lostCounter = 0; // Show image (navigation map) - getdisplay().drawBitmap(0, 25, imageData, imgWidth, imgHeight, commonData->fgcolor); + displayDrawBitmap(0, 25, imageData, imgWidth, imgHeight, commonData->fgcolor); // Clean PSRAM free(b64); @@ -454,7 +454,7 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map // Show backup image (backup navigation map) if (hasImageBackup) { - getdisplay().drawBitmap(0, 25, imageBackupData, imageBackupWidth, imageBackupHeight, commonData->fgcolor); + displayDrawBitmap(0, 25, imageBackupData, imageBackupWidth, imageBackupHeight, commonData->fgcolor); } // Show connection lost info when 5 page refreshes has a connection lost to the map server From 270642ad7ce08ebfa356351f30fe826398f1e870 Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sat, 28 Feb 2026 18:06:40 +0000 Subject: [PATCH 22/75] Fix for font using --- lib/obp60task/OBP60Extensions.h | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index 3a91b0a..f3ed0d7 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -130,12 +130,27 @@ public: } // compatibility helpers -------------------------------------------------- - // Adafruit GFX fonts support: ignore on TFT, use base on E-Ink - void setFont(const GFXfont *font) { (void)font; } + using lgfx::LGFX_Device::setFont; + // Adafruit GFX fonts support on TFT via LovyanGFX bridge + void setFont(const GFXfont *font) { + if (font == nullptr) { + lgfx::LGFX_Device::setFont(nullptr); + return; + } + _adfFontBridge = lgfx::GFXfont( + const_cast(font->bitmap), + reinterpret_cast(const_cast(font->glyph)), + font->first, + font->last, + font->yAdvance + ); + lgfx::LGFX_Device::setFont(&_adfFontBridge); + } // E-Ink interface compatibility void setFullWindow() { /* no-op on TFT */ } private: + lgfx::GFXfont _adfFontBridge { nullptr, nullptr, 0, 0, 0 }; lgfx::Panel_ST7796 _panel_instance; }; From b29d89525af4963a398319f413dc8ef7aa0ee60a Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sat, 28 Feb 2026 18:16:03 +0000 Subject: [PATCH 23/75] Fix boot loop --- lib/obp60task/OBP60Extensions.h | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index f3ed0d7..7d2ae8c 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -83,7 +83,6 @@ GxEPD2_BW & getdisplay(); class LGFX : public lgfx::LGFX_Device { public: lgfx::Bus_SPI _bus_instance; - lgfx::Light_PWM _light_instance; LGFX(void) { { @@ -120,12 +119,8 @@ public: cfg.bus_shared = true; _panel_instance.config(cfg); } - { - auto cfg = _light_instance.config(); - cfg.pin_bl = -1; - _light_instance.config(cfg); - _panel_instance.setLight(&_light_instance); - } + // No dedicated TFT PWM backlight pin configured on this board. + // Keep backlight handling outside LovyanGFX to avoid LEDC init on invalid GPIO. setPanel(&_panel_instance); } From 5a30d7d022986349b3526d56e81b21c120ce9f52 Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sat, 28 Feb 2026 18:23:23 +0000 Subject: [PATCH 24/75] Fix boot loop reimplement font implementation --- lib/obp60task/OBP60Extensions.h | 39 ++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index 7d2ae8c..f704c40 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -132,9 +132,44 @@ public: lgfx::LGFX_Device::setFont(nullptr); return; } + + if (font->glyph == nullptr || font->bitmap == nullptr || font->last < font->first) { + lgfx::LGFX_Device::setFont(nullptr); + return; + } + + const uint16_t glyphCount = static_cast(font->last - font->first + 1); + if (glyphCount == 0) { + lgfx::LGFX_Device::setFont(nullptr); + return; + } + + if (_adfGlyphCount != glyphCount || _adfGlyphBridge == nullptr) { + if (_adfGlyphBridge != nullptr) { + free(_adfGlyphBridge); + _adfGlyphBridge = nullptr; + _adfGlyphCount = 0; + } + _adfGlyphBridge = static_cast(malloc(sizeof(lgfx::GFXglyph) * glyphCount)); + if (_adfGlyphBridge == nullptr) { + lgfx::LGFX_Device::setFont(nullptr); + return; + } + _adfGlyphCount = glyphCount; + } + + for (uint16_t index = 0; index < glyphCount; ++index) { + _adfGlyphBridge[index].bitmapOffset = font->glyph[index].bitmapOffset; + _adfGlyphBridge[index].width = font->glyph[index].width; + _adfGlyphBridge[index].height = font->glyph[index].height; + _adfGlyphBridge[index].xAdvance = font->glyph[index].xAdvance; + _adfGlyphBridge[index].xOffset = font->glyph[index].xOffset; + _adfGlyphBridge[index].yOffset = font->glyph[index].yOffset; + } + _adfFontBridge = lgfx::GFXfont( const_cast(font->bitmap), - reinterpret_cast(const_cast(font->glyph)), + _adfGlyphBridge, font->first, font->last, font->yAdvance @@ -146,6 +181,8 @@ public: private: lgfx::GFXfont _adfFontBridge { nullptr, nullptr, 0, 0, 0 }; + lgfx::GFXglyph* _adfGlyphBridge = nullptr; + uint16_t _adfGlyphCount = 0; lgfx::Panel_ST7796 _panel_instance; }; From 39c4e7fb3290cfc34044bd1153335ede118c9f5e Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sat, 28 Feb 2026 18:33:32 +0000 Subject: [PATCH 25/75] Fix base line logic for fonts --- lib/obp60task/OBP60Extensions.cpp | 39 ++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/lib/obp60task/OBP60Extensions.cpp b/lib/obp60task/OBP60Extensions.cpp index 6dbcf05..39d997c 100644 --- a/lib/obp60task/OBP60Extensions.cpp +++ b/lib/obp60task/OBP60Extensions.cpp @@ -487,42 +487,55 @@ std::vector wordwrap(String &line, uint16_t maxwidth) { // Draw centered text void drawTextCenter(int16_t cx, int16_t cy, String text) { +#ifdef DISPLAY_ST7796 + auto oldDatum = getdisplay().getTextDatum(); + getdisplay().setTextDatum(textdatum_t::middle_center); + getdisplay().drawString(text, cx, cy); + getdisplay().setTextDatum(oldDatum); +#else int16_t x1, y1; uint16_t w, h; -#ifdef DISPLAY_ST7796 - // LovyanGFX doesn't expose getTextBounds; use width/height helpers - w = getdisplay().textWidth(text); - h = getdisplay().fontHeight(); -#else getdisplay().getTextBounds(text, 0, 150, &x1, &y1, &w, &h); -#endif getdisplay().setCursor(cx - w / 2, cy + h / 2); getdisplay().print(text); +#endif } // 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; -#ifdef DISPLAY_ST7796 - w = getdisplay().textWidth(text); - h = getdisplay().fontHeight(); -#else +#ifndef DISPLAY_ST7796 + int16_t x1, y1; + uint16_t w, h; getdisplay().getTextBounds(text, cx, cy, &x1, &y1, &w, &h); // Find text center #endif - 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); +#ifdef DISPLAY_ST7796 + auto oldDatum = getdisplay().getTextDatum(); + getdisplay().setTextDatum(textdatum_t::middle_center); + getdisplay().drawString(text, cx, cy); + getdisplay().setTextDatum(oldDatum); +#else + getdisplay().setCursor(cx - w/2, cy + h/2); // Set cursor to center getdisplay().print(text); // Draw text +#endif } else{ getdisplay().drawRoundRect(cx - sx / 2, cy - sy / 2, sx, sy, 5, fg); // Draw button getdisplay().setTextColor(fg); +#ifdef DISPLAY_ST7796 + auto oldDatum = getdisplay().getTextDatum(); + getdisplay().setTextDatum(textdatum_t::middle_center); + getdisplay().drawString(text, cx, cy); + getdisplay().setTextDatum(oldDatum); +#else + getdisplay().setCursor(cx - w/2, cy + h/2); // Set cursor to center getdisplay().print(text); // Draw text +#endif } } From ffa9c8ac834257c8262706c761f8c8baaf739bc9 Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sat, 28 Feb 2026 18:39:05 +0000 Subject: [PATCH 26/75] Fix basle line for fonts --- lib/obp60task/OBP60Extensions.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index f704c40..b0fa974 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -122,6 +122,8 @@ public: // No dedicated TFT PWM backlight pin configured on this board. // Keep backlight handling outside LovyanGFX to avoid LEDC init on invalid GPIO. setPanel(&_panel_instance); + // Match Adafruit GFX cursor semantics: y coordinate is text baseline. + setTextDatum(textdatum_t::baseline_left); } // compatibility helpers -------------------------------------------------- From 2e47263008f905a2e715f4548cf364d510a96e6a Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sat, 28 Feb 2026 22:10:57 +0000 Subject: [PATCH 27/75] Fix for drawTextCenter() for ST7796 --- lib/obp60task/OBP60Extensions.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/obp60task/OBP60Extensions.cpp b/lib/obp60task/OBP60Extensions.cpp index 39d997c..b596d9e 100644 --- a/lib/obp60task/OBP60Extensions.cpp +++ b/lib/obp60task/OBP60Extensions.cpp @@ -488,9 +488,10 @@ std::vector wordwrap(String &line, uint16_t maxwidth) { // Draw centered text void drawTextCenter(int16_t cx, int16_t cy, String text) { #ifdef DISPLAY_ST7796 + uint16_t h = getdisplay().fontHeight(); auto oldDatum = getdisplay().getTextDatum(); - getdisplay().setTextDatum(textdatum_t::middle_center); - getdisplay().drawString(text, cx, cy); + getdisplay().setTextDatum(textdatum_t::top_center); + getdisplay().drawString(text, cx, cy - static_cast(h / 2)); getdisplay().setTextDatum(oldDatum); #else int16_t x1, y1; From 03b9294a1e60bd794ad838f26585f55ba7a04b35 Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sat, 28 Feb 2026 22:16:16 +0000 Subject: [PATCH 28/75] Fix for drawTextCenter() --- lib/obp60task/OBP60Extensions.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/obp60task/OBP60Extensions.cpp b/lib/obp60task/OBP60Extensions.cpp index b596d9e..4b33954 100644 --- a/lib/obp60task/OBP60Extensions.cpp +++ b/lib/obp60task/OBP60Extensions.cpp @@ -488,11 +488,10 @@ std::vector wordwrap(String &line, uint16_t maxwidth) { // Draw centered text void drawTextCenter(int16_t cx, int16_t cy, String text) { #ifdef DISPLAY_ST7796 + uint16_t w = getdisplay().textWidth(text); uint16_t h = getdisplay().fontHeight(); - auto oldDatum = getdisplay().getTextDatum(); - getdisplay().setTextDatum(textdatum_t::top_center); - getdisplay().drawString(text, cx, cy - static_cast(h / 2)); - getdisplay().setTextDatum(oldDatum); + getdisplay().setCursor(cx - static_cast(w / 2), cy + static_cast(h / 2)); + getdisplay().print(text); #else int16_t x1, y1; uint16_t w, h; From abcbf50422ef5402d8494a052e467d1068cfacec Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sat, 28 Feb 2026 22:58:12 +0000 Subject: [PATCH 29/75] Fix for displayGetTextBounds() --- lib/obp60task/OBP60Extensions.h | 86 +++++++++++++++++++++++++++++++-- 1 file changed, 81 insertions(+), 5 deletions(-) diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index b0fa974..c0d9a58 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -128,14 +128,20 @@ public: // compatibility helpers -------------------------------------------------- using lgfx::LGFX_Device::setFont; + void setFont(const lgfx::IFont* font) { + _currentAdfFont = nullptr; + lgfx::LGFX_Device::setFont(font); + } // Adafruit GFX fonts support on TFT via LovyanGFX bridge void setFont(const GFXfont *font) { if (font == nullptr) { + _currentAdfFont = nullptr; lgfx::LGFX_Device::setFont(nullptr); return; } if (font->glyph == nullptr || font->bitmap == nullptr || font->last < font->first) { + _currentAdfFont = nullptr; lgfx::LGFX_Device::setFont(nullptr); return; } @@ -176,8 +182,81 @@ public: font->last, font->yAdvance ); + _currentAdfFont = font; lgfx::LGFX_Device::setFont(&_adfFontBridge); } + + void getTextBounds(const String &txt, int16_t x, int16_t y, + int16_t *x0, int16_t *y0, + uint16_t *w, uint16_t *h) { + if (w == nullptr || h == nullptr) { + return; + } + if (_currentAdfFont == nullptr || txt.length() == 0) { + *w = textWidth(txt); + *h = fontHeight(); + if (x0) *x0 = x; + if (y0) *y0 = y - static_cast(*h); + return; + } + + const float sx = getTextSizeX(); + const float sy = getTextSizeY(); + + int32_t cursorX = x; + int32_t cursorY = y; + int32_t minX = 0; + int32_t minY = 0; + int32_t maxX = 0; + int32_t maxY = 0; + bool hasPixel = false; + + for (size_t i = 0; i < txt.length(); ++i) { + char c = txt[i]; + if (c == '\r') continue; + if (c == '\n') { + cursorX = x; + cursorY += static_cast(_currentAdfFont->yAdvance * sy); + continue; + } + if (c < _currentAdfFont->first || c > _currentAdfFont->last) { + continue; + } + + const GFXglyph* glyph = &_currentAdfFont->glyph[static_cast(c) - _currentAdfFont->first]; + const int32_t gw = static_cast(glyph->width * sx); + const int32_t gh = static_cast(glyph->height * sy); + const int32_t gx1 = cursorX + static_cast(glyph->xOffset * sx); + const int32_t gy1 = cursorY + static_cast(glyph->yOffset * sy); + + if (gw > 0 && gh > 0) { + const int32_t gx2 = gx1 + gw - 1; + const int32_t gy2 = gy1 + gh - 1; + if (!hasPixel) { + minX = gx1; minY = gy1; maxX = gx2; maxY = gy2; + hasPixel = true; + } else { + if (gx1 < minX) minX = gx1; + if (gy1 < minY) minY = gy1; + if (gx2 > maxX) maxX = gx2; + if (gy2 > maxY) maxY = gy2; + } + } + cursorX += static_cast(glyph->xAdvance * sx); + } + + if (hasPixel) { + if (x0) *x0 = static_cast(minX); + if (y0) *y0 = static_cast(minY); + *w = static_cast(maxX - minX + 1); + *h = static_cast(maxY - minY + 1); + } else { + if (x0) *x0 = x; + if (y0) *y0 = y; + *w = 0; + *h = 0; + } + } // E-Ink interface compatibility void setFullWindow() { /* no-op on TFT */ } @@ -185,6 +264,7 @@ private: lgfx::GFXfont _adfFontBridge { nullptr, nullptr, 0, 0, 0 }; lgfx::GFXglyph* _adfGlyphBridge = nullptr; uint16_t _adfGlyphCount = 0; + const GFXfont* _currentAdfFont = nullptr; lgfx::Panel_ST7796 _panel_instance; }; @@ -297,11 +377,7 @@ inline void displayGetTextBounds(const String &txt, int16_t x, int16_t y, int16_t *x0, int16_t *y0, uint16_t *w, uint16_t *h) { #ifdef DISPLAY_ST7796 - // LovyanGFX doesn't expose getTextBounds; compute via helpers - *w = getdisplay().textWidth(txt); - *h = getdisplay().fontHeight(); - if (x0) *x0 = 0; - if (y0) *y0 = 0; + getdisplay().getTextBounds(txt, x, y, x0, y0, w, h); #else getdisplay().getTextBounds(txt, x, y, x0, y0, w, h); #endif From 75724b29ebac3c6165e488d9ad1fffd84090f84b Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sat, 28 Feb 2026 23:06:30 +0000 Subject: [PATCH 30/75] Fix for drawButtonCenter() --- lib/obp60task/OBP60Extensions.cpp | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/lib/obp60task/OBP60Extensions.cpp b/lib/obp60task/OBP60Extensions.cpp index 4b33954..46cb4e8 100644 --- a/lib/obp60task/OBP60Extensions.cpp +++ b/lib/obp60task/OBP60Extensions.cpp @@ -503,39 +503,26 @@ void drawTextCenter(int16_t cx, int16_t cy, String 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) { - uint16_t color; - -#ifndef DISPLAY_ST7796 - int16_t x1, y1; uint16_t w, h; +#ifdef DISPLAY_ST7796 + w = getdisplay().textWidth(text); + h = getdisplay().fontHeight(); +#else + int16_t x1, y1; getdisplay().getTextBounds(text, cx, cy, &x1, &y1, &w, &h); // Find text center #endif //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); -#ifdef DISPLAY_ST7796 - auto oldDatum = getdisplay().getTextDatum(); - getdisplay().setTextDatum(textdatum_t::middle_center); - getdisplay().drawString(text, cx, cy); - getdisplay().setTextDatum(oldDatum); -#else getdisplay().setCursor(cx - w/2, cy + h/2); // Set cursor to center getdisplay().print(text); // Draw text -#endif } else{ getdisplay().drawRoundRect(cx - sx / 2, cy - sy / 2, sx, sy, 5, fg); // Draw button getdisplay().setTextColor(fg); -#ifdef DISPLAY_ST7796 - auto oldDatum = getdisplay().getTextDatum(); - getdisplay().setTextDatum(textdatum_t::middle_center); - getdisplay().drawString(text, cx, cy); - getdisplay().setTextDatum(oldDatum); -#else getdisplay().setCursor(cx - w/2, cy + h/2); // Set cursor to center getdisplay().print(text); // Draw text -#endif } } From 2b2d2e3fd61e8fffb64063ad442ae01c254a3604 Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sat, 28 Feb 2026 23:14:38 +0000 Subject: [PATCH 31/75] Fix for drawButtonCenter() --- lib/obp60task/OBP60Extensions.cpp | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/lib/obp60task/OBP60Extensions.cpp b/lib/obp60task/OBP60Extensions.cpp index 46cb4e8..5c5bad6 100644 --- a/lib/obp60task/OBP60Extensions.cpp +++ b/lib/obp60task/OBP60Extensions.cpp @@ -487,41 +487,33 @@ std::vector wordwrap(String &line, uint16_t maxwidth) { // Draw centered text void drawTextCenter(int16_t cx, int16_t cy, String text) { -#ifdef DISPLAY_ST7796 - uint16_t w = getdisplay().textWidth(text); - uint16_t h = getdisplay().fontHeight(); - getdisplay().setCursor(cx - static_cast(w / 2), cy + static_cast(h / 2)); - getdisplay().print(text); -#else int16_t x1, y1; uint16_t w, h; - getdisplay().getTextBounds(text, 0, 150, &x1, &y1, &w, &h); - getdisplay().setCursor(cx - w / 2, cy + h / 2); + displayGetTextBounds(text, 0, 0, &x1, &y1, &w, &h); + int16_t cursorX = cx - (x1 + static_cast(w / 2)); + int16_t cursorY = cy - (y1 + static_cast(h / 2)); + getdisplay().setCursor(cursorX, cursorY); getdisplay().print(text); -#endif } // 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) { - uint16_t w, h; -#ifdef DISPLAY_ST7796 - w = getdisplay().textWidth(text); - h = getdisplay().fontHeight(); -#else int16_t x1, y1; - getdisplay().getTextBounds(text, cx, cy, &x1, &y1, &w, &h); // Find text center -#endif + uint16_t w, h; + displayGetTextBounds(text, 0, 0, &x1, &y1, &w, &h); + int16_t cursorX = cx - (x1 + static_cast(w / 2)); + int16_t cursorY = cy - (y1 + static_cast(h / 2)); //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().setCursor(cx - w/2, cy + h/2); // Set cursor to center + getdisplay().setCursor(cursorX, cursorY); // Set cursor to center getdisplay().print(text); // Draw text } else{ getdisplay().drawRoundRect(cx - sx / 2, cy - sy / 2, sx, sy, 5, fg); // Draw button getdisplay().setTextColor(fg); - getdisplay().setCursor(cx - w/2, cy + h/2); // Set cursor to center + getdisplay().setCursor(cursorX, cursorY); // Set cursor to center getdisplay().print(text); // Draw text } } From b10792b243d9c101f0fbc19b7e789418471b7d56 Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sat, 28 Feb 2026 23:42:10 +0000 Subject: [PATCH 32/75] Fix for display flicker by display refresh --- lib/obp60task/obp60task.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index fba82ac..7e1a631 100644 --- a/lib/obp60task/obp60task.cpp +++ b/lib/obp60task/obp60task.cpp @@ -839,7 +839,9 @@ void OBP60Task(GwApi *api){ // Clear display // getdisplay().fillRect(0, 0, getdisplay().width(), getdisplay().height(), commonData.bgcolor); + #ifndef DISPLAY_ST7796 getdisplay().fillScreen(commonData.bgcolor); // Clear display + #endif // Show header if enabled if (pages[pageNumber].description && pages[pageNumber].description->header or systemPage){ From b7fab08306e5b2c03a1c0ab6ad54e8bab9f21117 Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sat, 28 Feb 2026 23:54:26 +0000 Subject: [PATCH 33/75] Fix for display flicker by display refresh --- lib/obp60task/obp60task.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index 7e1a631..c5f20fa 100644 --- a/lib/obp60task/obp60task.cpp +++ b/lib/obp60task/obp60task.cpp @@ -826,6 +826,7 @@ void OBP60Task(GwApi *api){ if(millis() > starttime3 + pagetime){ LOG_DEBUG(GwLog::DEBUG,"Page with refreshtime=%d", pagetime); starttime3 = millis(); + bool pageChanged = (lastPage != pageNumber); //refresh data from api api->getBoatDataValues(boatValues.numValues,boatValues.allBoatValues); @@ -841,6 +842,10 @@ void OBP60Task(GwApi *api){ // getdisplay().fillRect(0, 0, getdisplay().width(), getdisplay().height(), commonData.bgcolor); #ifndef DISPLAY_ST7796 getdisplay().fillScreen(commonData.bgcolor); // Clear display + #else + if (pageChanged) { + getdisplay().fillScreen(commonData.bgcolor); // Clear display after page switch + } #endif // Show header if enabled @@ -873,7 +878,7 @@ void OBP60Task(GwApi *api){ displayNextPage(); // Partial update (fast) } else{ - if (lastPage != pageNumber){ + if (pageChanged){ 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 From a4b99fb619e401b39c7ba0b86462ddd482535c17 Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sun, 1 Mar 2026 00:32:50 +0000 Subject: [PATCH 34/75] Fix display refresh --- lib/obp60task/obp60task.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index c5f20fa..d16f749 100644 --- a/lib/obp60task/obp60task.cpp +++ b/lib/obp60task/obp60task.cpp @@ -840,13 +840,7 @@ void OBP60Task(GwApi *api){ // Clear display // getdisplay().fillRect(0, 0, getdisplay().width(), getdisplay().height(), commonData.bgcolor); - #ifndef DISPLAY_ST7796 getdisplay().fillScreen(commonData.bgcolor); // Clear display - #else - if (pageChanged) { - getdisplay().fillScreen(commonData.bgcolor); // Clear display after page switch - } - #endif // Show header if enabled if (pages[pageNumber].description && pages[pageNumber].description->header or systemPage){ From ab0bac7577e3494288b9711b67ef80f2fa0f2cf5 Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sun, 1 Mar 2026 14:52:53 +0000 Subject: [PATCH 35/75] Fix crash ith reboot (GwWifi + PageNavigation) --- lib/gwwifi/GwWifi.cpp | 36 ++++++++++++++++++++++------ lib/obp60task/OBP60Extensions.h | 3 +++ lib/obp60task/PageNavigation.cpp | 40 +++++++++++++++++++++++++++----- 3 files changed, 66 insertions(+), 13 deletions(-) diff --git a/lib/gwwifi/GwWifi.cpp b/lib/gwwifi/GwWifi.cpp index cd475c2..f4a81ce 100644 --- a/lib/gwwifi/GwWifi.cpp +++ b/lib/gwwifi/GwWifi.cpp @@ -132,18 +132,40 @@ void GwWifi::loop(){ { LOG_DEBUG(GwLog::LOG,"wifiClient: retry connect to %s", wifiSSID->asCString()); - // CRITICAL SECTION: WiFi-Operationen müssen serialisiert werden + // Keep locked sections short to avoid cross-core stalls/WDT. if (acquireMutex()){ - WiFi.disconnect(true); - delay(300); - esp_wifi_stop(); - delay(100); - esp_wifi_start(); + WiFi.disconnect(true); releaseMutex(); + } + else{ + LOG_DEBUG(GwLog::ERROR,"GwWifi: mutex timeout in loop (disconnect)"); + } + + delay(300); + + if (acquireMutex()){ + esp_err_t stopErr=esp_wifi_stop(); + releaseMutex(); + if (stopErr != ESP_OK){ + LOG_DEBUG(GwLog::ERROR,"GwWifi: esp_wifi_stop failed: %d",(int)stopErr); + } + } + else{ + LOG_DEBUG(GwLog::ERROR,"GwWifi: mutex timeout in loop (stop)"); + } + + delay(100); + + if (acquireMutex()){ + esp_err_t startErr=esp_wifi_start(); + releaseMutex(); + if (startErr != ESP_OK){ + LOG_DEBUG(GwLog::ERROR,"GwWifi: esp_wifi_start failed: %d",(int)startErr); + } connectInternal(); } else{ - LOG_DEBUG(GwLog::ERROR,"GwWifi: mutex timeout in loop"); + LOG_DEBUG(GwLog::ERROR,"GwWifi: mutex timeout in loop (start)"); } } } diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index c0d9a58..7923c75 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -316,6 +316,9 @@ inline void drawMonochromeBitmap( getdisplay().drawPixel(x + xx, y + yy, color); } } + if ((yy & 0x0F) == 0) { + yield(); + } } #else // E‑Paper: just hand over to driver (expects MSB‑first horizontal) diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp index 03729e1..262bce1 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -394,8 +394,20 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map int numPix = json["number_pixels"] | 0; // Read number of pixels imgWidth = json["width"] | 0; // Read width of image imgHeight = json["height"] | 0; // Read height og image + size_t requiredBytes = 0; + if (imgWidth > 0 && imgHeight > 0){ + requiredBytes = (size_t)((imgWidth + 7) / 8) * (size_t)imgHeight; + } + if (requiredBytes == 0){ + LOG_DEBUG(GwLog::ERROR,"Error PageNavigation: invalid image geometry w=%d h=%d",imgWidth,imgHeight); + return PAGE_UPDATE; + } const char* b64src = json["picture_base64"].as(); // Read picture as Base64 content + if (b64src == nullptr){ + LOG_DEBUG(GwLog::ERROR,"Error PageNavigation: picture_base64 missing"); + return PAGE_UPDATE; + } 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 @@ -407,7 +419,10 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map // Set image buffer in PSRAM //size_t imgSize = getdisplay().width() * getdisplay().height(); - size_t imgSize = numPix; // Calculate image size + size_t imgSize = (numPix > 0) ? (size_t)numPix : requiredBytes; // Calculate image size + if (imgSize < requiredBytes){ + imgSize = requiredBytes; + } uint8_t* imageData = (uint8_t*) heap_caps_malloc(imgSize, MALLOC_CAP_SPIRAM); // Allocate PSRAM for image if (!imageData) { LOG_DEBUG(GwLog::ERROR,"Error PageNavigation: PSRAM alloc image buffer failed"); @@ -417,17 +432,30 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map // Decode Base64 content to image size_t decodedSize = 0; - decoder.decodeBase64(b64, imageData, imgSize, decodedSize); + bool decodeOk = decoder.decodeBase64(b64, imageData, imgSize, decodedSize); + if (!decodeOk || decodedSize < requiredBytes){ + LOG_DEBUG(GwLog::ERROR, + "Error PageNavigation: decode failed (ok=%d, decoded=%u, required=%u)", + decodeOk ? 1 : 0, + (unsigned int)decodedSize, + (unsigned int)requiredBytes + ); + free(b64); + free(imageData); + return PAGE_UPDATE; + } // Copy actual navigation man to ackup map imageBackupWidth = imgWidth; imageBackupHeight = imgHeight; imageBackupSize = imgSize; - if (decodedSize > 0) { - memcpy(imageBackupData, imageData, decodedSize); - imageBackupSize = decodedSize; + if (decodedSize > 0 && imageBackupData != nullptr) { + size_t backupCapacity = (size_t)GxEPD_WIDTH * (size_t)GxEPD_HEIGHT; + size_t copySize = (decodedSize > backupCapacity) ? backupCapacity : decodedSize; + memcpy(imageBackupData, imageData, copySize); + imageBackupSize = copySize; } - hasImageBackup = true; + hasImageBackup = (imageBackupData != nullptr); lostCounter = 0; // Show image (navigation map) From f3f20f40c74a03c89244c4a7127d5af45af748f8 Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sun, 1 Mar 2026 14:52:53 +0000 Subject: [PATCH 36/75] Fix crash with reboot (GwWifi + PageNavigation) --- lib/gwwifi/GwWifi.cpp | 36 ++++++++++++++++++++++------ lib/obp60task/OBP60Extensions.h | 3 +++ lib/obp60task/PageNavigation.cpp | 40 +++++++++++++++++++++++++++----- 3 files changed, 66 insertions(+), 13 deletions(-) diff --git a/lib/gwwifi/GwWifi.cpp b/lib/gwwifi/GwWifi.cpp index cd475c2..f4a81ce 100644 --- a/lib/gwwifi/GwWifi.cpp +++ b/lib/gwwifi/GwWifi.cpp @@ -132,18 +132,40 @@ void GwWifi::loop(){ { LOG_DEBUG(GwLog::LOG,"wifiClient: retry connect to %s", wifiSSID->asCString()); - // CRITICAL SECTION: WiFi-Operationen müssen serialisiert werden + // Keep locked sections short to avoid cross-core stalls/WDT. if (acquireMutex()){ - WiFi.disconnect(true); - delay(300); - esp_wifi_stop(); - delay(100); - esp_wifi_start(); + WiFi.disconnect(true); releaseMutex(); + } + else{ + LOG_DEBUG(GwLog::ERROR,"GwWifi: mutex timeout in loop (disconnect)"); + } + + delay(300); + + if (acquireMutex()){ + esp_err_t stopErr=esp_wifi_stop(); + releaseMutex(); + if (stopErr != ESP_OK){ + LOG_DEBUG(GwLog::ERROR,"GwWifi: esp_wifi_stop failed: %d",(int)stopErr); + } + } + else{ + LOG_DEBUG(GwLog::ERROR,"GwWifi: mutex timeout in loop (stop)"); + } + + delay(100); + + if (acquireMutex()){ + esp_err_t startErr=esp_wifi_start(); + releaseMutex(); + if (startErr != ESP_OK){ + LOG_DEBUG(GwLog::ERROR,"GwWifi: esp_wifi_start failed: %d",(int)startErr); + } connectInternal(); } else{ - LOG_DEBUG(GwLog::ERROR,"GwWifi: mutex timeout in loop"); + LOG_DEBUG(GwLog::ERROR,"GwWifi: mutex timeout in loop (start)"); } } } diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index c0d9a58..7923c75 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -316,6 +316,9 @@ inline void drawMonochromeBitmap( getdisplay().drawPixel(x + xx, y + yy, color); } } + if ((yy & 0x0F) == 0) { + yield(); + } } #else // E‑Paper: just hand over to driver (expects MSB‑first horizontal) diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp index 03729e1..262bce1 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -394,8 +394,20 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map int numPix = json["number_pixels"] | 0; // Read number of pixels imgWidth = json["width"] | 0; // Read width of image imgHeight = json["height"] | 0; // Read height og image + size_t requiredBytes = 0; + if (imgWidth > 0 && imgHeight > 0){ + requiredBytes = (size_t)((imgWidth + 7) / 8) * (size_t)imgHeight; + } + if (requiredBytes == 0){ + LOG_DEBUG(GwLog::ERROR,"Error PageNavigation: invalid image geometry w=%d h=%d",imgWidth,imgHeight); + return PAGE_UPDATE; + } const char* b64src = json["picture_base64"].as(); // Read picture as Base64 content + if (b64src == nullptr){ + LOG_DEBUG(GwLog::ERROR,"Error PageNavigation: picture_base64 missing"); + return PAGE_UPDATE; + } 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 @@ -407,7 +419,10 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map // Set image buffer in PSRAM //size_t imgSize = getdisplay().width() * getdisplay().height(); - size_t imgSize = numPix; // Calculate image size + size_t imgSize = (numPix > 0) ? (size_t)numPix : requiredBytes; // Calculate image size + if (imgSize < requiredBytes){ + imgSize = requiredBytes; + } uint8_t* imageData = (uint8_t*) heap_caps_malloc(imgSize, MALLOC_CAP_SPIRAM); // Allocate PSRAM for image if (!imageData) { LOG_DEBUG(GwLog::ERROR,"Error PageNavigation: PSRAM alloc image buffer failed"); @@ -417,17 +432,30 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map // Decode Base64 content to image size_t decodedSize = 0; - decoder.decodeBase64(b64, imageData, imgSize, decodedSize); + bool decodeOk = decoder.decodeBase64(b64, imageData, imgSize, decodedSize); + if (!decodeOk || decodedSize < requiredBytes){ + LOG_DEBUG(GwLog::ERROR, + "Error PageNavigation: decode failed (ok=%d, decoded=%u, required=%u)", + decodeOk ? 1 : 0, + (unsigned int)decodedSize, + (unsigned int)requiredBytes + ); + free(b64); + free(imageData); + return PAGE_UPDATE; + } // Copy actual navigation man to ackup map imageBackupWidth = imgWidth; imageBackupHeight = imgHeight; imageBackupSize = imgSize; - if (decodedSize > 0) { - memcpy(imageBackupData, imageData, decodedSize); - imageBackupSize = decodedSize; + if (decodedSize > 0 && imageBackupData != nullptr) { + size_t backupCapacity = (size_t)GxEPD_WIDTH * (size_t)GxEPD_HEIGHT; + size_t copySize = (decodedSize > backupCapacity) ? backupCapacity : decodedSize; + memcpy(imageBackupData, imageData, copySize); + imageBackupSize = copySize; } - hasImageBackup = true; + hasImageBackup = (imageBackupData != nullptr); lostCounter = 0; // Show image (navigation map) From 53bdc491f1d515439d10ef5e89465f160dd819e6 Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sun, 1 Mar 2026 15:06:16 +0000 Subject: [PATCH 37/75] Fix crash with reboot --- lib/statistics/GwStatistics.h | 12 ++++++++++-- src/main.cpp | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/statistics/GwStatistics.h b/lib/statistics/GwStatistics.h index b2efec8..fbf7c5e 100644 --- a/lib/statistics/GwStatistics.h +++ b/lib/statistics/GwStatistics.h @@ -1,5 +1,13 @@ #pragma once #include +#include +#include + +static inline int64_t gwMonotonicUs(){ + TickType_t ticks=xTaskGetTickCount(); + return ((int64_t)ticks) * ((int64_t)portTICK_PERIOD_MS) * 1000; +} + class TimeAverage{ double factor=0.3; double current=0; @@ -70,7 +78,7 @@ class TimeMonitor{ } void reset(){ if (last != 0 && start != 0) loop->add(last-start); - start=esp_timer_get_time(); + start=gwMonotonicUs(); for (size_t i=0;i Date: Sun, 1 Mar 2026 16:08:54 +0100 Subject: [PATCH 38/75] Add info --- lib/obp60task/Changes_to_original.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/obp60task/Changes_to_original.txt b/lib/obp60task/Changes_to_original.txt index 052d644..b283d8e 100644 --- a/lib/obp60task/Changes_to_original.txt +++ b/lib/obp60task/Changes_to_original.txt @@ -2,6 +2,8 @@ Changes to original project (wellenvogel) * esp32-nmea2000-obp60/gwwifi/GwWifi.CPPDEFINES - any fixes for reconnect handling +* GWStatisticsw.h + - changed time source for log messages * esp32-nmea2000-obp60/platformio.ini - change to newer versions for AsyncTCP and AsyncWebServer (better handling for bad WiFi connections) From 762004ac9fa887e45d8aaf2bc1c72bd35a7a62c9 Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sun, 1 Mar 2026 15:55:34 +0000 Subject: [PATCH 39/75] Implement shaddow frame buffer for TFT display --- lib/obp60task/OBP60Extensions.cpp | 35 ++++++- lib/obp60task/OBP60Extensions.h | 149 +++++++++++++++++++++++++++++- lib/obp60task/obp60task.cpp | 14 ++- 3 files changed, 187 insertions(+), 11 deletions(-) diff --git a/lib/obp60task/OBP60Extensions.cpp b/lib/obp60task/OBP60Extensions.cpp index 5c5bad6..eaec447 100644 --- a/lib/obp60task/OBP60Extensions.cpp +++ b/lib/obp60task/OBP60Extensions.cpp @@ -57,9 +57,30 @@ GxEPD2_BW & getdisplay(){r #endif #ifdef DISPLAY_ST7796 -// only instantiate; class defined in header -static LGFX display; -LGFX & getdisplay(){return display;} +// panel device + offscreen shadow framebuffer +static LGFX panelDisplay; +static LGFXCanvas shadowDisplay(&panelDisplay); +static bool shadowDisplayInitialized = false; + +LGFXCanvas & getdisplay(){return shadowDisplay;} +LGFX & getpaneldisplay(){return panelDisplay;} + +bool initDisplayShadowBuffer(){ + if (shadowDisplayInitialized) return true; + + shadowDisplay.setPsram(true); + shadowDisplay.setColorDepth(16); + shadowDisplay.setTextDatum(textdatum_t::baseline_left); + + if (shadowDisplay.createSprite(GxEPD_WIDTH, GxEPD_HEIGHT) == nullptr) { + shadowDisplayInitialized = false; + return false; + } + + shadowDisplay.fillScreen(GxEPD_BLACK); + shadowDisplayInitialized = true; + return true; +} #endif // Horter I2C moduls @@ -258,7 +279,9 @@ void deepSleep(CommonData &common){ getdisplay().setCursor(65, 175); getdisplay().print("To wake up press key and wait 5s"); displayNextPage(); // Update display contents - #ifndef DISPLAY_ST7796 + #ifdef DISPLAY_ST7796 + getpaneldisplay().powerSave(true); // Display power save + #else getdisplay().powerOff(); // Display power off #endif setPortPin(OBP_POWER_50, false); // Power off ePaper display @@ -285,7 +308,9 @@ void deepSleep(CommonData &common){ getdisplay().setCursor(65, 175); getdisplay().print("To wake up press wheel and wait 5s"); displayNextPage(); // Partial update - #ifndef DISPLAY_ST7796 + #ifdef DISPLAY_ST7796 + getpaneldisplay().powerSave(true); // Display power save + #else getdisplay().powerOff(); // Display power off #endif setPortPin(OBP_POWER_EPD, false); // Power off ePaper display diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index 7923c75..db86a46 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -268,7 +268,148 @@ private: lgfx::Panel_ST7796 _panel_instance; }; -LGFX & getdisplay(); +class LGFXCanvas : public lgfx::LGFX_Sprite { +public: + explicit LGFXCanvas(lgfx::LGFX_Device* parent = nullptr) : lgfx::LGFX_Sprite(parent) {} + + using lgfx::LGFX_Sprite::setFont; + void setFont(const GFXfont *font) { + if (font == nullptr) { + _currentAdfFont = nullptr; + lgfx::LGFX_Sprite::setFont(nullptr); + return; + } + + if (font->glyph == nullptr || font->bitmap == nullptr || font->last < font->first) { + _currentAdfFont = nullptr; + lgfx::LGFX_Sprite::setFont(nullptr); + return; + } + + const uint16_t glyphCount = static_cast(font->last - font->first + 1); + if (glyphCount == 0) { + lgfx::LGFX_Sprite::setFont(nullptr); + return; + } + + if (_adfGlyphCount != glyphCount || _adfGlyphBridge == nullptr) { + if (_adfGlyphBridge != nullptr) { + free(_adfGlyphBridge); + _adfGlyphBridge = nullptr; + _adfGlyphCount = 0; + } + _adfGlyphBridge = static_cast(malloc(sizeof(lgfx::GFXglyph) * glyphCount)); + if (_adfGlyphBridge == nullptr) { + lgfx::LGFX_Sprite::setFont(nullptr); + return; + } + _adfGlyphCount = glyphCount; + } + + for (uint16_t index = 0; index < glyphCount; ++index) { + _adfGlyphBridge[index].bitmapOffset = font->glyph[index].bitmapOffset; + _adfGlyphBridge[index].width = font->glyph[index].width; + _adfGlyphBridge[index].height = font->glyph[index].height; + _adfGlyphBridge[index].xAdvance = font->glyph[index].xAdvance; + _adfGlyphBridge[index].xOffset = font->glyph[index].xOffset; + _adfGlyphBridge[index].yOffset = font->glyph[index].yOffset; + } + + _adfFontBridge = lgfx::GFXfont( + const_cast(font->bitmap), + _adfGlyphBridge, + font->first, + font->last, + font->yAdvance + ); + _currentAdfFont = font; + lgfx::LGFX_Sprite::setFont(&_adfFontBridge); + } + + void getTextBounds(const String &txt, int16_t x, int16_t y, + int16_t *x0, int16_t *y0, + uint16_t *w, uint16_t *h) { + if (w == nullptr || h == nullptr) { + return; + } + if (_currentAdfFont == nullptr || txt.length() == 0) { + *w = textWidth(txt); + *h = fontHeight(); + if (x0) *x0 = x; + if (y0) *y0 = y - static_cast(*h); + return; + } + + const float sx = getTextSizeX(); + const float sy = getTextSizeY(); + + int32_t cursorX = x; + int32_t cursorY = y; + int32_t minX = 0; + int32_t minY = 0; + int32_t maxX = 0; + int32_t maxY = 0; + bool hasPixel = false; + + for (size_t i = 0; i < txt.length(); ++i) { + char c = txt[i]; + if (c == '\r') continue; + if (c == '\n') { + cursorX = x; + cursorY += static_cast(_currentAdfFont->yAdvance * sy); + continue; + } + if (c < _currentAdfFont->first || c > _currentAdfFont->last) { + continue; + } + + const GFXglyph* glyph = &_currentAdfFont->glyph[static_cast(c) - _currentAdfFont->first]; + const int32_t gw = static_cast(glyph->width * sx); + const int32_t gh = static_cast(glyph->height * sy); + const int32_t gx1 = cursorX + static_cast(glyph->xOffset * sx); + const int32_t gy1 = cursorY + static_cast(glyph->yOffset * sy); + + if (gw > 0 && gh > 0) { + const int32_t gx2 = gx1 + gw - 1; + const int32_t gy2 = gy1 + gh - 1; + if (!hasPixel) { + minX = gx1; minY = gy1; maxX = gx2; maxY = gy2; + hasPixel = true; + } else { + if (gx1 < minX) minX = gx1; + if (gy1 < minY) minY = gy1; + if (gx2 > maxX) maxX = gx2; + if (gy2 > maxY) maxY = gy2; + } + } + cursorX += static_cast(glyph->xAdvance * sx); + } + + if (hasPixel) { + if (x0) *x0 = static_cast(minX); + if (y0) *y0 = static_cast(minY); + *w = static_cast(maxX - minX + 1); + *h = static_cast(maxY - minY + 1); + } else { + if (x0) *x0 = x; + if (y0) *y0 = y; + *w = 0; + *h = 0; + } + } + + void setFullWindow() { /* no-op on TFT */ } + +private: + lgfx::GFXfont _adfFontBridge { nullptr, nullptr, 0, 0, 0 }; + lgfx::GFXglyph* _adfGlyphBridge = nullptr; + uint16_t _adfGlyphCount = 0; + const GFXfont* _currentAdfFont = nullptr; +}; + +LGFXCanvas & getdisplay(); +LGFX & getpaneldisplay(); +bool initDisplayShadowBuffer(); #endif // Page display return values @@ -344,7 +485,7 @@ inline void displayDrawBitmap(int16_t x, int16_t y, inline void displayFirstPage() { #ifdef DISPLAY_ST7796 - // TFT LCD doesn't need firstPage() + initDisplayShadowBuffer(); #else getdisplay().firstPage(); #endif @@ -352,7 +493,9 @@ inline void displayFirstPage() { inline void displayNextPage() { #ifdef DISPLAY_ST7796 - // TFT LCD doesn't need nextPage() for refresh + if (initDisplayShadowBuffer()) { + getdisplay().pushSprite(0, 0); + } #else getdisplay().nextPage(); #endif diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index d16f749..963902b 100644 --- a/lib/obp60task/obp60task.cpp +++ b/lib/obp60task/obp60task.cpp @@ -285,7 +285,9 @@ void underVoltageError(CommonData &common) { getdisplay().setCursor(65, 175); getdisplay().print("Charge battery and restart system"); displayNextPage(); // Partial update - #ifndef DISPLAY_ST7796 + #ifdef DISPLAY_ST7796 + getpaneldisplay().powerSave(true); // Display power save + #else getdisplay().powerOff(); // Display power off #endif setPortPin(OBP_POWER_EPD, false); // Power off ePaper display @@ -307,7 +309,9 @@ void underVoltageError(CommonData &common) { getdisplay().setCursor(65, 175); getdisplay().print("To wake up repower system"); displayNextPage(); // Partial update - #ifndef DISPLAY_ST7796 + #ifdef DISPLAY_ST7796 + getpaneldisplay().powerSave(true); // Display power save + #else getdisplay().powerOff(); // Display power off #endif #endif @@ -380,12 +384,16 @@ void OBP60Task(GwApi *api){ #ifdef DISPLAY_GDEY042T81 getdisplay().init(115200, true, 2, false); // Init for Waveshare boards with "clever" reset circuit, 2ms reset pulse #elif defined DISPLAY_ST7796 - getdisplay().init(); // Init for ST7796 TFT LCD + getpaneldisplay().init(); // Init for ST7796 TFT LCD panel #else getdisplay().init(115200); // Init for normal displays #endif + #ifdef DISPLAY_ST7796 + getpaneldisplay().setRotation(0); // Set display orientation (horizontal) + #else getdisplay().setRotation(0); // Set display orientation (horizontal) + #endif displaySetFullWindow(); // Set full Refresh (E-Ink only) displayFirstPage(); // set first page getdisplay().fillScreen(commonData.bgcolor); From 9cfa9ccf67fcfa6b9909ad3fcd82539609343fcd Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sun, 1 Mar 2026 16:15:02 +0000 Subject: [PATCH 40/75] Start TFT with black background --- lib/obp60task/obp60task.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index 963902b..6509020 100644 --- a/lib/obp60task/obp60task.cpp +++ b/lib/obp60task/obp60task.cpp @@ -391,6 +391,7 @@ void OBP60Task(GwApi *api){ #ifdef DISPLAY_ST7796 getpaneldisplay().setRotation(0); // Set display orientation (horizontal) + getpaneldisplay().fillRect(0, 0, 480, 320, GxEPD_BLACK); // Initialize full TFT screen to black #else getdisplay().setRotation(0); // Set display orientation (horizontal) #endif From 44b318ea0c8677ffc9092e80263d49a202946c80 Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sun, 1 Mar 2026 16:26:13 +0000 Subject: [PATCH 41/75] Black background for unused area --- lib/obp60task/OBP60Extensions.h | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index db86a46..1332f46 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -494,6 +494,7 @@ inline void displayFirstPage() { inline void displayNextPage() { #ifdef DISPLAY_ST7796 if (initDisplayShadowBuffer()) { + getpaneldisplay().fillRect(0, 0, 480, 320, GxEPD_BLACK); getdisplay().pushSprite(0, 0); } #else From 8cd193f2fc005a051b54c15c37c4787f6174bce8 Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sun, 1 Mar 2026 16:36:20 +0000 Subject: [PATCH 42/75] Fix black background --- lib/obp60task/OBP60Extensions.h | 1 - lib/obp60task/obp60task.cpp | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index 1332f46..db86a46 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -494,7 +494,6 @@ inline void displayFirstPage() { inline void displayNextPage() { #ifdef DISPLAY_ST7796 if (initDisplayShadowBuffer()) { - getpaneldisplay().fillRect(0, 0, 480, 320, GxEPD_BLACK); getdisplay().pushSprite(0, 0); } #else diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index 6509020..9758c87 100644 --- a/lib/obp60task/obp60task.cpp +++ b/lib/obp60task/obp60task.cpp @@ -391,7 +391,7 @@ void OBP60Task(GwApi *api){ #ifdef DISPLAY_ST7796 getpaneldisplay().setRotation(0); // Set display orientation (horizontal) - getpaneldisplay().fillRect(0, 0, 480, 320, GxEPD_BLACK); // Initialize full TFT screen to black + getpaneldisplay().fillScreen(0x0000); // Initialize full TFT screen to black (native RGB565) #else getdisplay().setRotation(0); // Set display orientation (horizontal) #endif From 373878825c68fbfd34a97ac4b21797408e93acab Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sun, 1 Mar 2026 16:55:26 +0000 Subject: [PATCH 43/75] Fix black background --- lib/obp60task/OBP60Extensions.h | 20 +++++++++++++++++--- lib/obp60task/OBP60Hardware.h | 2 ++ lib/obp60task/obp60task.cpp | 2 ++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index db86a46..f799788 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -105,8 +105,8 @@ public: cfg.pin_busy = -1; cfg.panel_width = 320; // Native width resolution cfg.panel_height = 480; // Native hight resolution - cfg.offset_x = 10; // Display output 400x300 pix to housing center adjusted - cfg.offset_y = -20; // Display output 400x300 pix to housing center adjusted + cfg.offset_x = 0; // No panel offset: full framebuffer mapping + cfg.offset_y = 0; // No panel offset: full framebuffer mapping cfg.offset_rotation = 3; // Rotate display content conter clock wise 90 deg cfg.dummy_read_pixel = 8; cfg.dummy_read_bits = 1; @@ -260,6 +260,20 @@ public: // E-Ink interface compatibility void setFullWindow() { /* no-op on TFT */ } + // Runtime panel offset control (ST7796) + void setPanelOffset(int16_t x, int16_t y) { + auto cfg = _panel_instance.config(); + cfg.offset_x = x; + cfg.offset_y = y; + _panel_instance.config(cfg); + } + + void getPanelOffset(int16_t &x, int16_t &y) { + auto cfg = _panel_instance.config(); + x = cfg.offset_x; + y = cfg.offset_y; + } + private: lgfx::GFXfont _adfFontBridge { nullptr, nullptr, 0, 0, 0 }; lgfx::GFXglyph* _adfGlyphBridge = nullptr; @@ -494,7 +508,7 @@ inline void displayFirstPage() { inline void displayNextPage() { #ifdef DISPLAY_ST7796 if (initDisplayShadowBuffer()) { - getdisplay().pushSprite(0, 0); + getdisplay().pushSprite((480 - GxEPD_WIDTH) / 2, (320 - GxEPD_HEIGHT) / 2); } #else getdisplay().nextPage(); diff --git a/lib/obp60task/OBP60Hardware.h b/lib/obp60task/OBP60Hardware.h index 5aec5ac..416c81c 100644 --- a/lib/obp60task/OBP60Hardware.h +++ b/lib/obp60task/OBP60Hardware.h @@ -41,6 +41,8 @@ #define OBP_SPI_BUSY 42 #define OBP_SPI_CLK 38 #define OBP_SPI_DIN 48 + #define OBP_TFT_OFFSET_X 10 // ST7796 operating x-offset for centered 400x300 content + #define OBP_TFT_OFFSET_Y -20 // ST7796 operating y-offset for centered 400x300 content #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 diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index 9758c87..b26afca 100644 --- a/lib/obp60task/obp60task.cpp +++ b/lib/obp60task/obp60task.cpp @@ -391,7 +391,9 @@ void OBP60Task(GwApi *api){ #ifdef DISPLAY_ST7796 getpaneldisplay().setRotation(0); // Set display orientation (horizontal) + getpaneldisplay().setPanelOffset(0, 0); // Use full native framebuffer coordinates getpaneldisplay().fillScreen(0x0000); // Initialize full TFT screen to black (native RGB565) + getpaneldisplay().setPanelOffset(OBP_TFT_OFFSET_X, OBP_TFT_OFFSET_Y); // Restore centered 400x300 operating offset #else getdisplay().setRotation(0); // Set display orientation (horizontal) #endif From 2c2318adcc006a0a16f140a03e2abf1619883238 Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sun, 1 Mar 2026 17:14:46 +0000 Subject: [PATCH 44/75] Scale display output to max display hight and use antialiasing --- lib/obp60task/OBP60Extensions.h | 85 ++++++++++++++++++++++++++++++++- lib/obp60task/obp60task.cpp | 2 +- 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index f799788..d93b607 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -431,6 +431,39 @@ bool initDisplayShadowBuffer(); #define PAGE_UPDATE 1 // page wants display to update #define PAGE_HIBERNATE 2 // page wants displey to hibernate +#ifdef DISPLAY_ST7796 +#ifndef OBP_TFT_SCALE_ANTIALIAS +#define OBP_TFT_SCALE_ANTIALIAS 1 +#endif + +inline uint16_t lerpRgb565(uint16_t c0, uint16_t c1, uint16_t w8) { + const uint16_t r0 = (c0 >> 11) & 0x1F; + const uint16_t g0 = (c0 >> 5) & 0x3F; + const uint16_t b0 = c0 & 0x1F; + + const uint16_t r1 = (c1 >> 11) & 0x1F; + const uint16_t g1 = (c1 >> 5) & 0x3F; + const uint16_t b1 = c1 & 0x1F; + + const uint16_t r = static_cast(r0 + ((static_cast(r1) - r0) * w8 + 128) / 256); + const uint16_t g = static_cast(g0 + ((static_cast(g1) - g0) * w8 + 128) / 256); + const uint16_t b = static_cast(b0 + ((static_cast(b1) - b0) * w8 + 128) / 256); + + return static_cast((r << 11) | (g << 5) | b); +} + +inline uint16_t sampleBilinearRgb565(LGFXCanvas& src, uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t wx, uint16_t wy) { + const uint16_t c00 = src.readPixel(x0, y0); + const uint16_t c10 = src.readPixel(x1, y0); + const uint16_t c01 = src.readPixel(x0, y1); + const uint16_t c11 = src.readPixel(x1, y1); + + const uint16_t top = lerpRgb565(c00, c10, wx); + const uint16_t bot = lerpRgb565(c01, c11, wx); + return lerpRgb565(top, bot, wy); +} +#endif + // Draw monochrome bitmap on both E-Ink and TFT displays // supports various packing and bit orders; optional runtime conversion for TFT inline void drawMonochromeBitmap( @@ -508,7 +541,57 @@ inline void displayFirstPage() { inline void displayNextPage() { #ifdef DISPLAY_ST7796 if (initDisplayShadowBuffer()) { - getdisplay().pushSprite((480 - GxEPD_WIDTH) / 2, (320 - GxEPD_HEIGHT) / 2); + LGFXCanvas &src = getdisplay(); + LGFX &dst = getpaneldisplay(); + + const uint16_t srcW = GxEPD_WIDTH; + const uint16_t srcH = GxEPD_HEIGHT; + const uint16_t dstW = static_cast(dst.width()); + const uint16_t dstH = static_cast(dst.height()); + + const uint16_t targetH = (dstH < 320U) ? dstH : 320U; + const uint32_t scaledW32 = (static_cast(srcW) * targetH + (srcH / 2U)) / srcH; + const uint16_t targetW = static_cast((scaledW32 < dstW) ? scaledW32 : dstW); + + const uint16_t drawX = static_cast((dstW - targetW) / 2U); + const uint16_t drawY = static_cast((dstH - targetH) / 2U); + + dst.startWrite(); + const uint16_t borderColor = src.readPixel(0, 0); + if (drawX > 0) { + dst.fillRect(0, drawY, drawX, targetH, borderColor); + dst.fillRect(drawX + targetW, drawY, dstW - (drawX + targetW), targetH, borderColor); + } + + for (uint16_t y = 0; y < targetH; ++y) { + const uint32_t syfp = (targetH > 1) + ? (static_cast(y) * (srcH - 1) * 256U) / (targetH - 1) + : 0; + const uint16_t sy0 = static_cast(syfp >> 8); + const uint16_t sy1 = (sy0 + 1 < srcH) ? static_cast(sy0 + 1) : sy0; + const uint16_t wy = static_cast(syfp & 0xFFU); + + for (uint16_t x = 0; x < targetW; ++x) { + const uint32_t sxfp = (targetW > 1) + ? (static_cast(x) * (srcW - 1) * 256U) / (targetW - 1) + : 0; + const uint16_t sx0 = static_cast(sxfp >> 8); + const uint16_t sx1 = (sx0 + 1 < srcW) ? static_cast(sx0 + 1) : sx0; + + #if OBP_TFT_SCALE_ANTIALIAS + const uint16_t wx = static_cast(sxfp & 0xFFU); + const uint16_t color = sampleBilinearRgb565(src, sx0, sy0, sx1, sy1, wx, wy); + #else + const uint16_t color = src.readPixel(sx0, sy0); + #endif + + dst.drawPixel(drawX + x, drawY + y, color); + } + if ((y & 0x0F) == 0) { + yield(); + } + } + dst.endWrite(); } #else getdisplay().nextPage(); diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index b26afca..18fb479 100644 --- a/lib/obp60task/obp60task.cpp +++ b/lib/obp60task/obp60task.cpp @@ -393,7 +393,7 @@ void OBP60Task(GwApi *api){ getpaneldisplay().setRotation(0); // Set display orientation (horizontal) getpaneldisplay().setPanelOffset(0, 0); // Use full native framebuffer coordinates getpaneldisplay().fillScreen(0x0000); // Initialize full TFT screen to black (native RGB565) - getpaneldisplay().setPanelOffset(OBP_TFT_OFFSET_X, OBP_TFT_OFFSET_Y); // Restore centered 400x300 operating offset + getpaneldisplay().setPanelOffset(OBP_TFT_OFFSET_X, OBP_TFT_OFFSET_Y); // Restore configured operating panel offset #else getdisplay().setRotation(0); // Set display orientation (horizontal) #endif From 44e99cda50d7d60095b1d7b0888777d429784ed3 Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Sun, 1 Mar 2026 17:24:32 +0000 Subject: [PATCH 45/75] Add compiler flags for scaling and antialiasing --- lib/obp60task/OBP60Extensions.h | 13 +++++++++++++ lib/obp60task/platformio.ini | 2 ++ 2 files changed, 15 insertions(+) diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index d93b607..662b2c0 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -432,10 +432,15 @@ bool initDisplayShadowBuffer(); #define PAGE_HIBERNATE 2 // page wants displey to hibernate #ifdef DISPLAY_ST7796 +#ifndef OBP_TFT_ENABLE_SCALING +#define OBP_TFT_ENABLE_SCALING 1 +#endif + #ifndef OBP_TFT_SCALE_ANTIALIAS #define OBP_TFT_SCALE_ANTIALIAS 1 #endif +#if OBP_TFT_SCALE_ANTIALIAS inline uint16_t lerpRgb565(uint16_t c0, uint16_t c1, uint16_t w8) { const uint16_t r0 = (c0 >> 11) & 0x1F; const uint16_t g0 = (c0 >> 5) & 0x3F; @@ -463,6 +468,7 @@ inline uint16_t sampleBilinearRgb565(LGFXCanvas& src, uint16_t x0, uint16_t y0, return lerpRgb565(top, bot, wy); } #endif +#endif // Draw monochrome bitmap on both E-Ink and TFT displays // supports various packing and bit orders; optional runtime conversion for TFT @@ -549,6 +555,12 @@ inline void displayNextPage() { const uint16_t dstW = static_cast(dst.width()); const uint16_t dstH = static_cast(dst.height()); + #if !OBP_TFT_ENABLE_SCALING + const uint16_t drawX = static_cast((dstW > srcW) ? ((dstW - srcW) / 2U) : 0U); + const uint16_t drawY = static_cast((dstH > srcH) ? ((dstH - srcH) / 2U) : 0U); + src.pushSprite(drawX, drawY); + #else + const uint16_t targetH = (dstH < 320U) ? dstH : 320U; const uint32_t scaledW32 = (static_cast(srcW) * targetH + (srcH / 2U)) / srcH; const uint16_t targetW = static_cast((scaledW32 < dstW) ? scaledW32 : dstW); @@ -592,6 +604,7 @@ inline void displayNextPage() { } } dst.endWrite(); + #endif } #else getdisplay().nextPage(); diff --git a/lib/obp60task/platformio.ini b/lib/obp60task/platformio.ini index 71fbcbc..dd70278 100644 --- a/lib/obp60task/platformio.ini +++ b/lib/obp60task/platformio.ini @@ -59,6 +59,8 @@ 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_ST7796 #ST7796 TFT LCD display (480x320 color display) + -D OBP_TFT_ENABLE_SCALING=1 #TFT scaling on/off (1=scale to max Y=320, 0=no scaling) + -D OBP_TFT_SCALE_ANTIALIAS=1 #Antialiasing for TFT scaling (1=on, 0=off) # -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} From c5533ab0eff0981d4fbf236684c4ab6780d23e74 Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Wed, 4 Mar 2026 09:02:29 +0000 Subject: [PATCH 46/75] Add frame buffer for scaled picture --- lib/obp60task/OBP60Extensions.cpp | 28 ++++++++++++++ lib/obp60task/OBP60Extensions.h | 62 ++++++++++++++++--------------- 2 files changed, 61 insertions(+), 29 deletions(-) diff --git a/lib/obp60task/OBP60Extensions.cpp b/lib/obp60task/OBP60Extensions.cpp index eaec447..6566a62 100644 --- a/lib/obp60task/OBP60Extensions.cpp +++ b/lib/obp60task/OBP60Extensions.cpp @@ -60,10 +60,15 @@ GxEPD2_BW & getdisplay(){r // panel device + offscreen shadow framebuffer static LGFX panelDisplay; static LGFXCanvas shadowDisplay(&panelDisplay); +static LGFXCanvas scaleDisplay(&panelDisplay); static bool shadowDisplayInitialized = false; +static bool scaleDisplayInitialized = false; +static uint16_t scaleDisplayWidth = 0; +static uint16_t scaleDisplayHeight = 0; LGFXCanvas & getdisplay(){return shadowDisplay;} LGFX & getpaneldisplay(){return panelDisplay;} +LGFXCanvas & getscaleddisplay(){return scaleDisplay;} bool initDisplayShadowBuffer(){ if (shadowDisplayInitialized) return true; @@ -81,6 +86,29 @@ bool initDisplayShadowBuffer(){ shadowDisplayInitialized = true; return true; } + +bool initDisplayScaleBuffer(uint16_t width, uint16_t height){ + if (scaleDisplayInitialized && scaleDisplayWidth == width && scaleDisplayHeight == height) { + return true; + } + + scaleDisplay.deleteSprite(); + scaleDisplay.setPsram(true); + scaleDisplay.setColorDepth(16); + scaleDisplay.setTextDatum(textdatum_t::baseline_left); + + if (scaleDisplay.createSprite(width, height) == nullptr) { + scaleDisplayInitialized = false; + scaleDisplayWidth = 0; + scaleDisplayHeight = 0; + return false; + } + + scaleDisplayWidth = width; + scaleDisplayHeight = height; + scaleDisplayInitialized = true; + return true; +} #endif // Horter I2C moduls diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index 662b2c0..62c7c2a 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -423,7 +423,9 @@ private: LGFXCanvas & getdisplay(); LGFX & getpaneldisplay(); +LGFXCanvas & getscaleddisplay(); bool initDisplayShadowBuffer(); +bool initDisplayScaleBuffer(uint16_t width, uint16_t height); #endif // Page display return values @@ -568,42 +570,44 @@ inline void displayNextPage() { const uint16_t drawX = static_cast((dstW - targetW) / 2U); const uint16_t drawY = static_cast((dstH - targetH) / 2U); - dst.startWrite(); const uint16_t borderColor = src.readPixel(0, 0); - if (drawX > 0) { - dst.fillRect(0, drawY, drawX, targetH, borderColor); - dst.fillRect(drawX + targetW, drawY, dstW - (drawX + targetW), targetH, borderColor); - } + if (initDisplayScaleBuffer(dstW, dstH)) { + LGFXCanvas &scaled = getscaleddisplay(); + scaled.fillScreen(borderColor); - for (uint16_t y = 0; y < targetH; ++y) { - const uint32_t syfp = (targetH > 1) - ? (static_cast(y) * (srcH - 1) * 256U) / (targetH - 1) - : 0; - const uint16_t sy0 = static_cast(syfp >> 8); - const uint16_t sy1 = (sy0 + 1 < srcH) ? static_cast(sy0 + 1) : sy0; - const uint16_t wy = static_cast(syfp & 0xFFU); - - for (uint16_t x = 0; x < targetW; ++x) { - const uint32_t sxfp = (targetW > 1) - ? (static_cast(x) * (srcW - 1) * 256U) / (targetW - 1) + for (uint16_t y = 0; y < targetH; ++y) { + const uint32_t syfp = (targetH > 1) + ? (static_cast(y) * (srcH - 1) * 256U) / (targetH - 1) : 0; - const uint16_t sx0 = static_cast(sxfp >> 8); - const uint16_t sx1 = (sx0 + 1 < srcW) ? static_cast(sx0 + 1) : sx0; + const uint16_t sy0 = static_cast(syfp >> 8); + const uint16_t sy1 = (sy0 + 1 < srcH) ? static_cast(sy0 + 1) : sy0; + const uint16_t wy = static_cast(syfp & 0xFFU); - #if OBP_TFT_SCALE_ANTIALIAS - const uint16_t wx = static_cast(sxfp & 0xFFU); - const uint16_t color = sampleBilinearRgb565(src, sx0, sy0, sx1, sy1, wx, wy); - #else - const uint16_t color = src.readPixel(sx0, sy0); - #endif + for (uint16_t x = 0; x < targetW; ++x) { + const uint32_t sxfp = (targetW > 1) + ? (static_cast(x) * (srcW - 1) * 256U) / (targetW - 1) + : 0; + const uint16_t sx0 = static_cast(sxfp >> 8); + const uint16_t sx1 = (sx0 + 1 < srcW) ? static_cast(sx0 + 1) : sx0; - dst.drawPixel(drawX + x, drawY + y, color); - } - if ((y & 0x0F) == 0) { - yield(); + #if OBP_TFT_SCALE_ANTIALIAS + const uint16_t wx = static_cast(sxfp & 0xFFU); + const uint16_t color = sampleBilinearRgb565(src, sx0, sy0, sx1, sy1, wx, wy); + #else + const uint16_t color = src.readPixel(sx0, sy0); + #endif + + scaled.drawPixel(drawX + x, drawY + y, color); + } + if ((y & 0x0F) == 0) { + yield(); + } } + + scaled.pushSprite(0, 0); + } else { + src.pushSprite(drawX, drawY); } - dst.endWrite(); #endif } #else From a17db389fb4b6620cb0c6b22f526252b3e3c59b7 Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Wed, 4 Mar 2026 18:37:44 +0000 Subject: [PATCH 47/75] Fix for ImageDecoder.cpp --- lib/obp60task/ImageDecoder.cpp | 15 ++++++++++++--- lib/obp60task/ImageDecoder.h | 1 + lib/obp60task/PageNavigation.cpp | 15 ++++++++++++--- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/lib/obp60task/ImageDecoder.cpp b/lib/obp60task/ImageDecoder.cpp index 545ace6..25783d2 100644 --- a/lib/obp60task/ImageDecoder.cpp +++ b/lib/obp60task/ImageDecoder.cpp @@ -2,13 +2,22 @@ #include // Decoder for Base64 content -bool ImageDecoder::decodeBase64(const String& base64, uint8_t* outBuffer, size_t outSize, size_t& decodedSize) { +bool ImageDecoder::decodeBase64(const char* base64, size_t base64Len, uint8_t* outBuffer, size_t outSize, size_t& decodedSize) { + if (base64 == nullptr) { + decodedSize = 0; + return false; + } int ret = mbedtls_base64_decode( outBuffer, outSize, &decodedSize, - (const unsigned char*)base64.c_str(), - base64.length() + (const unsigned char*)base64, + base64Len ); return (ret == 0); } + +// Decoder for Base64 content +bool ImageDecoder::decodeBase64(const String& base64, uint8_t* outBuffer, size_t outSize, size_t& decodedSize) { + return decodeBase64(base64.c_str(), base64.length(), outBuffer, outSize, decodedSize); +} diff --git a/lib/obp60task/ImageDecoder.h b/lib/obp60task/ImageDecoder.h index 473b168..5bae456 100644 --- a/lib/obp60task/ImageDecoder.h +++ b/lib/obp60task/ImageDecoder.h @@ -5,5 +5,6 @@ class ImageDecoder { public: + bool decodeBase64(const char* base64, size_t base64Len, uint8_t* outBuffer, size_t outSize, size_t& decodedSize); bool decodeBase64(const String& base64, uint8_t* outBuffer, size_t outSize, size_t& decodedSize); }; diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp index 262bce1..9924f84 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -4,6 +4,7 @@ #include "OBP60Extensions.h" #include "NetworkClient.h" // Network connection #include "ImageDecoder.h" // Image decoder for navigation map +#include #include "Logo_OBP_400x300_sw.h" @@ -432,13 +433,21 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map // Decode Base64 content to image size_t decodedSize = 0; - bool decodeOk = decoder.decodeBase64(b64, imageData, imgSize, decodedSize); + bool decodeOk = decoder.decodeBase64(b64, b64len, imageData, imgSize, decodedSize); if (!decodeOk || decodedSize < requiredBytes){ + int base64Ret = mbedtls_base64_decode( + nullptr, + 0, + &decodedSize, + (const unsigned char*)b64, + b64len + ); LOG_DEBUG(GwLog::ERROR, - "Error PageNavigation: decode failed (ok=%d, decoded=%u, required=%u)", + "Error PageNavigation: decode failed (ok=%d, decoded=%u, required=%u, b64ret=%d)", decodeOk ? 1 : 0, (unsigned int)decodedSize, - (unsigned int)requiredBytes + (unsigned int)requiredBytes, + base64Ret ); free(b64); free(imageData); From 5f161ce839512475c1bbe4497ce4ed34a157b567 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 6 Mar 2026 10:39:18 +0100 Subject: [PATCH 48/75] Modify platformio.ini --- lib/obp60task/platformio.ini | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/obp60task/platformio.ini b/lib/obp60task/platformio.ini index dd70278..1c68782 100644 --- a/lib/obp60task/platformio.ini +++ b/lib/obp60task/platformio.ini @@ -55,12 +55,12 @@ build_flags= # -D HARDWARE_V20 #OBP60 hardware revision V2.0 -D HARDWARE_V21 #OBP60 hardware revision V2.1 # -D DISPLAY_GDEW042T2 #old E-Ink display from GoodDisplay (Waveshare), R10 0.47 ohm - very good -# -D DISPLAY_GDEY042T81 #new E-Ink display from GoodDisplay (Waveshare), R10 2.2 ohm - good (contast lost by shunshine) + -D DISPLAY_GDEY042T81 #new E-Ink display from GoodDisplay (Waveshare), R10 2.2 ohm - good (contast lost by shunshine) # -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_ST7796 #ST7796 TFT LCD display (480x320 color display) - -D OBP_TFT_ENABLE_SCALING=1 #TFT scaling on/off (1=scale to max Y=320, 0=no scaling) - -D OBP_TFT_SCALE_ANTIALIAS=1 #Antialiasing for TFT scaling (1=on, 0=off) +# -D DISPLAY_ST7796 #ST7796 TFT LCD display (480x320 color display) +# -D OBP_TFT_ENABLE_SCALING=1 #TFT scaling on/off (1=scale to max Y=320, 0=no scaling) +# -D OBP_TFT_SCALE_ANTIALIAS=1 #Antialiasing for TFT scaling (1=on, 0=off) # -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} From bc1440c428d07794916663f259117acefe563fa3 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 6 Mar 2026 14:48:55 +0100 Subject: [PATCH 49/75] Add code_size.sh --- lib/obp60task/Code_Size.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 lib/obp60task/Code_Size.sh diff --git a/lib/obp60task/Code_Size.sh b/lib/obp60task/Code_Size.sh new file mode 100644 index 0000000..7bff89c --- /dev/null +++ b/lib/obp60task/Code_Size.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +dir="$1" + +total=0 + +while IFS= read -r -d '' file; do + lines=$(wc -l < "$file") + echo "$file : $lines" + total=$((total + lines)) +done < <(find "$dir" \( -name "*.c" -o -name "*.cpp" -o -name "*.h" \) -type f -print0) + +echo "-----------------------" +echo "Over all files: $total" \ No newline at end of file From af9bca4d6616ab2c4c41a9ffb52bf4a8c9823dae Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 6 Mar 2026 14:59:06 +0100 Subject: [PATCH 50/75] Add power shell script --- lib/obp60task/code_size.ps1 | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 lib/obp60task/code_size.ps1 diff --git a/lib/obp60task/code_size.ps1 b/lib/obp60task/code_size.ps1 new file mode 100644 index 0000000..0cf586b --- /dev/null +++ b/lib/obp60task/code_size.ps1 @@ -0,0 +1,12 @@ +param([string]$dir = ".") + +$total = 0 + +Get-ChildItem $dir -Recurse -Include *.c, *.cpp, *.h -File | ForEach-Object { + $lines = [System.IO.File]::ReadLines($_.FullName).Count + Write-Output "$($_.FullName) : $lines" + $total += $lines +} + +Write-Output "-----------------------" +Write-Output "Over all files: $total" \ No newline at end of file From ec667c18f5441ac88e6174737c2105816b575c32 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 6 Mar 2026 15:20:47 +0100 Subject: [PATCH 51/75] Fix for power shell script --- lib/obp60task/code_size.ps1 | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/obp60task/code_size.ps1 b/lib/obp60task/code_size.ps1 index 0cf586b..3c1a0c7 100644 --- a/lib/obp60task/code_size.ps1 +++ b/lib/obp60task/code_size.ps1 @@ -1,12 +1,16 @@ -param([string]$dir = ".") +param( + [string]$dir = "." +) $total = 0 -Get-ChildItem $dir -Recurse -Include *.c, *.cpp, *.h -File | ForEach-Object { - $lines = [System.IO.File]::ReadLines($_.FullName).Count +Get-ChildItem -Path $dir -Recurse -File | Where-Object { + $_.Extension -in ".c", ".cpp", ".h" +} | ForEach-Object { + $lines = [System.Linq.Enumerable]::Count([System.IO.File]::ReadLines($_.FullName)) Write-Output "$($_.FullName) : $lines" $total += $lines } -Write-Output "-----------------------" +Write-Output "-----------------------------" Write-Output "Over all files: $total" \ No newline at end of file From 86cd1ed97ca63607789fa1c1a372d1cb47477987 Mon Sep 17 00:00:00 2001 From: Thomas Hooge Date: Sun, 8 Mar 2026 15:22:26 +0100 Subject: [PATCH 52/75] Add missing functions to LedSpiTask --- lib/obp60task/LedSpiTask.cpp | 26 +++++++++++++++++++++++++- lib/obp60task/LedSpiTask.h | 4 +++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/lib/obp60task/LedSpiTask.cpp b/lib/obp60task/LedSpiTask.cpp index 74497e7..506c457 100644 --- a/lib/obp60task/LedSpiTask.cpp +++ b/lib/obp60task/LedSpiTask.cpp @@ -14,6 +14,30 @@ https://controllerstech.com/ws2812-leds-using-spi/ */ +String Color::toHex() { + char hexColor[8]; + sprintf(hexColor, "#%02X%02X%02X", r, g, b); + return String(hexColor); +} + +String Color::toName() { + static std::map const names = { + {0xff0000, "Red"}, + {0x00ff00, "Green"}, + {0x0000ff, "Blue",}, + {0xff9900, "Orange"}, + {0xffff00, "Yellow"}, + {0x3366ff, "Aqua"}, + {0xff0066, "Violet"}, + {0xffffff, "White"} + }; + int color = (r << 16) + (g << 8) + b; + auto it = names.find(color); + if (it == names.end()) { + return toHex(); + } + return it->second; +} static uint8_t mulcolor(uint8_t f1, uint8_t f2){ uint16_t rt=f1; @@ -231,4 +255,4 @@ void handleSpiLeds(void *param){ void createSpiLedTask(LedTaskData *param){ xTaskCreate(handleSpiLeds,"handleLeds",4000,param,3,NULL); -} \ No newline at end of file +} diff --git a/lib/obp60task/LedSpiTask.h b/lib/obp60task/LedSpiTask.h index c058503..ff63919 100644 --- a/lib/obp60task/LedSpiTask.h +++ b/lib/obp60task/LedSpiTask.h @@ -10,7 +10,7 @@ class Color{ uint8_t g; uint8_t b; Color():r(0),g(0),b(0){} - Color(uint8_t cr, uint8_t cg,uint8_t cb): + Color(uint8_t cr, uint8_t cg, uint8_t cb): b(cb),g(cg),r(cr){} Color(const Color &o):b(o.b),g(o.g),r(o.r){} bool equal(const Color &o) const{ @@ -22,6 +22,8 @@ class Color{ bool operator != (const Color &other) const{ return ! equal(other); } + String toHex(); + String toName(); }; static Color COLOR_GREEN=Color(0,255,0); From cd84e427a3ac18f8c85831011e9d9a5cde94fb41 Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Thu, 12 Mar 2026 17:33:37 +0000 Subject: [PATCH 53/75] Color navigation map for PageNavigation. --- lib/obp60task/PageNavigation.cpp | 112 ++++++++++++++++++++++++++++--- 1 file changed, 101 insertions(+), 11 deletions(-) diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp index 9924f84..55647d1 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -13,6 +13,54 @@ NetworkClient net(JSON_BUFFER); // Define network client ImageDecoder decoder; // Define image decoder +#ifdef DISPLAY_ST7796 +// Set to true to render a generated RGB565 color-bar test image. +static constexpr bool kShowRgb565StripeTestImage = true; + +static void drawRgb565Image(int16_t x, int16_t y, const uint16_t *img, int16_t w, int16_t h) { + if (img == nullptr || w <= 0 || h <= 0) { + return; + } + for (int16_t yy = 0; yy < h; ++yy) { + const uint16_t *row = img + ((size_t)yy * (size_t)w); + for (int16_t xx = 0; xx < w; ++xx) { + getdisplay().drawPixel(x + xx, y + yy, row[xx]); + } + if ((yy & 0x0F) == 0) { + yield(); + } + } +} + +static void createRgb565StripeImage(uint16_t *img, int16_t w, int16_t h) { + if (img == nullptr || w <= 0 || h <= 0) { + return; + } + static const uint16_t stripes[] = { + 0xF800, // red + 0xFD20, // orange + 0xFFE0, // yellow + 0x07E0, // green + 0x07FF, // cyan + 0x001F, // blue + 0xF81F, // magenta + 0xFFFF // white + }; + const int stripeCount = (int)(sizeof(stripes) / sizeof(stripes[0])); + + for (int16_t y = 0; y < h; ++y) { + uint16_t *row = img + ((size_t)y * (size_t)w); + for (int16_t x = 0; x < w; ++x) { + int idx = ((int)x * stripeCount) / (int)w; + if (idx >= stripeCount) { + idx = stripeCount - 1; + } + row[x] = stripes[idx]; + } + } +} +#endif + class PageNavigation : public Page { // Values for buttons @@ -25,13 +73,19 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map int imageBackupWidth = 0; int imageBackupHeight = 0; size_t imageBackupSize = 0; + size_t imageBackupCapacity = 0; bool hasImageBackup = false; + bool imageBackupIsRgb565 = 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); + imageBackupCapacity = (size_t)GxEPD_WIDTH * (size_t)GxEPD_HEIGHT; + #ifdef DISPLAY_ST7796 + imageBackupCapacity *= 2U; + #endif + imageBackupData = (uint8_t*)heap_caps_malloc(imageBackupCapacity, MALLOC_CAP_SPIRAM); } // Set botton labels @@ -395,11 +449,13 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map int numPix = json["number_pixels"] | 0; // Read number of pixels imgWidth = json["width"] | 0; // Read width of image imgHeight = json["height"] | 0; // Read height og image - size_t requiredBytes = 0; + size_t requiredBytesMono = 0; + size_t requiredBytesRgb565 = 0; if (imgWidth > 0 && imgHeight > 0){ - requiredBytes = (size_t)((imgWidth + 7) / 8) * (size_t)imgHeight; + requiredBytesMono = (size_t)((imgWidth + 7) / 8) * (size_t)imgHeight; + requiredBytesRgb565 = (size_t)imgWidth * (size_t)imgHeight * 2U; } - if (requiredBytes == 0){ + if (requiredBytesMono == 0){ LOG_DEBUG(GwLog::ERROR,"Error PageNavigation: invalid image geometry w=%d h=%d",imgWidth,imgHeight); return PAGE_UPDATE; } @@ -420,10 +476,15 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map // Set image buffer in PSRAM //size_t imgSize = getdisplay().width() * getdisplay().height(); - size_t imgSize = (numPix > 0) ? (size_t)numPix : requiredBytes; // Calculate image size - if (imgSize < requiredBytes){ - imgSize = requiredBytes; + size_t imgSize = (numPix > 0) ? (size_t)numPix : requiredBytesMono; // Calculate image size + if (imgSize < requiredBytesMono){ + imgSize = requiredBytesMono; } + #ifdef DISPLAY_ST7796 + if (imgSize < requiredBytesRgb565){ + imgSize = requiredBytesRgb565; + } + #endif uint8_t* imageData = (uint8_t*) heap_caps_malloc(imgSize, MALLOC_CAP_SPIRAM); // Allocate PSRAM for image if (!imageData) { LOG_DEBUG(GwLog::ERROR,"Error PageNavigation: PSRAM alloc image buffer failed"); @@ -434,7 +495,7 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map // Decode Base64 content to image size_t decodedSize = 0; bool decodeOk = decoder.decodeBase64(b64, b64len, imageData, imgSize, decodedSize); - if (!decodeOk || decodedSize < requiredBytes){ + if (!decodeOk || decodedSize < requiredBytesMono){ int base64Ret = mbedtls_base64_decode( nullptr, 0, @@ -446,7 +507,7 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map "Error PageNavigation: decode failed (ok=%d, decoded=%u, required=%u, b64ret=%d)", decodeOk ? 1 : 0, (unsigned int)decodedSize, - (unsigned int)requiredBytes, + (unsigned int)requiredBytesMono, base64Ret ); free(b64); @@ -454,21 +515,42 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map return PAGE_UPDATE; } + bool imageIsRgb565 = false; + #ifdef DISPLAY_ST7796 + imageIsRgb565 = (decodedSize >= requiredBytesRgb565); + #endif + + #ifdef DISPLAY_ST7796 + if (kShowRgb565StripeTestImage) { + createRgb565StripeImage(reinterpret_cast(imageData), imgWidth, imgHeight); + decodedSize = requiredBytesRgb565; + imageIsRgb565 = true; + } + #endif + // Copy actual navigation man to ackup map imageBackupWidth = imgWidth; imageBackupHeight = imgHeight; imageBackupSize = imgSize; if (decodedSize > 0 && imageBackupData != nullptr) { - size_t backupCapacity = (size_t)GxEPD_WIDTH * (size_t)GxEPD_HEIGHT; - size_t copySize = (decodedSize > backupCapacity) ? backupCapacity : decodedSize; + size_t copySize = (decodedSize > imageBackupCapacity) ? imageBackupCapacity : decodedSize; memcpy(imageBackupData, imageData, copySize); imageBackupSize = copySize; } + imageBackupIsRgb565 = imageIsRgb565; hasImageBackup = (imageBackupData != nullptr); lostCounter = 0; // Show image (navigation map) + #ifdef DISPLAY_ST7796 + if (imageIsRgb565) { + drawRgb565Image(0, 25, reinterpret_cast(imageData), imgWidth, imgHeight); + } else { + displayDrawBitmap(0, 25, imageData, imgWidth, imgHeight, commonData->fgcolor); + } + #else displayDrawBitmap(0, 25, imageData, imgWidth, imgHeight, commonData->fgcolor); + #endif // Clean PSRAM free(b64); @@ -491,7 +573,15 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map // Show backup image (backup navigation map) if (hasImageBackup) { + #ifdef DISPLAY_ST7796 + if (imageBackupIsRgb565) { + drawRgb565Image(0, 25, reinterpret_cast(imageBackupData), imageBackupWidth, imageBackupHeight); + } else { + displayDrawBitmap(0, 25, imageBackupData, imageBackupWidth, imageBackupHeight, commonData->fgcolor); + } + #else displayDrawBitmap(0, 25, imageBackupData, imageBackupWidth, imageBackupHeight, commonData->fgcolor); + #endif } // Show connection lost info when 5 page refreshes has a connection lost to the map server From 64f9058f858b9d24ea5fbf39cd877018436b9fcb Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Thu, 12 Mar 2026 19:32:10 +0100 Subject: [PATCH 54/75] Activate color map --- lib/obp60task/PageNavigation.cpp | 30 ++++++++++++++++++------------ lib/obp60task/platformio.ini | 8 ++++---- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp index 55647d1..dcd6506 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -15,7 +15,7 @@ ImageDecoder decoder; // Define image decoder #ifdef DISPLAY_ST7796 // Set to true to render a generated RGB565 color-bar test image. -static constexpr bool kShowRgb565StripeTestImage = true; +static constexpr bool kShowRgb565StripeTestImage = false; static void drawRgb565Image(int16_t x, int16_t y, const uint16_t *img, int16_t w, int16_t h) { if (img == nullptr || w <= 0 || h <= 0) { @@ -402,22 +402,28 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map // 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 + // Default zoom level: 15 + String("/get_image_json?") + // Service: Output B&W picture as JSON (Base64 + gzip) +// "oformat=4" + // Image output format in JSON: 4=b/w 1-Bit format + "oformat=3" + // Image output format in JSON: 3=RGB565 format + "&zoom=" + zoom + // Default zoom level: 15 "&lat=" + String(latitude, 6) + // Latitude "&lon=" + String(longitude, 6) + // Longitude "&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 + "&itype=1" + // Image type: 1=Color +// "&itype=2" + // Image type: 2=Gray scale +// "&itype=4" + // Image type: 4=b/w with dithering + "&dtype=" + dType + // Dithering type: Atkinson dithering (only activ when itype=4 otherwise inactive) + "&width=400" + // With navigation map + "&height=250" + // Height navigation map + "&cutout=0" + // No picture cutouts (tab, border and alpha are unused when cutout=0) + "&tab=0" + // No tab size (only available when sqare cutouts selected coutout=3...7) + "&border=2" + // Border line size: 2 pixel (only available when sqare cutouts selected) + "&alpha=80" + // Alpha for tabs: 80% visible (only available when sqare cutouts selected) + "&symbol=2" + // Symbol: Triangle "&srot=" + symbolRot + // Symbol rotation angle - "&ssize=15" + // Symbole size: 15 pixel - "&grid=" + mapGrid // Show grid: On + "&ssize=15" + // Symbole size: 15 pixel (center pointer) + "&grid=" + mapGrid // Show grid: On ; // Draw page diff --git a/lib/obp60task/platformio.ini b/lib/obp60task/platformio.ini index 1c68782..71046aa 100644 --- a/lib/obp60task/platformio.ini +++ b/lib/obp60task/platformio.ini @@ -55,12 +55,12 @@ build_flags= # -D HARDWARE_V20 #OBP60 hardware revision V2.0 -D HARDWARE_V21 #OBP60 hardware revision V2.1 # -D DISPLAY_GDEW042T2 #old E-Ink display from GoodDisplay (Waveshare), R10 0.47 ohm - very good - -D DISPLAY_GDEY042T81 #new E-Ink display from GoodDisplay (Waveshare), R10 2.2 ohm - good (contast lost by shunshine) +# -D DISPLAY_GDEY042T81 #new E-Ink display from GoodDisplay (Waveshare), R10 2.2 ohm - good (contast lost by shunshine) # -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_ST7796 #ST7796 TFT LCD display (480x320 color display) -# -D OBP_TFT_ENABLE_SCALING=1 #TFT scaling on/off (1=scale to max Y=320, 0=no scaling) -# -D OBP_TFT_SCALE_ANTIALIAS=1 #Antialiasing for TFT scaling (1=on, 0=off) + -D DISPLAY_ST7796 #ST7796 TFT LCD display (480x320 color display) + -D OBP_TFT_ENABLE_SCALING=1 #TFT scaling on/off (1=scale to max Y=320, 0=no scaling) + -D OBP_TFT_SCALE_ANTIALIAS=1 #Antialiasing for TFT scaling (1=on, 0=off) # -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} From 57272f3a44d81e484caf6e903478314ad6b6d903 Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Thu, 12 Mar 2026 18:40:36 +0000 Subject: [PATCH 55/75] Fix for PageNavigation --- lib/obp60task/NetworkClient.cpp | 27 ++++++++++++++++++++++++--- lib/obp60task/NetworkClient.h | 3 +++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/lib/obp60task/NetworkClient.cpp b/lib/obp60task/NetworkClient.cpp index b8ebaee..371f3b4 100644 --- a/lib/obp60task/NetworkClient.cpp +++ b/lib/obp60task/NetworkClient.cpp @@ -10,10 +10,20 @@ extern "C" { // Constructor NetworkClient::NetworkClient(size_t reserveSize) : _doc(reserveSize), - _valid(false) + _valid(false), + _jsonRaw(nullptr), + _jsonRawLen(0) { } +NetworkClient::~NetworkClient() { + if (_jsonRaw != nullptr) { + free(_jsonRaw); + _jsonRaw = nullptr; + _jsonRawLen = 0; + } +} + // Skip GZIP Header an goto DEFLATE content int NetworkClient::skipGzipHeader(const uint8_t* data, size_t len) { if (len < 10) return -1; @@ -324,6 +334,13 @@ bool NetworkClient::httpGetGzip(const String& url, uint8_t*& outData, size_t& ou bool NetworkClient::fetchAndDecompressJson(const String& url) { _valid = false; + _doc.clear(); + + if (_jsonRaw != nullptr) { + free(_jsonRaw); + _jsonRaw = nullptr; + _jsonRawLen = 0; + } uint8_t* raw = nullptr; size_t rawLen = 0; @@ -333,14 +350,18 @@ bool NetworkClient::fetchAndDecompressJson(const String& url) { return false; } - DeserializationError err = deserializeJson(_doc, raw, rawLen); - free(raw); + // Parse in zero-copy mode and keep the backing buffer alive in the class. + DeserializationError err = deserializeJson(_doc, reinterpret_cast(raw), rawLen); if (err) { Serial.printf("JSON ERROR: %s\n", err.c_str()); + free(raw); return false; } + _jsonRaw = raw; + _jsonRawLen = rawLen; + if (DEBUGING) {Serial.println("JSON OK!");} _valid = true; return true; diff --git a/lib/obp60task/NetworkClient.h b/lib/obp60task/NetworkClient.h index 5d3f448..80e0703 100644 --- a/lib/obp60task/NetworkClient.h +++ b/lib/obp60task/NetworkClient.h @@ -12,6 +12,7 @@ class NetworkClient { public: NetworkClient(size_t reserveSize = 0); + ~NetworkClient(); bool fetchAndDecompressJson(const String& url); JsonDocument& json(); @@ -20,6 +21,8 @@ public: private: DynamicJsonDocument _doc; bool _valid; + uint8_t* _jsonRaw; + size_t _jsonRawLen; int skipGzipHeader(const uint8_t* data, size_t len); bool httpGetGzip(const String& url, uint8_t*& outData, size_t& outLen); From a0afd5deca9cd5c95db51cc5a06f11f0334af94d Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Thu, 12 Mar 2026 18:49:53 +0000 Subject: [PATCH 56/75] Fix for NetworkCleint.cpp --- lib/obp60task/NetworkClient.cpp | 46 +++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/lib/obp60task/NetworkClient.cpp b/lib/obp60task/NetworkClient.cpp index 371f3b4..96a649d 100644 --- a/lib/obp60task/NetworkClient.cpp +++ b/lib/obp60task/NetworkClient.cpp @@ -292,23 +292,43 @@ bool NetworkClient::httpGetGzip(const String& url, uint8_t*& outData, size_t& ou if (headerOffset < 0) { aborting = true; } else { - unsigned long testLen = len * 8; // Dynamic expansion - uint8_t* test = (uint8_t*)malloc(testLen); + size_t deflateLen = len - (size_t)headerOffset; + // GZIP trailer (CRC32 + ISIZE) is 8 bytes and not part of deflate stream. + if (deflateLen >= 8) { + deflateLen -= 8; + } - if (!test) { - Serial.println("Malloc failed test buffer, aborting."); + unsigned long srcLenForSize = (unsigned long)deflateLen; + unsigned long outNeeded = 0; + int sizeRes = puff(NIL, &outNeeded, buffer + headerOffset, &srcLenForSize); + + if (sizeRes != 0) { + if (DEBUGING) { + Serial.printf("Decompress size probe failed: res=%d src=%lu\n", sizeRes, srcLenForSize); + } aborting = true; } else { - unsigned long srcLen = (unsigned long)(len - (size_t)headerOffset); - int res = puff(test, &testLen, buffer + headerOffset, &srcLen); - - if (res == 0) { - if (DEBUGING) {Serial.printf("Decompress OK! Size: %lu bytes\n", testLen);} - outData = test; - outLen = (size_t)testLen; - complete = true; + uint8_t* test = (uint8_t*)malloc((size_t)outNeeded); + if (!test) { + Serial.println("Malloc failed test buffer, aborting."); + aborting = true; } else { - free(test); + unsigned long srcLen = (unsigned long)deflateLen; + unsigned long testLen = outNeeded; + int res = puff(test, &testLen, buffer + headerOffset, &srcLen); + + if (res == 0) { + if (DEBUGING) {Serial.printf("Decompress OK! Size: %lu bytes\n", testLen);} + outData = test; + outLen = (size_t)testLen; + complete = true; + } else { + if (DEBUGING) { + Serial.printf("Decompress failed: res=%d out=%lu src=%lu\n", res, testLen, srcLen); + } + free(test); + aborting = true; + } } } } From 12fc96210777282741b5cd591234fe9b9a6a6cdc Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 13 Mar 2026 15:44:08 +0100 Subject: [PATCH 57/75] Add color replacement for b/w --- lib/obp60task/OBP60Extensions.h | 14 +++++++++----- lib/obp60task/OBP60Hardware.h | 2 ++ lib/obp60task/obp60task.cpp | 1 - 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index 62c7c2a..c6db616 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -5,18 +5,22 @@ #include "OBP60Hardware.h" #include "LedSpiTask.h" #include "Graphics.h" -#include // E-paper lib V2 +#include // GxEPD2 lib for b/w E-Ink displays #include // I2C FRAM #include #ifdef DISPLAY_ST7796 -#include // TFT LCD lib for ST7796 + #include // TFT LCD lib for ST7796 color displays + #undef GxEPD_WHITE + #define GxEPD_WHITE TFT_WHITE // Replacement color for white on TFT (OBPHardware.h) + #undef GxEPD_BLACK + #define GxEPD_BLACK TFT_BLACK // Replacement color for black on TFT (OBPHardware.h) #endif #ifdef BOARD_OBP40S3 -#include "esp_vfs_fat.h" -#include "sdmmc_cmd.h" -#define MOUNT_POINT "/sdcard" + #include "esp_vfs_fat.h" + #include "sdmmc_cmd.h" + #define MOUNT_POINT "/sdcard" #endif // FRAM address reservations 32kB: 0x0000 - 0x7FFF diff --git a/lib/obp60task/OBP60Hardware.h b/lib/obp60task/OBP60Hardware.h index 416c81c..0884b70 100644 --- a/lib/obp60task/OBP60Hardware.h +++ b/lib/obp60task/OBP60Hardware.h @@ -43,6 +43,8 @@ #define OBP_SPI_DIN 48 #define OBP_TFT_OFFSET_X 10 // ST7796 operating x-offset for centered 400x300 content #define OBP_TFT_OFFSET_Y -20 // ST7796 operating y-offset for centered 400x300 content + #define TFT_BLACK 0x0109 // Replacement color for black on TFT (RGB565) + #define TFT_WHITE 0xFFFF // Replacement color for white on TFT (RGB565) #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 diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index 18fb479..ef29b35 100644 --- a/lib/obp60task/obp60task.cpp +++ b/lib/obp60task/obp60task.cpp @@ -9,7 +9,6 @@ #include // NMEA0183 #include #include -#include // GxEPD2 lib for b/w E-Ink displays #include "OBP60Extensions.h" // Functions lib for extension board #include "OBP60Keypad.h" // Functions for keypad #include "OBPDataOperations.h" // Functions lib for data operations such as true wind calculation From 019f7a11ef1385ff4da91e3966bbfbc87b732296 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 13 Mar 2026 16:59:02 +0100 Subject: [PATCH 58/75] Add device OBP70 in paltformio.ini and config_obp70.json --- lib/obp60task/Changes_to_original.txt | 2 +- lib/obp60task/PageNavigation.cpp | 13 +- lib/obp60task/config_obp70.json | 4141 +++++++++++++++++++++++++ lib/obp60task/platformio.ini | 64 +- 4 files changed, 4210 insertions(+), 10 deletions(-) create mode 100644 lib/obp60task/config_obp70.json diff --git a/lib/obp60task/Changes_to_original.txt b/lib/obp60task/Changes_to_original.txt index b283d8e..1b59dab 100644 --- a/lib/obp60task/Changes_to_original.txt +++ b/lib/obp60task/Changes_to_original.txt @@ -1,6 +1,6 @@ Changes to original project (wellenvogel) -* esp32-nmea2000-obp60/gwwifi/GwWifi.CPPDEFINES +* esp32-nmea2000-obp60/gwwifi/GwWifi.cpp - any fixes for reconnect handling * GWStatisticsw.h - changed time source for log messages diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp index dcd6506..44045fd 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -403,16 +403,21 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map // 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) -// "oformat=4" + // Image output format in JSON: 4=b/w 1-Bit format - "oformat=3" + // Image output format in JSON: 3=RGB565 format + #ifdef DISPLAY_ST7796 + "oformat=3" + // Image output format in JSON: 3=RGB565 format + #else + "oformat=4" + // Image output format in JSON: 4=b/w 1-Bit format + #endif "&zoom=" + zoom + // Default zoom level: 15 "&lat=" + String(latitude, 6) + // Latitude "&lon=" + String(longitude, 6) + // Longitude "&mrot=" + mapRot + // Rotation angle navigation map in degree "&mtype=" + mType + // Default Map: Open Street Map + #ifdef DISPLAY_ST7796 "&itype=1" + // Image type: 1=Color -// "&itype=2" + // Image type: 2=Gray scale -// "&itype=4" + // Image type: 4=b/w with dithering + #else + "&itype=4" + // Image type: 4=b/w with dithering + #endif "&dtype=" + dType + // Dithering type: Atkinson dithering (only activ when itype=4 otherwise inactive) "&width=400" + // With navigation map "&height=250" + // Height navigation map diff --git a/lib/obp60task/config_obp70.json b/lib/obp60task/config_obp70.json new file mode 100644 index 0000000..58cbe4d --- /dev/null +++ b/lib/obp60task/config_obp70.json @@ -0,0 +1,4141 @@ +[ + { + "name": "deviceName", + "label": "system name", + "type": "string", + "default": "OBP70", + "check": "checkSystemName", + "description": "system name, used for the access point and for services", + "category": "system" + }, + { + "name": "timeServer", + "label": "time server", + "type": "string", + "default": "pool.ntp.org", + "description": "NTP time server. Use only one hostname or IP address", + "category": "wifi client", + "capabilities": { + "obp70": "true" + } + }, + { + "name": "timeZone", + "label": "Time Zone", + "type": "number", + "default": "0.00", + "check": "checkMinMax", + "min": -12.00, + "max": 14.00, + "description": "Time zone [UTC -12...+14]", + "category": "OBP70 Settings", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "homeLAT", + "label": "Home latitude", + "type": "number", + "default": "0.00000", + "check": "checkMinMax", + "min": -90.0, + "max": 90.0, + "description": "Latitude of boat home location [-90.0...+90.0]", + "category": "OBP70 Settings", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "homeLON", + "label": "Home longitude", + "type": "number", + "default": "0.00000", + "check": "checkMinMax", + "min": -180.0, + "max": 180.0, + "description": "Longitude of boat home location [-180.0...+180.0]", + "category": "OBP70 Settings", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "draft", + "label": "Boat Draft [m]", + "type": "number", + "default": "0.00", + "check": "checkMinMax", + "min": 0.00, + "max": 50.00, + "description": "The draft of the boat [0...50m]", + "category": "OBP70 Settings", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "fuelTank", + "label": "Fuel Tank [l]", + "type": "number", + "default": "0", + "check": "checkMinMax", + "min": 0, + "max": 5000, + "description": "Fuel tank capacity [0...5000l]", + "category": "OBP70 Settings", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "fuelConsumption", + "label": "Fuel Consuption [l/h]", + "type": "number", + "default": "0.00", + "check": "checkMinMax", + "min": 0.00, + "max": 1000.00, + "description": "Medium fuel consumption [0...1000l/h]", + "category": "OBP70 Settings", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "waterTank", + "label": "Water Tank [l]", + "type": "number", + "default": "0", + "check": "checkMinMax", + "min": 0, + "max": 5000, + "description": "Water tank capacity [0...5000l]", + "category": "OBP70 Settings", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "wasteTank", + "label": "Waste Tank [l]", + "type": "number", + "default": "0", + "check": "checkMinMax", + "min": 0, + "max": 5000, + "description": "Waste tank capacity [0...5000l]", + "category": "OBP70 Settings", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "batteryVoltage", + "label": "Battery Voltage [V]", + "type": "list", + "default": "12V", + "description": "Battery Voltage [12V|24V]", + "list": [ + "12V", + "24V" + ], + "category": "OBP70 Settings", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "batteryType", + "label": "Battery Type", + "type": "list", + "default": "Pb", + "description": "Type of battery [Pb|Gel|AGM|LiFePo4]", + "list": [ + "Pb", + "Gel", + "AGM", + "LiFePo4" + ], + "category": "OBP70 Settings", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "batteryCapacity", + "label": "Battery Capacity [Ah]", + "type": "number", + "default": "0.0", + "check": "checkMinMax", + "min": 0.0, + "max": 10000.0, + "description": "Battery capacity [0...10000Ah]", + "category": "OBP70 Settings", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "solarPower", + "label": "Solar Power [W]", + "type": "number", + "default": "0.0", + "check": "checkMinMax", + "min": 0.0, + "max": 10000.0, + "description": "Solar power [0...10000W]", + "category": "OBP70 Settings", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "genPower", + "label": "Genarator Power [W]", + "type": "number", + "default": "0.0", + "check": "checkMinMax", + "min": 0.0, + "max": 10000.0, + "description": "Generator power [0...10000W]", + "category": "OBP70 Settings", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "trackStep", + "label": "angle [deg]", + "type": "number", + "default": "3.0", + "check": "checkMinMax", + "min": 1.0, + "max": 12.0, + "description": "track step offset [1...12deg]", + "category": "OBP70 Settings", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "calcTrueWnds", + "label": "Calculate True Wind", + "type": "boolean", + "default": "false", + "description": "If not available, calculate true wind data from apparent wind and other boat data", + "category": "OBP70 Settings", + "capabilities": { + "obp70": "true" + } + }, + { + "name": "lengthFormat", + "label": "Length Format", + "type": "list", + "default": "m", + "description": "Length format [m|ft]", + "list": [ + "m", + "ft" + ], + "category": "OBP70 Units", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "distanceFormat", + "label": "Distance Format", + "type": "list", + "default": "nm", + "description": "Distance format [m|km|nm]", + "list": [ + "m", + "km", + "nm" + ], + "category": "OBP70 Units", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "speedFormat", + "label": "Speed Format", + "type": "list", + "default": "kn", + "description": "Distance format [m/s|km/h|kn]", + "list": [ + "m/s", + "km/h", + "kn" + ], + "category": "OBP70 Units", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "windspeedFormat", + "label": "Wind Speed Format", + "type": "list", + "default": "kn", + "description": "Wind speed format [m/s|km/h|kn|bft]", + "list": [ + "m/s", + "km/h", + "kn", + "bft" + ], + "category": "OBP70 Units", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "tempFormat", + "label": "Temperature Format", + "type": "list", + "default": "C", + "description": "Temperature format [K|C|F]", + "list": [ + "K", + "C", + "F" + ], + "category": "OBP70 Units", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "dateFormat", + "label": "Date Format", + "type": "list", + "default": "DE", + "description": "Date format [DE|GB|US|ISO] DE: 31.12.2022, GB: 31/12/2022, US: 12/31/2022, ISO: 2022-12-31", + "list": [ + "DE", + "GB", + "US", + "ISO" + ], + "category": "OBP70 Units", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "cpuSpeed", + "label": "CPU Speed [MHz]", + "type": "list", + "default": "160", + "description": "CPU speed in MHz [80|160|240]", + "list": [ + "80", + "160", + "240" + ], + "category": "OBP70 Hardware", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "useRTC", + "label": "RTC Modul", + "type": "list", + "default": "DS1388", + "description": "Use internal RTC module type [off|DS1388]", + "list": [ + "off", + "DS1388" + ], + "category": "OBP70 Hardware", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "useGPS", + "label": "GPS Sensor", + "type": "list", + "default": "ATGM336H", + "description": "Use internal GPS module type [off|NEO-6M|NEO-M8N|ATGM336H]", + "list": [ + "off", + "NEO-6M", + "NEO-M8N", + "ATGM336H" + ], + "category": "OBP70 Hardware", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "hdopAccuracy", + "label": "HDOP Accuracy [m]", + "type": "number", + "default": "20", + "check": "checkMinMax", + "min": 1, + "max": 50, + "description": "HDOP ccuracy in m for a valid GPS signal [1...50]", + "category": "OBP70 Hardware", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "useEnvSensor", + "label": "Env. Sensor", + "type": "list", + "default": "BMP280", + "description": "Use internal or external environment sensor via I2C bus [off|BME280|BMP280|BMP180|BMP085|HTU21|SHT21]", + "list": [ + "off", + "BME280", + "BMP280", + "BMP180", + "BMP085", + "HTU21", + "SHT21" + ], + "category": "OBP70 Hardware", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "usePowSensor1", + "label": "Battery Sensor", + "type": "list", + "default": "off", + "description": "Use external power management sensor via I2C bus for battery [off|INA219|INA226|]", + "list": [ + "off", + "INA219", + "INA226" + ], + "category": "OBP70 Hardware", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "shunt1", + "label": "Battery Shunt", + "type": "list", + "default": "10", + "description": "Shunt current value [10A|50A|100A|200A|300A|400A|500A]", + "list": [ + "10", + "50", + "100", + "200", + "300", + "400", + "500" + ], + "category": "OBP70 Hardware", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "usePowSensor2", + "label": "Solar Sensor", + "type": "list", + "default": "off", + "description": "Use external power management sensor via I2C bus for solar panels [off|INA219|INA226|]", + "list": [ + "off", + "INA219", + "INA226" + ], + "category": "OBP70 Hardware", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "shunt2", + "label": "Solar Shunt", + "type": "list", + "default": "10", + "description": "Shunt current value [10A|50A|100A|200A|300A|400A|500A]", + "list": [ + "10", + "50", + "100", + "200", + "300", + "400", + "500" + ], + "category": "OBP70 Hardware", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "usePowSensor3", + "label": "Gen. Sensor", + "type": "list", + "default": "off", + "description": "Use external power management sensor via I2C bus for generator [off|INA219|INA226|]", + "list": [ + "off", + "INA219", + "INA226" + ], + "category": "OBP70 Hardware", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "shunt3", + "label": "Gen. Shunt", + "type": "list", + "default": "10", + "description": "Shunt current value [10A|50A|100A|200A|300A|400A|500A] @ 75mV", + "list": [ + "10", + "50", + "100", + "200", + "300", + "400", + "500" + ], + "category": "OBP70 Hardware", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "useRotSensor", + "label": "Rot. Sensor", + "type": "list", + "default": "off", + "description": "Use external rotation sensor via I2C bus [off|AS5600]", + "list": [ + "off", + "AS5600" + ], + "category": "OBP70 Hardware", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "rotFunction", + "label": "Rot. Function", + "type": "list", + "default": "off", + "description": "Function for rotation sensor [off|Rudder|Wind|Mast|Keel|Trim|Boom]", + "list": [ + "off", + "Rudder", + "Wind", + "Mast", + "Keel", + "Trim", + "Boom" + ], + "category": "OBP70 Hardware", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "rotOffset", + "label": "Rot. Offset", + "type": "number", + "default": "0", + "check": "checkMinMax", + "min": -180, + "max": 180, + "description": "Offset for rotation sensor [-180°...+180°]", + "category": "OBP70 Hardware", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "rollLimit", + "label": "Roll Limit", + "type": "number", + "default": "25", + "check": "checkMinMax", + "min": -90, + "max": 90, + "description": "Limit violation for roll angle [-90°...+90°]", + "category": "OBP70 Hardware", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "rollOffset", + "label": "Roll Offset", + "type": "number", + "default": "0", + "check": "checkMinMax", + "min": -45, + "max": 45, + "description": "Roll offset angle [-45°...+45°]", + "category": "OBP70 Hardware", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "pitchOffset", + "label": "Pitch Offset", + "type": "number", + "default": "0", + "check": "checkMinMax", + "min": -45, + "max": 45, + "description": "Pitch offset angle [-45°...+45°]", + "category": "OBP70 Hardware", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "useTempSensor", + "label": "Temp. Sensor", + "type": "boolean", + "default": "off", + "description": "Use max. 8 external 1Wire devices [off|DS18B20]", + "list": [ + "off", + "DS18B20" + ], + "category": "OBP70 Hardware", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "powerMode", + "label": "Power Mode", + "type": "list", + "default": "Max Power", + "description": "Settings for power mode", + "list": [ + "Max Power", + "Only 5.0V", + "Min Power" + ], + "category": "OBP70 Hardware", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "underVoltage", + "label": "Undervoltage", + "type": "boolean", + "default": "false", + "description": "Switch off device if voltage drops below 9V [on|off]", + "category": "OBP70 Hardware", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "useSimuData", + "label": "Simulation Data", + "type": "boolean", + "default": "false", + "description": "Use simulation data when bus data are missing [on|off]", + "category": "OBP70 Hardware", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "mod1Out1", + "label": "Name1", + "type": "string", + "default": "text1", + "description": "Button name", + "category": "OBP70 IO-Modul1", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "mod1Out2", + "label": "Name2", + "type": "string", + "default": "text2", + "description": "Button name", + "category": "OBP70 IO-Modul1", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "mod1Out3", + "label": "Name3", + "type": "string", + "default": "text3", + "description": "Button name", + "category": "OBP70 IO-Modul1", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "mod1Out4", + "label": "Name4", + "type": "string", + "default": "text4", + "description": "Button name", + "category": "OBP70 IO-Modul1", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "mod1Out5", + "label": "Name5", + "type": "string", + "default": "text5", + "description": "Button name", + "category": "OBP70 IO-Modul1", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "tSensitivity", + "label": "Touch Sensitivity [%]", + "type": "number", + "default": "100", + "check": "checkMinMax", + "min": 0, + "max": 100, + "description": "Touch sensitivity [0...100%] for sensor buttons", + "category": "OBP70 Calibrations", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "vOffset", + "label": "VSensor Offset", + "type": "number", + "default": "-1.00", + "description": "Offset for internal voltage sensor (ESP32)", + "category": "OBP70 Calibrations", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "vSlope", + "label": "VSensor Slope", + "type": "number", + "default": "1.00", + "description": "Slope for internal voltage sensor (ESP32)", + "category": "OBP70 Calibrations", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "calInstance1", + "label": "Calibration Data Instance 1", + "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": "OBP70 Calibrations", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "calOffset1", + "label": "Data Instance 1 Calibration Offset", + "type": "number", + "default": "0.00", + "description": "Offset for data instance 1", + "category": "OBP70 Calibrations", + "capabilities": { + "obp70":"true" + }, + "condition": [ + { "calInstance1": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + ] + }, + { + "name": "calSlope1", + "label": "Data Instance 1 Calibration Slope", + "type": "number", + "default": "1.00", + "description": "Slope for data instance 1; Default: 1(!)", + "category": "OBP70 Calibrations", + "capabilities": { + "obp70":"true" + }, + "condition": [ + { "calInstance1": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + ] + }, + { + "name": "calSmooth1", + "label": "Data Instance 1 Smoothing", + "type": "number", + "default": "0", + "check": "checkMinMax", + "min": 0, + "max": 10, + "description": "Smoothing factor [0..10]; 0 = no smoothing", + "category": "OBP70 Calibrations", + "capabilities": { + "obp70":"true" + }, + "condition": [ + { "calInstance1": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + ] + }, + { + "name": "calInstance2", + "label": "Calibration Data Instance 2", + "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": "OBP70 Calibrations", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "calOffset2", + "label": "Data Instance 2 Calibration Offset", + "type": "number", + "default": "0.00", + "description": "Offset for data instance 2", + "category": "OBP70 Calibrations", + "capabilities": { + "obp70":"true" + }, + "condition": [ + { "calInstance2": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + ] + }, + { + "name": "calSlope2", + "label": "Data Instance 2 Calibration Slope", + "type": "number", + "default": "1.00", + "description": "Slope for data instance 2; Default: 1(!)", + "category": "OBP70 Calibrations", + "capabilities": { + "obp70":"true" + }, + "condition": [ + { "calInstance2": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + ] + }, + { + "name": "calSmooth2", + "label": "Data Instance 2 Smoothing", + "type": "number", + "default": "0", + "check": "checkMinMax", + "min": 0, + "max": 10, + "description": "Smoothing factor [0..10]; 0 = no smoothing", + "category": "OBP70 Calibrations", + "capabilities": { + "obp70":"true" + }, + "condition": [ + { "calInstance2": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + ] + }, + { + "name": "calInstance3", + "label": "Calibration Data Instance 3", + "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": "OBP70 Calibrations", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "calOffset3", + "label": "Data Instance 3 Calibration Offset", + "type": "number", + "default": "0.00", + "description": "Offset for data instance 3", + "category": "OBP70 Calibrations", + "capabilities": { + "obp70":"true" + }, + "condition": [ + { "calInstance3": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + ] + }, + { + "name": "calSlope3", + "label": "Data Instance 3 Calibration Slope", + "type": "number", + "default": "1.00", + "description": "Slope for data instance 3; Default: 1(!)", + "category": "OBP70 Calibrations", + "capabilities": { + "obp70":"true" + }, + "condition": [ + { "calInstance3": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "PRPOS", "RPOS", "SOG", "STW", "TWA", "TWS", "TWD", "WTemp" ] } + ] + }, + { + "name": "calSmooth3", + "label": "Data Instance 3 Smoothing", + "type": "number", + "default": "0", + "check": "checkMinMax", + "min": 0, + "max": 10, + "description": "Smoothing factor [0..10]; 0 = no smoothing", + "category": "OBP70 Calibrations", + "capabilities": { + "obp70":"true" + }, + "condition": [ + { "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": "OBP70 Calibrations", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "calOffset4", + "label": "Data Instance 4 Calibration Offset", + "type": "number", + "default": "0.00", + "description": "Offset for data instance 4", + "category": "OBP70 Calibrations", + "capabilities": { + "obp70":"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": "OBP70 Calibrations", + "capabilities": { + "obp70":"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": "OBP70 Calibrations", + "capabilities": { + "obp70":"true" + }, + "condition": [ + { "calInstance4": ["AWA", "AWS", "COG", "DBS", "DBT", "HDM", "HDT", "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": "OBP70 Navigation", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "ipAddress", + "label": "IP Address", + "type": "string", + "default": "192.168.15.10", + "check": "checkIpAddress", + "description": "IP address for local map service e.g. 192.168.15.10\nor an MDNS name like Raspi.local", + "category": "OBP70 Navigation", + "capabilities": { + "obp70":"true" + }, + "condition": [ + { "mapsource": ["Local Service"] } + ] + }, + { + "name": "localPort", + "label": "Port", + "type": "number", + "default": "8080", + "check":"checkPort", + "description": "TCP port for local map server", + "category": "OBP70 Navigation", + "capabilities": { + "obp70":"true" + }, + "condition": [ + { "mapsource": ["Local Service"] } + ] + }, + { + "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": "OBP70 Navigation", + "capabilities": { + "obp70":"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": "OBP70 Navigation", + "capabilities": { + "obp70":"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": "OBP70 Navigation", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "orientation", + "label": "Map Orientation", + "type": "list", + "default": "North Dirirection", + "description": "Map orientation for navigation", + "list": [ + "North Direction", + "Travel Direction" + ], + "category": "OBP70 Navigation", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "grid", + "label": "Show Grid", + "type": "boolean", + "default": "false", + "description": "Show the grid for latutude and longitude", + "category": "OBP70 Navigation", + "capabilities": { + "obp70": "true" + } + }, + { + "name": "showvalues", + "label": "Show Values", + "type": "boolean", + "default": "false", + "description": "Show boat data values in the left upper map corner", + "category": "OBP70 Navigation", + "capabilities": { + "obp70": "true" + } + }, + { + "name": "ownheading", + "label": "Alternativ Heading", + "type": "boolean", + "default": "false", + "description": "Calculating an alternative travel direction for\na better and calmer map orientation", + "category": "OBP70 Navigation", + "capabilities": { + "obp70": "true" + } + }, + { + "name": "display", + "label": "Display Mode", + "type": "list", + "default": "Logo + QR Code", + "description": "Settings for startup display", + "list": [ + "White Screen", + "Logo", + "Logo + QR Code", + "Off" + ], + "category": "OBP70 Display", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "displaycolor", + "label": "Inverted Display Mode", + "type": "list", + "default": "Normal", + "description": "Invert display to white letters on black background [Normal|Inverse]", + "list": [ + "Normal", + "Inverse" + ], + "category": "OBP70 Display", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "statusLine", + "label": "Status Line", + "type": "boolean", + "default": "true", + "description": "Show status line [on|off]", + "category": "OBP70 Display", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "timeSource", + "label": "Status Time Source", + "type": "list", + "default": "GPS", + "description": "Data source for date and time display in status line [iRTC|RTC|GPS]", + "list": [ + {"l":"Internal real time clock (iRTC)","v":"iRTC"}, + {"l":"Real time clock (RTC)","v":"RTC"}, + {"l":"Time via bus (GPS)","v":"GPS"} + ], + "category": "OBP70 Display", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "refresh", + "label": "Refresh", + "type": "boolean", + "default": "true", + "description": "Refresh e-paper display after each new page request to reduce ghost effects [on|off]", + "category": "OBP70 Display", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "fastRefresh", + "label": "Fast Refresh", + "type": "boolean", + "default": "false", + "description": "Fast refresh for e-paper display [on|off]", + "category": "OBP70 Display", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "fullRefreshTime", + "label": "Full Refresh Time [min]", + "type": "number", + "default": "1", + "check": "checkMinMax", + "min": 1, + "max": 10, + "description": "E-Paper full refresh time all [1...10 min]", + "category": "OBP70 Display", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "holdvalues", + "label": "Hold Values", + "type": "boolean", + "default": "false", + "description": "Retain old values when data stream stops [on|off]", + "category": "OBP70 Display", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "valueprecision", + "label": "Display value precision", + "type": "list", + "default": "2", + "description": "Maximum number of decimal places to display [1|2]", + "list": [ + "1", + "2" + ], + "category": "OBP70 Display", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "backlight", + "label": "Backlight Mode", + "type": "list", + "default": "Control by Key", + "description": "Settings for automatic backlight mode", + "list": [ + "Off", + "Control by Sun", + "Control by Bus", + "Control by Time", + "Control by Key", + "On" + ], + "category": "OBP70 Display", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "blColor", + "label": "Backlight Color", + "type": "list", + "default": "Red", + "description": "Backlight color", + "list": [ + "Red", + "Orange", + "Yellow", + "Green", + "Blue", + "Aqua", + "Violet", + "White" + ], + "category": "OBP70 Display", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "blBrightness", + "label": "Brightness [%]", + "type": "number", + "default": "50", + "check": "checkMinMax", + "min": 5, + "max": 100, + "description": "Backlight brightness [5...100%]", + "category": "OBP70 Display", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "flashLED", + "label": "Flash LED Mode", + "type": "list", + "default": "Limit Violation", + "description": "Settings for flash LED", + "list": [ + "Off", + "Bus Data", + "GPS Fix Lost", + "Limit Violation" + ], + "category": "OBP70 Display", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "buzzerError", + "label": "Buzzer Error", + "type": "boolean", + "default": "false", + "description": "Sound on error [on|off]", + "category": "OBP70 Buzzer", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "buzzerGps", + "label": "Buzzer GPS Fix", + "type": "boolean", + "default": "false", + "description": "Sound on missing or lost GPS fix", + "category": "OBP70 Buzzer", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "buzzerLim", + "label": "Buzzer by Limits", + "type": "boolean", + "default": "false", + "description": "Sound on limit violation", + "category": "OBP70 Buzzer", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "buzzerMode", + "label": "Buzzer Mode", + "type": "list", + "default": "Off", + "description": "Settings for buzzer behaviour", + "list": [ + "Off", + "Short Single Beep", + "Longer Single Beep", + "Beep until Confirmation" + ], + "category": "OBP70 Buzzer", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "buzzerPower", + "label": "Buzzer Power [%]", + "type": "number", + "default": "50", + "check": "checkMinMax", + "min": 0, + "max": 100, + "description": "Buzzer loudness [0...100%]", + "category": "OBP70 Buzzer", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "visiblePages", + "label": "Number of Pages", + "type": "number", + "check": "checkMinMax", + "min": 1, + "max": 10, + "default":"10", + "description": "Number of visible data pages [1...10]", + "category":"OBP70 Pages", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "startPage", + "label": "Start Page", + "type": "number", + "check": "checkMinMax", + "min": 1, + "max": 10, + "default":"1", + "description": "First page number to display after device startup", + "category":"OBP70 Pages", + "capabilities": { + "obp70":"true" + } + }, + { + "name": "imageFormat", + "label": "Screenshot Format", + "type": "list", + "default":"PBM", + "description": "Graphics file format for screenshots [GIF|PBM|BMP]", + "list": [ + {"l":"Compressed image (GIF)","v":"GIF"}, + {"l":"Portable bitmap (PBM)","v":"PBM"}, + {"l":"Windows bitmap (BMP)","v":"BMP"} + ], + "category":"OBP70 Pages", + "capabilities": { + "obp70":"true" + } + }, + + { + "name": "page1type", + "label": "Type", + "type": "list", + "default": "Voltage", + "description": "Type of page for page 1", + "list": [ + "Autopilot", + "BME280", + "Battery", + "Battery2", + "Clock", + "Compass", + "DigitalOut", + "DST810", + "Fluid", + "FourValues", + "FourValues2", + "Generator", + "KeelPosition", + "Navigation", + "OneValue", + "RollPitch", + "RudderPosition", + "SixValues", + "SkyView", + "Solar", + "ThreeValues", + "TwoValues", + "Voltage", + "WhitePage", + "Wind", + "WindPlot", + "WindRose", + "WindRoseFlex", + "XTETrack" + ], + "category": "OBP70 Page 1", + "capabilities": { + "obp70": "true" + }, + "condition": { + "visiblePages": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page1value1", + "label": "Field 1", + "type": "boatData", + "default": "", + "description": "The display for field one", + "category": "OBP70 Page 1", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page1type": [ + "Fluid", + "FourValues", + "FourValues2", + "OneValue", + "RollPitch", + "SixValues", + "ThreeValues", + "TwoValues", + "WindRoseFlex" + ], + "visiblePages": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page1value2", + "label": "Field 2", + "type": "boatData", + "default": "", + "description": "The display for field two", + "category": "OBP70 Page 1", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page1type": [ + "FourValues", + "FourValues2", + "RollPitch", + "SixValues", + "ThreeValues", + "TwoValues", + "WindRoseFlex" + ], + "visiblePages": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page1value3", + "label": "Field 3", + "type": "boatData", + "default": "", + "description": "The display for field three", + "category": "OBP70 Page 1", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page1type": [ + "FourValues", + "FourValues2", + "SixValues", + "ThreeValues", + "WindRoseFlex" + ], + "visiblePages": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page1value4", + "label": "Field 4", + "type": "boatData", + "default": "", + "description": "The display for field four", + "category": "OBP70 Page 1", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page1type": [ + "FourValues", + "FourValues2", + "SixValues", + "WindRoseFlex" + ], + "visiblePages": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page1value5", + "label": "Field 5", + "type": "boatData", + "default": "", + "description": "The display for field five", + "category": "OBP70 Page 1", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page1type": [ + "SixValues" + ], + "visiblePages": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page1value6", + "label": "Field 6", + "type": "boatData", + "default": "", + "description": "The display for field six", + "category": "OBP70 Page 1", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page1type": [ + "SixValues" + ], + "visiblePages": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page1fluid", + "label": "Fluid type", + "type": "list", + "default": "0", + "list": [ + { + "l": "Fuel (0)", + "v": "0" + }, + { + "l": "Water (1)", + "v": "1" + }, + { + "l": "Gray Water (2)", + "v": "2" + }, + { + "l": "Live Well (3)", + "v": "3" + }, + { + "l": "Oil (4)", + "v": "4" + }, + { + "l": "Black Water (5)", + "v": "5" + }, + { + "l": "Fuel Gasoline (6)", + "v": "6" + } + ], + "description": "Fluid type in tank", + "category": "OBP70 Page 1", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page1type": "Fluid", + "visiblePages": [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page2type", + "label": "Type", + "type": "list", + "default": "WindRose", + "description": "Type of page for page 2", + "list": [ + "Autopilot", + "BME280", + "Battery", + "Battery2", + "Clock", + "Compass", + "DigitalOut", + "DST810", + "Fluid", + "FourValues", + "FourValues2", + "Generator", + "KeelPosition", + "Navigation", + "OneValue", + "RollPitch", + "RudderPosition", + "SixValues", + "SkyView", + "Solar", + "ThreeValues", + "TwoValues", + "Voltage", + "WhitePage", + "Wind", + "WindPlot", + "WindRose", + "WindRoseFlex", + "XTETrack" + ], + "category": "OBP70 Page 2", + "capabilities": { + "obp70": "true" + }, + "condition": { + "visiblePages": [ + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page2value1", + "label": "Field 1", + "type": "boatData", + "default": "", + "description": "The display for field one", + "category": "OBP70 Page 2", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page2type": [ + "Fluid", + "FourValues", + "FourValues2", + "OneValue", + "RollPitch", + "SixValues", + "ThreeValues", + "TwoValues", + "WindRoseFlex" + ], + "visiblePages": [ + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page2value2", + "label": "Field 2", + "type": "boatData", + "default": "", + "description": "The display for field two", + "category": "OBP70 Page 2", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page2type": [ + "FourValues", + "FourValues2", + "RollPitch", + "SixValues", + "ThreeValues", + "TwoValues", + "WindRoseFlex" + ], + "visiblePages": [ + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page2value3", + "label": "Field 3", + "type": "boatData", + "default": "", + "description": "The display for field three", + "category": "OBP70 Page 2", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page2type": [ + "FourValues", + "FourValues2", + "SixValues", + "ThreeValues", + "WindRoseFlex" + ], + "visiblePages": [ + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page2value4", + "label": "Field 4", + "type": "boatData", + "default": "", + "description": "The display for field four", + "category": "OBP70 Page 2", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page2type": [ + "FourValues", + "FourValues2", + "SixValues", + "WindRoseFlex" + ], + "visiblePages": [ + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page2value5", + "label": "Field 5", + "type": "boatData", + "default": "", + "description": "The display for field five", + "category": "OBP70 Page 2", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page2type": [ + "SixValues" + ], + "visiblePages": [ + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page2value6", + "label": "Field 6", + "type": "boatData", + "default": "", + "description": "The display for field six", + "category": "OBP70 Page 2", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page2type": [ + "SixValues" + ], + "visiblePages": [ + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page2fluid", + "label": "Fluid type", + "type": "list", + "default": "0", + "list": [ + { + "l": "Fuel (0)", + "v": "0" + }, + { + "l": "Water (1)", + "v": "1" + }, + { + "l": "Gray Water (2)", + "v": "2" + }, + { + "l": "Live Well (3)", + "v": "3" + }, + { + "l": "Oil (4)", + "v": "4" + }, + { + "l": "Black Water (5)", + "v": "5" + }, + { + "l": "Fuel Gasoline (6)", + "v": "6" + } + ], + "description": "Fluid type in tank", + "category": "OBP70 Page 2", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page2type": "Fluid", + "visiblePages": [ + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page3type", + "label": "Type", + "type": "list", + "default": "OneValue", + "description": "Type of page for page 3", + "list": [ + "Autopilot", + "BME280", + "Battery", + "Battery2", + "Clock", + "Compass", + "DigitalOut", + "DST810", + "Fluid", + "FourValues", + "FourValues2", + "Generator", + "KeelPosition", + "Navigation", + "OneValue", + "RollPitch", + "RudderPosition", + "SixValues", + "SkyView", + "Solar", + "ThreeValues", + "TwoValues", + "Voltage", + "WhitePage", + "Wind", + "WindPlot", + "WindRose", + "WindRoseFlex", + "XTETrack" + ], + "category": "OBP70 Page 3", + "capabilities": { + "obp70": "true" + }, + "condition": { + "visiblePages": [ + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page3value1", + "label": "Field 1", + "type": "boatData", + "default": "", + "description": "The display for field one", + "category": "OBP70 Page 3", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page3type": [ + "Fluid", + "FourValues", + "FourValues2", + "OneValue", + "RollPitch", + "SixValues", + "ThreeValues", + "TwoValues", + "WindRoseFlex" + ], + "visiblePages": [ + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page3value2", + "label": "Field 2", + "type": "boatData", + "default": "", + "description": "The display for field two", + "category": "OBP70 Page 3", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page3type": [ + "FourValues", + "FourValues2", + "RollPitch", + "SixValues", + "ThreeValues", + "TwoValues", + "WindRoseFlex" + ], + "visiblePages": [ + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page3value3", + "label": "Field 3", + "type": "boatData", + "default": "", + "description": "The display for field three", + "category": "OBP70 Page 3", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page3type": [ + "FourValues", + "FourValues2", + "SixValues", + "ThreeValues", + "WindRoseFlex" + ], + "visiblePages": [ + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page3value4", + "label": "Field 4", + "type": "boatData", + "default": "", + "description": "The display for field four", + "category": "OBP70 Page 3", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page3type": [ + "FourValues", + "FourValues2", + "SixValues", + "WindRoseFlex" + ], + "visiblePages": [ + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page3value5", + "label": "Field 5", + "type": "boatData", + "default": "", + "description": "The display for field five", + "category": "OBP70 Page 3", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page3type": [ + "SixValues" + ], + "visiblePages": [ + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page3value6", + "label": "Field 6", + "type": "boatData", + "default": "", + "description": "The display for field six", + "category": "OBP70 Page 3", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page3type": [ + "SixValues" + ], + "visiblePages": [ + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page3fluid", + "label": "Fluid type", + "type": "list", + "default": "0", + "list": [ + { + "l": "Fuel (0)", + "v": "0" + }, + { + "l": "Water (1)", + "v": "1" + }, + { + "l": "Gray Water (2)", + "v": "2" + }, + { + "l": "Live Well (3)", + "v": "3" + }, + { + "l": "Oil (4)", + "v": "4" + }, + { + "l": "Black Water (5)", + "v": "5" + }, + { + "l": "Fuel Gasoline (6)", + "v": "6" + } + ], + "description": "Fluid type in tank", + "category": "OBP70 Page 3", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page3type": "Fluid", + "visiblePages": [ + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page4type", + "label": "Type", + "type": "list", + "default": "TwoValues", + "description": "Type of page for page 4", + "list": [ + "Autopilot", + "BME280", + "Battery", + "Battery2", + "Clock", + "Compass", + "DigitalOut", + "DST810", + "Fluid", + "FourValues", + "FourValues2", + "Generator", + "KeelPosition", + "Navigation", + "OneValue", + "RollPitch", + "RudderPosition", + "SixValues", + "SkyView", + "Solar", + "ThreeValues", + "TwoValues", + "Voltage", + "WhitePage", + "Wind", + "WindPlot", + "WindRose", + "WindRoseFlex", + "XTETrack" + ], + "category": "OBP70 Page 4", + "capabilities": { + "obp70": "true" + }, + "condition": { + "visiblePages": [ + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page4value1", + "label": "Field 1", + "type": "boatData", + "default": "", + "description": "The display for field one", + "category": "OBP70 Page 4", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page4type": [ + "Fluid", + "FourValues", + "FourValues2", + "OneValue", + "RollPitch", + "SixValues", + "ThreeValues", + "TwoValues", + "WindRoseFlex" + ], + "visiblePages": [ + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page4value2", + "label": "Field 2", + "type": "boatData", + "default": "", + "description": "The display for field two", + "category": "OBP70 Page 4", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page4type": [ + "FourValues", + "FourValues2", + "RollPitch", + "SixValues", + "ThreeValues", + "TwoValues", + "WindRoseFlex" + ], + "visiblePages": [ + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page4value3", + "label": "Field 3", + "type": "boatData", + "default": "", + "description": "The display for field three", + "category": "OBP70 Page 4", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page4type": [ + "FourValues", + "FourValues2", + "SixValues", + "ThreeValues", + "WindRoseFlex" + ], + "visiblePages": [ + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page4value4", + "label": "Field 4", + "type": "boatData", + "default": "", + "description": "The display for field four", + "category": "OBP70 Page 4", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page4type": [ + "FourValues", + "FourValues2", + "SixValues", + "WindRoseFlex" + ], + "visiblePages": [ + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page4value5", + "label": "Field 5", + "type": "boatData", + "default": "", + "description": "The display for field five", + "category": "OBP70 Page 4", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page4type": [ + "SixValues" + ], + "visiblePages": [ + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page4value6", + "label": "Field 6", + "type": "boatData", + "default": "", + "description": "The display for field six", + "category": "OBP70 Page 4", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page4type": [ + "SixValues" + ], + "visiblePages": [ + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page4fluid", + "label": "Fluid type", + "type": "list", + "default": "0", + "list": [ + { + "l": "Fuel (0)", + "v": "0" + }, + { + "l": "Water (1)", + "v": "1" + }, + { + "l": "Gray Water (2)", + "v": "2" + }, + { + "l": "Live Well (3)", + "v": "3" + }, + { + "l": "Oil (4)", + "v": "4" + }, + { + "l": "Black Water (5)", + "v": "5" + }, + { + "l": "Fuel Gasoline (6)", + "v": "6" + } + ], + "description": "Fluid type in tank", + "category": "OBP70 Page 4", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page4type": "Fluid", + "visiblePages": [ + "4", + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page5type", + "label": "Type", + "type": "list", + "default": "ThreeValues", + "description": "Type of page for page 5", + "list": [ + "Autopilot", + "BME280", + "Battery", + "Battery2", + "Clock", + "Compass", + "DigitalOut", + "DST810", + "Fluid", + "FourValues", + "FourValues2", + "Generator", + "KeelPosition", + "Navigation", + "OneValue", + "RollPitch", + "RudderPosition", + "SixValues", + "SkyView", + "Solar", + "ThreeValues", + "TwoValues", + "Voltage", + "WhitePage", + "Wind", + "WindPlot", + "WindRose", + "WindRoseFlex", + "XTETrack" + ], + "category": "OBP70 Page 5", + "capabilities": { + "obp70": "true" + }, + "condition": { + "visiblePages": [ + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page5value1", + "label": "Field 1", + "type": "boatData", + "default": "", + "description": "The display for field one", + "category": "OBP70 Page 5", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page5type": [ + "Fluid", + "FourValues", + "FourValues2", + "OneValue", + "RollPitch", + "SixValues", + "ThreeValues", + "TwoValues", + "WindRoseFlex" + ], + "visiblePages": [ + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page5value2", + "label": "Field 2", + "type": "boatData", + "default": "", + "description": "The display for field two", + "category": "OBP70 Page 5", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page5type": [ + "FourValues", + "FourValues2", + "RollPitch", + "SixValues", + "ThreeValues", + "TwoValues", + "WindRoseFlex" + ], + "visiblePages": [ + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page5value3", + "label": "Field 3", + "type": "boatData", + "default": "", + "description": "The display for field three", + "category": "OBP70 Page 5", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page5type": [ + "FourValues", + "FourValues2", + "SixValues", + "ThreeValues", + "WindRoseFlex" + ], + "visiblePages": [ + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page5value4", + "label": "Field 4", + "type": "boatData", + "default": "", + "description": "The display for field four", + "category": "OBP70 Page 5", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page5type": [ + "FourValues", + "FourValues2", + "SixValues", + "WindRoseFlex" + ], + "visiblePages": [ + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page5value5", + "label": "Field 5", + "type": "boatData", + "default": "", + "description": "The display for field five", + "category": "OBP70 Page 5", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page5type": [ + "SixValues" + ], + "visiblePages": [ + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page5value6", + "label": "Field 6", + "type": "boatData", + "default": "", + "description": "The display for field six", + "category": "OBP70 Page 5", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page5type": [ + "SixValues" + ], + "visiblePages": [ + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page5fluid", + "label": "Fluid type", + "type": "list", + "default": "0", + "list": [ + { + "l": "Fuel (0)", + "v": "0" + }, + { + "l": "Water (1)", + "v": "1" + }, + { + "l": "Gray Water (2)", + "v": "2" + }, + { + "l": "Live Well (3)", + "v": "3" + }, + { + "l": "Oil (4)", + "v": "4" + }, + { + "l": "Black Water (5)", + "v": "5" + }, + { + "l": "Fuel Gasoline (6)", + "v": "6" + } + ], + "description": "Fluid type in tank", + "category": "OBP70 Page 5", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page5type": "Fluid", + "visiblePages": [ + "5", + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page6type", + "label": "Type", + "type": "list", + "default": "FourValues", + "description": "Type of page for page 6", + "list": [ + "Autopilot", + "BME280", + "Battery", + "Battery2", + "Clock", + "Compass", + "DigitalOut", + "DST810", + "Fluid", + "FourValues", + "FourValues2", + "Generator", + "KeelPosition", + "Navigation", + "OneValue", + "RollPitch", + "RudderPosition", + "SixValues", + "SkyView", + "Solar", + "ThreeValues", + "TwoValues", + "Voltage", + "WhitePage", + "Wind", + "WindPlot", + "WindRose", + "WindRoseFlex", + "XTETrack" + ], + "category": "OBP70 Page 6", + "capabilities": { + "obp70": "true" + }, + "condition": { + "visiblePages": [ + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page6value1", + "label": "Field 1", + "type": "boatData", + "default": "", + "description": "The display for field one", + "category": "OBP70 Page 6", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page6type": [ + "Fluid", + "FourValues", + "FourValues2", + "OneValue", + "RollPitch", + "SixValues", + "ThreeValues", + "TwoValues", + "WindRoseFlex" + ], + "visiblePages": [ + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page6value2", + "label": "Field 2", + "type": "boatData", + "default": "", + "description": "The display for field two", + "category": "OBP70 Page 6", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page6type": [ + "FourValues", + "FourValues2", + "RollPitch", + "SixValues", + "ThreeValues", + "TwoValues", + "WindRoseFlex" + ], + "visiblePages": [ + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page6value3", + "label": "Field 3", + "type": "boatData", + "default": "", + "description": "The display for field three", + "category": "OBP70 Page 6", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page6type": [ + "FourValues", + "FourValues2", + "SixValues", + "ThreeValues", + "WindRoseFlex" + ], + "visiblePages": [ + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page6value4", + "label": "Field 4", + "type": "boatData", + "default": "", + "description": "The display for field four", + "category": "OBP70 Page 6", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page6type": [ + "FourValues", + "FourValues2", + "SixValues", + "WindRoseFlex" + ], + "visiblePages": [ + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page6value5", + "label": "Field 5", + "type": "boatData", + "default": "", + "description": "The display for field five", + "category": "OBP70 Page 6", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page6type": [ + "SixValues" + ], + "visiblePages": [ + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page6value6", + "label": "Field 6", + "type": "boatData", + "default": "", + "description": "The display for field six", + "category": "OBP70 Page 6", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page6type": [ + "SixValues" + ], + "visiblePages": [ + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page6fluid", + "label": "Fluid type", + "type": "list", + "default": "0", + "list": [ + { + "l": "Fuel (0)", + "v": "0" + }, + { + "l": "Water (1)", + "v": "1" + }, + { + "l": "Gray Water (2)", + "v": "2" + }, + { + "l": "Live Well (3)", + "v": "3" + }, + { + "l": "Oil (4)", + "v": "4" + }, + { + "l": "Black Water (5)", + "v": "5" + }, + { + "l": "Fuel Gasoline (6)", + "v": "6" + } + ], + "description": "Fluid type in tank", + "category": "OBP70 Page 6", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page6type": "Fluid", + "visiblePages": [ + "6", + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page7type", + "label": "Type", + "type": "list", + "default": "FourValues2", + "description": "Type of page for page 7", + "list": [ + "Autopilot", + "BME280", + "Battery", + "Battery2", + "Clock", + "Compass", + "DigitalOut", + "DST810", + "Fluid", + "FourValues", + "FourValues2", + "Generator", + "KeelPosition", + "Navigation", + "OneValue", + "RollPitch", + "RudderPosition", + "SixValues", + "SkyView", + "Solar", + "ThreeValues", + "TwoValues", + "Voltage", + "WhitePage", + "Wind", + "WindPlot", + "WindRose", + "WindRoseFlex", + "XTETrack" + ], + "category": "OBP70 Page 7", + "capabilities": { + "obp70": "true" + }, + "condition": { + "visiblePages": [ + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page7value1", + "label": "Field 1", + "type": "boatData", + "default": "", + "description": "The display for field one", + "category": "OBP70 Page 7", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page7type": [ + "Fluid", + "FourValues", + "FourValues2", + "OneValue", + "RollPitch", + "SixValues", + "ThreeValues", + "TwoValues", + "WindRoseFlex" + ], + "visiblePages": [ + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page7value2", + "label": "Field 2", + "type": "boatData", + "default": "", + "description": "The display for field two", + "category": "OBP70 Page 7", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page7type": [ + "FourValues", + "FourValues2", + "RollPitch", + "SixValues", + "ThreeValues", + "TwoValues", + "WindRoseFlex" + ], + "visiblePages": [ + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page7value3", + "label": "Field 3", + "type": "boatData", + "default": "", + "description": "The display for field three", + "category": "OBP70 Page 7", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page7type": [ + "FourValues", + "FourValues2", + "SixValues", + "ThreeValues", + "WindRoseFlex" + ], + "visiblePages": [ + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page7value4", + "label": "Field 4", + "type": "boatData", + "default": "", + "description": "The display for field four", + "category": "OBP70 Page 7", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page7type": [ + "FourValues", + "FourValues2", + "SixValues", + "WindRoseFlex" + ], + "visiblePages": [ + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page7value5", + "label": "Field 5", + "type": "boatData", + "default": "", + "description": "The display for field five", + "category": "OBP70 Page 7", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page7type": [ + "SixValues" + ], + "visiblePages": [ + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page7value6", + "label": "Field 6", + "type": "boatData", + "default": "", + "description": "The display for field six", + "category": "OBP70 Page 7", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page7type": [ + "SixValues" + ], + "visiblePages": [ + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page7fluid", + "label": "Fluid type", + "type": "list", + "default": "0", + "list": [ + { + "l": "Fuel (0)", + "v": "0" + }, + { + "l": "Water (1)", + "v": "1" + }, + { + "l": "Gray Water (2)", + "v": "2" + }, + { + "l": "Live Well (3)", + "v": "3" + }, + { + "l": "Oil (4)", + "v": "4" + }, + { + "l": "Black Water (5)", + "v": "5" + }, + { + "l": "Fuel Gasoline (6)", + "v": "6" + } + ], + "description": "Fluid type in tank", + "category": "OBP70 Page 7", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page7type": "Fluid", + "visiblePages": [ + "7", + "8", + "9", + "10" + ] + } + }, + { + "name": "page8type", + "label": "Type", + "type": "list", + "default": "Clock", + "description": "Type of page for page 8", + "list": [ + "Autopilot", + "BME280", + "Battery", + "Battery2", + "Clock", + "Compass", + "DigitalOut", + "DST810", + "Fluid", + "FourValues", + "FourValues2", + "Generator", + "KeelPosition", + "Navigation", + "OneValue", + "RollPitch", + "RudderPosition", + "SixValues", + "SkyView", + "Solar", + "ThreeValues", + "TwoValues", + "Voltage", + "WhitePage", + "Wind", + "WindPlot", + "WindRose", + "WindRoseFlex", + "XTETrack" + ], + "category": "OBP70 Page 8", + "capabilities": { + "obp70": "true" + }, + "condition": { + "visiblePages": [ + "8", + "9", + "10" + ] + } + }, + { + "name": "page8value1", + "label": "Field 1", + "type": "boatData", + "default": "", + "description": "The display for field one", + "category": "OBP70 Page 8", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page8type": [ + "Fluid", + "FourValues", + "FourValues2", + "OneValue", + "RollPitch", + "SixValues", + "ThreeValues", + "TwoValues", + "WindRoseFlex" + ], + "visiblePages": [ + "8", + "9", + "10" + ] + } + }, + { + "name": "page8value2", + "label": "Field 2", + "type": "boatData", + "default": "", + "description": "The display for field two", + "category": "OBP70 Page 8", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page8type": [ + "FourValues", + "FourValues2", + "RollPitch", + "SixValues", + "ThreeValues", + "TwoValues", + "WindRoseFlex" + ], + "visiblePages": [ + "8", + "9", + "10" + ] + } + }, + { + "name": "page8value3", + "label": "Field 3", + "type": "boatData", + "default": "", + "description": "The display for field three", + "category": "OBP70 Page 8", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page8type": [ + "FourValues", + "FourValues2", + "SixValues", + "ThreeValues", + "WindRoseFlex" + ], + "visiblePages": [ + "8", + "9", + "10" + ] + } + }, + { + "name": "page8value4", + "label": "Field 4", + "type": "boatData", + "default": "", + "description": "The display for field four", + "category": "OBP70 Page 8", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page8type": [ + "FourValues", + "FourValues2", + "SixValues", + "WindRoseFlex" + ], + "visiblePages": [ + "8", + "9", + "10" + ] + } + }, + { + "name": "page8value5", + "label": "Field 5", + "type": "boatData", + "default": "", + "description": "The display for field five", + "category": "OBP70 Page 8", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page8type": [ + "SixValues" + ], + "visiblePages": [ + "8", + "9", + "10" + ] + } + }, + { + "name": "page8value6", + "label": "Field 6", + "type": "boatData", + "default": "", + "description": "The display for field six", + "category": "OBP70 Page 8", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page8type": [ + "SixValues" + ], + "visiblePages": [ + "8", + "9", + "10" + ] + } + }, + { + "name": "page8fluid", + "label": "Fluid type", + "type": "list", + "default": "0", + "list": [ + { + "l": "Fuel (0)", + "v": "0" + }, + { + "l": "Water (1)", + "v": "1" + }, + { + "l": "Gray Water (2)", + "v": "2" + }, + { + "l": "Live Well (3)", + "v": "3" + }, + { + "l": "Oil (4)", + "v": "4" + }, + { + "l": "Black Water (5)", + "v": "5" + }, + { + "l": "Fuel Gasoline (6)", + "v": "6" + } + ], + "description": "Fluid type in tank", + "category": "OBP70 Page 8", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page8type": "Fluid", + "visiblePages": [ + "8", + "9", + "10" + ] + } + }, + { + "name": "page9type", + "label": "Type", + "type": "list", + "default": "RollPitch", + "description": "Type of page for page 9", + "list": [ + "Autopilot", + "BME280", + "Battery", + "Battery2", + "Clock", + "Compass", + "DigitalOut", + "DST810", + "Fluid", + "FourValues", + "FourValues2", + "Generator", + "KeelPosition", + "Navigation", + "OneValue", + "RollPitch", + "RudderPosition", + "SixValues", + "SkyView", + "Solar", + "ThreeValues", + "TwoValues", + "Voltage", + "WhitePage", + "Wind", + "WindPlot", + "WindRose", + "WindRoseFlex", + "XTETrack" + ], + "category": "OBP70 Page 9", + "capabilities": { + "obp70": "true" + }, + "condition": { + "visiblePages": [ + "9", + "10" + ] + } + }, + { + "name": "page9value1", + "label": "Field 1", + "type": "boatData", + "default": "", + "description": "The display for field one", + "category": "OBP70 Page 9", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page9type": [ + "Fluid", + "FourValues", + "FourValues2", + "OneValue", + "RollPitch", + "SixValues", + "ThreeValues", + "TwoValues", + "WindRoseFlex" + ], + "visiblePages": [ + "9", + "10" + ] + } + }, + { + "name": "page9value2", + "label": "Field 2", + "type": "boatData", + "default": "", + "description": "The display for field two", + "category": "OBP70 Page 9", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page9type": [ + "FourValues", + "FourValues2", + "RollPitch", + "SixValues", + "ThreeValues", + "TwoValues", + "WindRoseFlex" + ], + "visiblePages": [ + "9", + "10" + ] + } + }, + { + "name": "page9value3", + "label": "Field 3", + "type": "boatData", + "default": "", + "description": "The display for field three", + "category": "OBP70 Page 9", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page9type": [ + "FourValues", + "FourValues2", + "SixValues", + "ThreeValues", + "WindRoseFlex" + ], + "visiblePages": [ + "9", + "10" + ] + } + }, + { + "name": "page9value4", + "label": "Field 4", + "type": "boatData", + "default": "", + "description": "The display for field four", + "category": "OBP70 Page 9", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page9type": [ + "FourValues", + "FourValues2", + "SixValues", + "WindRoseFlex" + ], + "visiblePages": [ + "9", + "10" + ] + } + }, + { + "name": "page9value5", + "label": "Field 5", + "type": "boatData", + "default": "", + "description": "The display for field five", + "category": "OBP70 Page 9", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page9type": [ + "SixValues" + ], + "visiblePages": [ + "9", + "10" + ] + } + }, + { + "name": "page9value6", + "label": "Field 6", + "type": "boatData", + "default": "", + "description": "The display for field six", + "category": "OBP70 Page 9", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page9type": [ + "SixValues" + ], + "visiblePages": [ + "9", + "10" + ] + } + }, + { + "name": "page9fluid", + "label": "Fluid type", + "type": "list", + "default": "0", + "list": [ + { + "l": "Fuel (0)", + "v": "0" + }, + { + "l": "Water (1)", + "v": "1" + }, + { + "l": "Gray Water (2)", + "v": "2" + }, + { + "l": "Live Well (3)", + "v": "3" + }, + { + "l": "Oil (4)", + "v": "4" + }, + { + "l": "Black Water (5)", + "v": "5" + }, + { + "l": "Fuel Gasoline (6)", + "v": "6" + } + ], + "description": "Fluid type in tank", + "category": "OBP70 Page 9", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page9type": "Fluid", + "visiblePages": [ + "9", + "10" + ] + } + }, + { + "name": "page10type", + "label": "Type", + "type": "list", + "default": "Battery2", + "description": "Type of page for page 10", + "list": [ + "Autopilot", + "BME280", + "Battery", + "Battery2", + "Clock", + "Compass", + "DigitalOut", + "DST810", + "Fluid", + "FourValues", + "FourValues2", + "Generator", + "KeelPosition", + "Navigation", + "OneValue", + "RollPitch", + "RudderPosition", + "SixValues", + "SkyView", + "Solar", + "ThreeValues", + "TwoValues", + "Voltage", + "WhitePage", + "Wind", + "WindPlot", + "WindRose", + "WindRoseFlex", + "XTETrack" + ], + "category": "OBP70 Page 10", + "capabilities": { + "obp70": "true" + }, + "condition": { + "visiblePages": [ + "10" + ] + } + }, + { + "name": "page10value1", + "label": "Field 1", + "type": "boatData", + "default": "", + "description": "The display for field one", + "category": "OBP70 Page 10", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page10type": [ + "Fluid", + "FourValues", + "FourValues2", + "OneValue", + "RollPitch", + "SixValues", + "ThreeValues", + "TwoValues", + "WindRoseFlex" + ], + "visiblePages": [ + "10" + ] + } + }, + { + "name": "page10value2", + "label": "Field 2", + "type": "boatData", + "default": "", + "description": "The display for field two", + "category": "OBP70 Page 10", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page10type": [ + "FourValues", + "FourValues2", + "RollPitch", + "SixValues", + "ThreeValues", + "TwoValues", + "WindRoseFlex" + ], + "visiblePages": [ + "10" + ] + } + }, + { + "name": "page10value3", + "label": "Field 3", + "type": "boatData", + "default": "", + "description": "The display for field three", + "category": "OBP70 Page 10", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page10type": [ + "FourValues", + "FourValues2", + "SixValues", + "ThreeValues", + "WindRoseFlex" + ], + "visiblePages": [ + "10" + ] + } + }, + { + "name": "page10value4", + "label": "Field 4", + "type": "boatData", + "default": "", + "description": "The display for field four", + "category": "OBP70 Page 10", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page10type": [ + "FourValues", + "FourValues2", + "SixValues", + "WindRoseFlex" + ], + "visiblePages": [ + "10" + ] + } + }, + { + "name": "page10value5", + "label": "Field 5", + "type": "boatData", + "default": "", + "description": "The display for field five", + "category": "OBP70 Page 10", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page10type": [ + "SixValues" + ], + "visiblePages": [ + "10" + ] + } + }, + { + "name": "page10value6", + "label": "Field 6", + "type": "boatData", + "default": "", + "description": "The display for field six", + "category": "OBP70 Page 10", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page10type": [ + "SixValues" + ], + "visiblePages": [ + "10" + ] + } + }, + { + "name": "page10fluid", + "label": "Fluid type", + "type": "list", + "default": "0", + "list": [ + { + "l": "Fuel (0)", + "v": "0" + }, + { + "l": "Water (1)", + "v": "1" + }, + { + "l": "Gray Water (2)", + "v": "2" + }, + { + "l": "Live Well (3)", + "v": "3" + }, + { + "l": "Oil (4)", + "v": "4" + }, + { + "l": "Black Water (5)", + "v": "5" + }, + { + "l": "Fuel Gasoline (6)", + "v": "6" + } + ], + "description": "Fluid type in tank", + "category": "OBP70 Page 10", + "capabilities": { + "obp70": "true" + }, + "condition": { + "page10type": "Fluid", + "visiblePages": [ + "10" + ] + } + } +] diff --git a/lib/obp60task/platformio.ini b/lib/obp60task/platformio.ini index 71046aa..20eb2d7 100644 --- a/lib/obp60task/platformio.ini +++ b/lib/obp60task/platformio.ini @@ -7,6 +7,64 @@ default_envs = obp60_s3 obp40_s3 +[env:obp70_s3] +platform = espressif32@6.8.1 +board_build.variants_dir = variants +#board = obp60_s3_n8 #ESP32-S3 N8, 8MB flash, no PSRAM +#board = obp60_s3_n16 #ESP32-S3 N16,16MB flash, no PSRAM, zero series +#board = obp60_s3_n8r8 #ESP32-S3 N8R8, 8MB flash, 8MB PSRAM +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} + Wire + SPI + ESP32time + HTTPClient + WiFiClientSecure + esphome/AsyncTCP-esphome@2.0.1 + robtillaart/PCF8574@0.3.9 + adafruit/Adafruit Unified Sensor @ 1.1.13 + blemasle/MCP23017@2.0.0 + adafruit/Adafruit BusIO@1.5.0 + adafruit/Adafruit GFX Library@1.11.9 + #zinggjm/GxEPD2@1.5.8 + #https://github.com/ZinggJM/GxEPD2 + https://github.com/thooge/GxEPD2 + sstaub/Ticker@4.4.0 + adafruit/Adafruit BMP280 Library@2.6.2 + adafruit/Adafruit BME280 Library@2.2.2 + adafruit/Adafruit BMP085 Library@1.2.1 + enjoyneering/HTU21D@1.2.1 + robtillaart/INA226@0.2.0 + paulstoffregen/OneWire@2.3.8 + milesburton/DallasTemperature@3.11.0 + signetica/SunRise@2.0.2 + adafruit/Adafruit FRAM I2C@2.0.3 + lovyan03/LovyanGFX@1.2.19 +build_flags= + #https://thingpulse.com/usb-settings-for-logging-with-the-esp32-s3-in-platformio/?srsltid=AfmBOopGskbkr4GoeVkNlFaZXe_zXkLceKF6Rn-tmoXABCeAR2vWsdHL +# -D CORE_DEBUG_LEVEL=1 #Debug level for CPU core via CDC (serial device) +# -D TIME=$UNIX_TIME #Set PC time for RTC (only settable via VSC) + -D DISABLE_DIAGNOSTIC_OUTPUT #Disable diagnostic output for GxEPD2 lib + -D BOARD_OBP60S3 #Board OBP60 V2.1 with ESP32S3 +# -D HARDWARE_V20 #OBP60 hardware revision V2.0 + -D HARDWARE_V21 #OBP60 hardware revision V2.1 + -D DISPLAY_ST7796 #ST7796 TFT LCD display (480x320 color display) + -D OBP_TFT_ENABLE_SCALING=1 #TFT scaling on/off (1=scale to max Y=320, 0=no scaling) + -D OBP_TFT_SCALE_ANTIALIAS=1 #Antialiasing for TFT scaling (1=on, 0=off) +# -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 +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 +upload_speed = 230400 +monitor_speed = 115200 + [env:obp60_s3] platform = espressif32@6.8.1 board_build.variants_dir = variants @@ -45,7 +103,6 @@ lib_deps = milesburton/DallasTemperature@3.11.0 signetica/SunRise@2.0.2 adafruit/Adafruit FRAM I2C@2.0.3 - lovyan03/LovyanGFX@^1.2.19 build_flags= #https://thingpulse.com/usb-settings-for-logging-with-the-esp32-s3-in-platformio/?srsltid=AfmBOopGskbkr4GoeVkNlFaZXe_zXkLceKF6Rn-tmoXABCeAR2vWsdHL # -D CORE_DEBUG_LEVEL=1 #Debug level for CPU core via CDC (serial device) @@ -55,12 +112,9 @@ build_flags= # -D HARDWARE_V20 #OBP60 hardware revision V2.0 -D HARDWARE_V21 #OBP60 hardware revision V2.1 # -D DISPLAY_GDEW042T2 #old E-Ink display from GoodDisplay (Waveshare), R10 0.47 ohm - very good -# -D DISPLAY_GDEY042T81 #new E-Ink display from GoodDisplay (Waveshare), R10 2.2 ohm - good (contast lost by shunshine) + -D DISPLAY_GDEY042T81 #new E-Ink display from GoodDisplay (Waveshare), R10 2.2 ohm - good (contast lost by shunshine) # -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_ST7796 #ST7796 TFT LCD display (480x320 color display) - -D OBP_TFT_ENABLE_SCALING=1 #TFT scaling on/off (1=scale to max Y=320, 0=no scaling) - -D OBP_TFT_SCALE_ANTIALIAS=1 #Antialiasing for TFT scaling (1=on, 0=off) # -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} From c1a49fac4ba6c5acf9b0e4be742965e7b3bf6d0c Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 13 Mar 2026 18:04:09 +0100 Subject: [PATCH 59/75] Changes --- lib/obp60task/platformio.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/obp60task/platformio.ini b/lib/obp60task/platformio.ini index 20eb2d7..9cb5fc7 100644 --- a/lib/obp60task/platformio.ini +++ b/lib/obp60task/platformio.ini @@ -26,7 +26,7 @@ lib_deps = ESP32time HTTPClient WiFiClientSecure - esphome/AsyncTCP-esphome@2.0.1 + esphome/AsyncTCP-esphome@2.1.1 robtillaart/PCF8574@0.3.9 adafruit/Adafruit Unified Sensor @ 1.1.13 blemasle/MCP23017@2.0.0 @@ -84,7 +84,7 @@ lib_deps = ESP32time HTTPClient WiFiClientSecure - esphome/AsyncTCP-esphome@2.0.1 + esphome/AsyncTCP-esphome@2.1.1 robtillaart/PCF8574@0.3.9 adafruit/Adafruit Unified Sensor @ 1.1.13 blemasle/MCP23017@2.0.0 @@ -140,7 +140,7 @@ lib_deps = ESP32time HTTPClient WiFiClientSecure - esphome/AsyncTCP-esphome@2.0.1 + esphome/AsyncTCP-esphome@2.1.1 robtillaart/PCF8574@0.3.9 adafruit/Adafruit Unified Sensor @ 1.1.13 blemasle/MCP23017@2.0.0 From 4e0bc88419ab6c480b16f9704958a9676baa56bb Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 13 Mar 2026 19:57:44 +0100 Subject: [PATCH 60/75] Fix custom config for obp70_s3 --- lib/obp60task/platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/obp60task/platformio.ini b/lib/obp60task/platformio.ini index 9cb5fc7..dfb0300 100644 --- a/lib/obp60task/platformio.ini +++ b/lib/obp60task/platformio.ini @@ -16,7 +16,7 @@ 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_config = lib/obp60task/config_obp70.json custom_script = lib/obp60task/extra_task.py framework = arduino lib_deps = From 604e2a15bdb0237d9ac1affaeceb231e91873136 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 13 Mar 2026 20:02:30 +0100 Subject: [PATCH 61/75] Fix for platformio.ini --- lib/obp60task/platformio.ini | 1 + lib/obp60task/run_obp70_s3 | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 lib/obp60task/run_obp70_s3 diff --git a/lib/obp60task/platformio.ini b/lib/obp60task/platformio.ini index dfb0300..bcb213a 100644 --- a/lib/obp60task/platformio.ini +++ b/lib/obp60task/platformio.ini @@ -4,6 +4,7 @@ #by uncommenting the next line default_envs = + obp70_s3 obp60_s3 obp40_s3 diff --git a/lib/obp60task/run_obp70_s3 b/lib/obp60task/run_obp70_s3 new file mode 100644 index 0000000..3d8fef0 --- /dev/null +++ b/lib/obp60task/run_obp70_s3 @@ -0,0 +1,10 @@ +#!/bin/bash + +# This script compile the software für OBP60-S3 + +# Attention! Start this cript only in the Gitpod Docker container. +# Start the script with: bash run + +# Compile the firmware +echo "Compiling Firmware" +platformio run -e obp70_s3 \ No newline at end of file From 54dcb5fd5aac32c3f61e9773167e00a20e3f0887 Mon Sep 17 00:00:00 2001 From: Tobias Edler Date: Fri, 13 Mar 2026 20:48:16 +0100 Subject: [PATCH 62/75] mark scripts as executable --- lib/obp60task/run_install_tools | 0 lib/obp60task/run_obp40_s3 | 0 lib/obp60task/run_obp60_s3 | 0 lib/obp60task/run_obp70_s3 | 0 4 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 lib/obp60task/run_install_tools mode change 100644 => 100755 lib/obp60task/run_obp40_s3 mode change 100644 => 100755 lib/obp60task/run_obp60_s3 mode change 100644 => 100755 lib/obp60task/run_obp70_s3 diff --git a/lib/obp60task/run_install_tools b/lib/obp60task/run_install_tools old mode 100644 new mode 100755 diff --git a/lib/obp60task/run_obp40_s3 b/lib/obp60task/run_obp40_s3 old mode 100644 new mode 100755 diff --git a/lib/obp60task/run_obp60_s3 b/lib/obp60task/run_obp60_s3 old mode 100644 new mode 100755 diff --git a/lib/obp60task/run_obp70_s3 b/lib/obp60task/run_obp70_s3 old mode 100644 new mode 100755 From f165c628ae4ec80c41d521644a55d1f0e8ad745d Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Fri, 13 Mar 2026 20:28:08 +0000 Subject: [PATCH 63/75] Fix NetworkClient.cpp for more robust data transmission and decompression --- lib/obp60task/NetworkClient.cpp | 221 +++++++++++++++++++++++++++++-- lib/obp60task/NetworkClient.h | 12 ++ lib/obp60task/PageNavigation.cpp | 11 +- 3 files changed, 226 insertions(+), 18 deletions(-) diff --git a/lib/obp60task/NetworkClient.cpp b/lib/obp60task/NetworkClient.cpp index 96a649d..d4a1605 100644 --- a/lib/obp60task/NetworkClient.cpp +++ b/lib/obp60task/NetworkClient.cpp @@ -7,12 +7,29 @@ extern "C" { #include "puff.h" } +static uint32_t crc32_update(uint32_t crc, const uint8_t* data, size_t len) { + crc = ~crc; + for (size_t i = 0; i < len; ++i) { + crc ^= data[i]; + for (int bit = 0; bit < 8; ++bit) { + uint32_t mask = -(int32_t)(crc & 1U); + crc = (crc >> 1) ^ (0xEDB88320U & mask); + } + } + return ~crc; +} + // Constructor NetworkClient::NetworkClient(size_t reserveSize) : _doc(reserveSize), _valid(false), _jsonRaw(nullptr), - _jsonRawLen(0) + _jsonRawLen(0), + _imageWidth(0), + _imageHeight(0), + _numberPixels(0), + _pictureBase64(nullptr), + _pictureBase64Len(0) { } @@ -24,6 +41,100 @@ NetworkClient::~NetworkClient() { } } +bool NetworkClient::findJsonIntField(const char* json, size_t len, const char* key, int& outValue) { + if (json == nullptr || key == nullptr || len == 0) { + return false; + } + + char pattern[64]; + int plen = snprintf(pattern, sizeof(pattern), "\"%s\"", key); + if (plen <= 0 || (size_t)plen >= sizeof(pattern)) { + return false; + } + + const char* keyPos = strstr(json, pattern); + if (keyPos == nullptr) { + return false; + } + + const char* end = json + len; + const char* colon = strchr(keyPos + plen, ':'); + if (colon == nullptr || colon >= end) { + return false; + } + + const char* p = colon + 1; + while (p < end && (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n')) { + ++p; + } + if (p >= end) { + return false; + } + + char* parseEnd = nullptr; + long value = strtol(p, &parseEnd, 10); + if (parseEnd == p) { + return false; + } + outValue = (int)value; + return true; +} + +bool NetworkClient::extractJsonStringInPlace(char* json, size_t len, const char* key, char*& outValue, size_t& outLen) { + outValue = nullptr; + outLen = 0; + + if (json == nullptr || key == nullptr || len == 0) { + return false; + } + + char pattern[64]; + int plen = snprintf(pattern, sizeof(pattern), "\"%s\"", key); + if (plen <= 0 || (size_t)plen >= sizeof(pattern)) { + return false; + } + + char* keyPos = strstr(json, pattern); + if (keyPos == nullptr) { + return false; + } + + char* end = json + len; + char* colon = strchr(keyPos + plen, ':'); + if (colon == nullptr || colon >= end) { + return false; + } + + char* p = colon + 1; + while (p < end && (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n')) { + ++p; + } + if (p >= end || *p != '"') { + return false; + } + + char* valueStart = p + 1; + char* cur = valueStart; + while (cur < end) { + if (*cur == '\\') { + ++cur; + if (cur < end) { + ++cur; + } + continue; + } + if (*cur == '"') { + *cur = '\0'; + outValue = valueStart; + outLen = (size_t)(cur - valueStart); + return true; + } + ++cur; + } + + return false; +} + // Skip GZIP Header an goto DEFLATE content int NetworkClient::skipGzipHeader(const uint8_t* data, size_t len) { if (len < 10) return -1; @@ -207,8 +318,16 @@ bool NetworkClient::httpGetGzip(const String& url, uint8_t*& outData, size_t& ou return false; } + if (total > 0 && (int)len != total) { + Serial.printf("Plain response incomplete: got=%d expected=%d\n", (int)len, total); + if (stream) stream->stop(); + http.end(); + free(buffer); + return false; + } + // Return plain body to caller - outData = (uint8_t*)malloc(len); + outData = (uint8_t*)malloc(len + 1); if (!outData) { Serial.println("Malloc failed outData (plain)."); // --- Added: Force-close connection only if aborted to avoid TCP RST storms --- @@ -218,6 +337,7 @@ bool NetworkClient::httpGetGzip(const String& url, uint8_t*& outData, size_t& ou return false; } memcpy(outData, buffer, len); + outData[len] = 0; outLen = len; http.end(); @@ -284,6 +404,13 @@ bool NetworkClient::httpGetGzip(const String& url, uint8_t*& outData, size_t& ou // NEW: only attempt gzip parse/decompress after we have a complete body (when Content-Length is known) // This avoids wasting heap with repeated malloc/free and reduces fragmentation over long runtimes. + if (!aborting) { + if (total > 0 && (int)len != total) { + Serial.printf("GZIP response incomplete: got=%d expected=%d\n", (int)len, total); + aborting = true; + } + } + if (!aborting) { if (len < 20) { aborting = true; @@ -308,7 +435,7 @@ bool NetworkClient::httpGetGzip(const String& url, uint8_t*& outData, size_t& ou } aborting = true; } else { - uint8_t* test = (uint8_t*)malloc((size_t)outNeeded); + uint8_t* test = (uint8_t*)malloc((size_t)outNeeded + 1); if (!test) { Serial.println("Malloc failed test buffer, aborting."); aborting = true; @@ -318,10 +445,36 @@ bool NetworkClient::httpGetGzip(const String& url, uint8_t*& outData, size_t& ou int res = puff(test, &testLen, buffer + headerOffset, &srcLen); if (res == 0) { - if (DEBUGING) {Serial.printf("Decompress OK! Size: %lu bytes\n", testLen);} - outData = test; - outLen = (size_t)testLen; - complete = true; + uint32_t trailerCrc = + (uint32_t)buffer[len - 8] | + ((uint32_t)buffer[len - 7] << 8) | + ((uint32_t)buffer[len - 6] << 16) | + ((uint32_t)buffer[len - 5] << 24); + uint32_t trailerIsize = + (uint32_t)buffer[len - 4] | + ((uint32_t)buffer[len - 3] << 8) | + ((uint32_t)buffer[len - 2] << 16) | + ((uint32_t)buffer[len - 1] << 24); + uint32_t calcCrc = crc32_update(0, test, (size_t)testLen); + uint32_t calcIsize = (uint32_t)testLen; + + if (calcCrc != trailerCrc || calcIsize != trailerIsize) { + Serial.printf( + "GZIP CRC/ISIZE mismatch crc=%08lx/%08lx isize=%lu/%lu\n", + (unsigned long)calcCrc, + (unsigned long)trailerCrc, + (unsigned long)calcIsize, + (unsigned long)trailerIsize + ); + free(test); + aborting = true; + } else { + test[testLen] = 0; + if (DEBUGING) {Serial.printf("Decompress OK! Size: %lu bytes\n", testLen);} + outData = test; + outLen = (size_t)testLen; + complete = true; + } } else { if (DEBUGING) { Serial.printf("Decompress failed: res=%d out=%lu src=%lu\n", res, testLen, srcLen); @@ -355,6 +508,11 @@ bool NetworkClient::fetchAndDecompressJson(const String& url) { _valid = false; _doc.clear(); + _imageWidth = 0; + _imageHeight = 0; + _numberPixels = 0; + _pictureBase64 = nullptr; + _pictureBase64Len = 0; if (_jsonRaw != nullptr) { free(_jsonRaw); @@ -370,11 +528,24 @@ bool NetworkClient::fetchAndDecompressJson(const String& url) { return false; } - // Parse in zero-copy mode and keep the backing buffer alive in the class. - DeserializationError err = deserializeJson(_doc, reinterpret_cast(raw), rawLen); + char* json = reinterpret_cast(raw); + bool ok = true; + ok = findJsonIntField(json, rawLen, "number_pixels", _numberPixels) && ok; + ok = findJsonIntField(json, rawLen, "width", _imageWidth) && ok; + ok = findJsonIntField(json, rawLen, "height", _imageHeight) && ok; + ok = extractJsonStringInPlace(json, rawLen, "picture_base64", _pictureBase64, _pictureBase64Len) && ok; - if (err) { - Serial.printf("JSON ERROR: %s\n", err.c_str()); + if (!ok) { + Serial.println("JSON field extraction failed."); + free(raw); + return false; + } + + if (_imageWidth <= 0 || _imageHeight <= 0 || _pictureBase64Len == 0) { + Serial.printf("JSON invalid geometry/data w=%d h=%d b64=%u\n", + _imageWidth, + _imageHeight, + (unsigned int)_pictureBase64Len); free(raw); return false; } @@ -382,7 +553,13 @@ bool NetworkClient::fetchAndDecompressJson(const String& url) { _jsonRaw = raw; _jsonRawLen = rawLen; - if (DEBUGING) {Serial.println("JSON OK!");} + if (DEBUGING) { + Serial.printf("JSON fields OK: num=%d w=%d h=%d b64=%u\n", + _numberPixels, + _imageWidth, + _imageHeight, + (unsigned int)_pictureBase64Len); + } _valid = true; return true; } @@ -391,6 +568,26 @@ JsonDocument& NetworkClient::json() { return _doc; } +int NetworkClient::imageWidth() const { + return _imageWidth; +} + +int NetworkClient::imageHeight() const { + return _imageHeight; +} + +int NetworkClient::numberPixels() const { + return _numberPixels; +} + +const char* NetworkClient::pictureBase64() const { + return _pictureBase64; +} + +size_t NetworkClient::pictureBase64Len() const { + return _pictureBase64Len; +} + bool NetworkClient::isValid() const { return _valid; } diff --git a/lib/obp60task/NetworkClient.h b/lib/obp60task/NetworkClient.h index 80e0703..a04bf1f 100644 --- a/lib/obp60task/NetworkClient.h +++ b/lib/obp60task/NetworkClient.h @@ -16,6 +16,11 @@ public: bool fetchAndDecompressJson(const String& url); JsonDocument& json(); + int imageWidth() const; + int imageHeight() const; + int numberPixels() const; + const char* pictureBase64() const; + size_t pictureBase64Len() const; bool isValid() const; private: @@ -23,8 +28,15 @@ private: bool _valid; uint8_t* _jsonRaw; size_t _jsonRawLen; + int _imageWidth; + int _imageHeight; + int _numberPixels; + char* _pictureBase64; + size_t _pictureBase64Len; int skipGzipHeader(const uint8_t* data, size_t len); bool httpGetGzip(const String& url, uint8_t*& outData, size_t& outLen); + static bool findJsonIntField(const char* json, size_t len, const char* key, int& outValue); + static bool extractJsonStringInPlace(char* json, size_t len, const char* key, char*& outValue, size_t& outLen); }; diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp index 44045fd..31153e7 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -456,10 +456,9 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map failCount = 0; nextAllowedMs = now + 1000; // keep 1 Hz on success - auto& json = net.json(); // Extract JSON content - int numPix = json["number_pixels"] | 0; // Read number of pixels - imgWidth = json["width"] | 0; // Read width of image - imgHeight = json["height"] | 0; // Read height og image + int numPix = net.numberPixels(); // Read number of pixels + imgWidth = net.imageWidth(); // Read width of image + imgHeight = net.imageHeight(); // Read height of image size_t requiredBytesMono = 0; size_t requiredBytesRgb565 = 0; if (imgWidth > 0 && imgHeight > 0){ @@ -471,12 +470,12 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map return PAGE_UPDATE; } - const char* b64src = json["picture_base64"].as(); // Read picture as Base64 content + const char* b64src = net.pictureBase64(); // Read picture as Base64 content if (b64src == nullptr){ LOG_DEBUG(GwLog::ERROR,"Error PageNavigation: picture_base64 missing"); return PAGE_UPDATE; } - size_t b64len = strlen(b64src); // Calculate length of Base64 content + size_t b64len = net.pictureBase64Len(); // 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) { From a7cdfe700bf17d1d26fc40ecca3bd7a415a31b57 Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Fri, 13 Mar 2026 20:57:51 +0000 Subject: [PATCH 64/75] Typo --- lib/obp60task/PageNavigation.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp index 31153e7..8be5e7e 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -475,7 +475,7 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map LOG_DEBUG(GwLog::ERROR,"Error PageNavigation: picture_base64 missing"); return PAGE_UPDATE; } - size_t b64len = net.pictureBase64Len(); // Calculate length of Base64 content + size_t b64len = net.pictureBase64Len(); // 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) { @@ -485,7 +485,6 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map memcpy(b64, b64src, b64len + 1); // Copy Base64 content in PSRAM // Set image buffer in PSRAM - //size_t imgSize = getdisplay().width() * getdisplay().height(); size_t imgSize = (numPix > 0) ? (size_t)numPix : requiredBytesMono; // Calculate image size if (imgSize < requiredBytesMono){ imgSize = requiredBytesMono; @@ -538,7 +537,7 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map } #endif - // Copy actual navigation man to ackup map + // Copy actual navigation map to backup map imageBackupWidth = imgWidth; imageBackupHeight = imgHeight; imageBackupSize = imgSize; From d13c4af9cbe48112b44183f116acfdb26f9fc3c9 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 13 Mar 2026 21:58:29 +0100 Subject: [PATCH 65/75] Changes --- lib/obp60task/debugging.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/obp60task/debugging.txt b/lib/obp60task/debugging.txt index c0a22e0..73e4f3e 100644 --- a/lib/obp60task/debugging.txt +++ b/lib/obp60task/debugging.txt @@ -3,4 +3,7 @@ Debugging tool log.txt = text file with error messages from terminal console -tools/decoder.py -p ESP32S3 -t ~/.platformio/packages/toolchain-xtensa-esp32s3/ -e .pio/build/obp60_s3/firmware.elf log.txt \ No newline at end of file +cd /home/norbert/esp32-nmea2000-obp60 +tools/decoder.py -p ESP32S3 -t ~/.platformio/packages/toolchain-xtensa-esp32s3/ -e .pio/build/obp60_s3/firmware.elf log.txt + +tools/decoder.py -p ESP32S3 -t ~/.platformio/packages/toolchain-xtensa-esp32s3/ -e .pio/build/obp70_s3/firmware.elf /home/norbert/Dokumente/Multifunktionsdisplay_OBP60/Crashes/20260313/log.txt \ No newline at end of file From 588c88dffff9874f59dafb848112e6c03f03c991 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Sat, 14 Mar 2026 23:08:57 +0100 Subject: [PATCH 66/75] Fix obp60task.h for new obp70 config --- lib/obp60task/obp60task.h | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/obp60task/obp60task.h b/lib/obp60task/obp60task.h index b4e1400..784eb30 100644 --- a/lib/obp60task/obp60task.h +++ b/lib/obp60task/obp60task.h @@ -35,13 +35,19 @@ // OBP60 Task void OBP60Task(GwApi *param); DECLARE_USERTASK_PARAM(OBP60Task, 35000); // Need 35k RAM as stack size - #ifdef HARDWARE_V21 + #if defined(BOARD_OBP60S3) && defined(HARDWARE_V21) && defined(DISPLAY_ST7796) + DECLARE_CAPABILITY(obp70,true); + #endif + #if defined(BOARD_OBP60S3) && defined(HARDWARE_V21) && !defined(DISPLAY_ST7796) DECLARE_CAPABILITY(obp60,true); #endif #ifdef BOARD_OBP40S3 DECLARE_CAPABILITY(obp40,true) #endif - #ifdef BOARD_OBP60S3 + #if defined(BOARD_OBP60S3) && defined(HARDWARE_V21) && defined(DISPLAY_ST7796) + DECLARE_STRING_CAPABILITY(HELP_URL, "https://obp60-v2-docu.readthedocs.io/en/latest/"); // Link to help pages + #endif + #if defined(BOARD_OBP60S3) && defined(HARDWARE_V21) && !defined(DISPLAY_ST7796) DECLARE_STRING_CAPABILITY(HELP_URL, "https://obp60-v2-docu.readthedocs.io/en/latest/"); // Link to help pages #endif #ifdef BOARD_OBP40S3 From 27d0e5d1b7f987c00e5db3ed06ca68d6e51c01e6 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Sun, 15 Mar 2026 15:23:52 +0100 Subject: [PATCH 67/75] Modify PageNavigation --- lib/obp60task/PageNavigation.cpp | 12 ++++++++++++ lib/obp60task/config_obp70.json | 5 ++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp index 8be5e7e..95d0b6a 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -350,6 +350,18 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map mType = 9; dType = 1; } + else if(mapType == "C-Map"){ + mType = 103486987; + dType = 1; + } + else if(mapType == "Garmin Fish"){ + mType = 113486987; + dType = 1; + } + else if(mapType == "Garmin Nav"){ + mType = 123486987; + dType = 1; + } else{ mType = 1; dType = 1; diff --git a/lib/obp60task/config_obp70.json b/lib/obp60task/config_obp70.json index 58cbe4d..b9b7a99 100644 --- a/lib/obp60task/config_obp70.json +++ b/lib/obp60task/config_obp70.json @@ -1104,7 +1104,10 @@ "Google Street", "Open Topo Map", "Stadimaps Toner", - "Free Nautical Chart" + "Free Nautical Chart", + "C-Map", + "Garmin Fish", + "Garmin Nav" ], "category": "OBP70 Navigation", "capabilities": { From 7c6005958e8e51f934898eaa7d4582efc81ef21b Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Thu, 19 Mar 2026 16:17:22 +0000 Subject: [PATCH 68/75] Add new TFT display ILI9488 --- lib/obp60task/OBP60Extensions.cpp | 12 ++++---- lib/obp60task/OBP60Extensions.h | 46 ++++++++++++++++++++----------- lib/obp60task/OBPcharts.cpp | 2 +- lib/obp60task/PageNavigation.cpp | 18 ++++++------ lib/obp60task/obp60task.cpp | 18 ++++++------ lib/obp60task/obp60task.h | 8 +++--- lib/obp60task/platformio.ini | 4 ++- 7 files changed, 62 insertions(+), 46 deletions(-) diff --git a/lib/obp60task/OBP60Extensions.cpp b/lib/obp60task/OBP60Extensions.cpp index 6566a62..8a7b879 100644 --- a/lib/obp60task/OBP60Extensions.cpp +++ b/lib/obp60task/OBP60Extensions.cpp @@ -56,7 +56,7 @@ GxEPD2_BW display(GxEPD2_4 GxEPD2_BW & getdisplay(){return display;} #endif -#ifdef DISPLAY_ST7796 +#ifdef TFT_DISPLAY // panel device + offscreen shadow framebuffer static LGFX panelDisplay; static LGFXCanvas shadowDisplay(&panelDisplay); @@ -307,7 +307,7 @@ void deepSleep(CommonData &common){ getdisplay().setCursor(65, 175); getdisplay().print("To wake up press key and wait 5s"); displayNextPage(); // Update display contents - #ifdef DISPLAY_ST7796 + #ifdef TFT_DISPLAY getpaneldisplay().powerSave(true); // Display power save #else getdisplay().powerOff(); // Display power off @@ -336,7 +336,7 @@ void deepSleep(CommonData &common){ getdisplay().setCursor(65, 175); getdisplay().print("To wake up press wheel and wait 5s"); displayNextPage(); // Partial update - #ifdef DISPLAY_ST7796 + #ifdef TFT_DISPLAY getpaneldisplay().powerSave(true); // Display power save #else getdisplay().powerOff(); // Display power off @@ -575,7 +575,7 @@ void drawButtonCenter(int16_t cx, int16_t cy, int8_t sx, int8_t sy, String text, void drawTextRalign(int16_t x, int16_t y, String text) { int16_t x1, y1; uint16_t w, h; -#ifdef DISPLAY_ST7796 +#ifdef TFT_DISPLAY w = getdisplay().textWidth(text); h = getdisplay().fontHeight(); #else @@ -1071,7 +1071,7 @@ void displayRudderPosition(int rudderPosition, uint8_t rangeDeg, uint16_t cx, ui String lbl = String(angle); int16_t bx, by; uint16_t bw, bh; - #ifdef DISPLAY_ST7796 + #ifdef TFT_DISPLAY // LovyanGFX: compute width/height manually bw = getdisplay().textWidth(lbl); bh = getdisplay().fontHeight(); @@ -1100,7 +1100,7 @@ void doImageRequest(GwApi *api, int *pageno, const PageStruct pages[MAX_PAGE_NUM uint8_t *fb = nullptr; // EPD framebuffer std::vector imageBuffer; // image in webserver transferbuffer String mimetype; - #ifndef DISPLAY_ST7796 + #ifndef TFT_DISPLAY fb = getdisplay().getBuffer(); // available only for EPD #endif if (!fb) { diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index c6db616..1dc130f 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -9,8 +9,14 @@ #include // I2C FRAM #include -#ifdef DISPLAY_ST7796 - #include // TFT LCD lib for ST7796 color displays +#ifdef TFT_DISPLAY + #if !defined(TFT_320x480_ST7796) && !defined(TFT_320x480_ILI9488) + #error "TFT_DISPLAY requires one panel type: TFT_320x480_ST7796 or TFT_320x480_ILI9488" + #endif + #if defined(TFT_320x480_ST7796) && defined(TFT_320x480_ILI9488) + #error "Select exactly one TFT panel type: TFT_320x480_ST7796 or TFT_320x480_ILI9488" + #endif + #include // TFT LCD lib for 320x480 color displays #undef GxEPD_WHITE #define GxEPD_WHITE TFT_WHITE // Replacement color for white on TFT (OBPHardware.h) #undef GxEPD_BLACK @@ -82,8 +88,8 @@ GxEPD2_BW & getdisplay(); GxEPD2_BW & getdisplay(); #endif -#ifdef DISPLAY_ST7796 -// LovyanGFX based display wrapper for ST7796 +#ifdef TFT_DISPLAY +// LovyanGFX based display wrapper for TFT panels class LGFX : public lgfx::LGFX_Device { public: lgfx::Bus_SPI _bus_instance; @@ -93,7 +99,11 @@ public: auto cfg = _bus_instance.config(); cfg.spi_host = SPI2_HOST; cfg.spi_mode = 0; - cfg.freq_write = 80000000; + #if defined(TFT_320x480_ILI9488) + cfg.freq_write = 40000000; + #else + cfg.freq_write = 80000000; + #endif cfg.freq_read = 16000000; cfg.pin_sclk = OBP_SPI_CLK; cfg.pin_mosi = OBP_SPI_DIN; @@ -264,7 +274,7 @@ public: // E-Ink interface compatibility void setFullWindow() { /* no-op on TFT */ } - // Runtime panel offset control (ST7796) + // Runtime panel offset control for TFT panels void setPanelOffset(int16_t x, int16_t y) { auto cfg = _panel_instance.config(); cfg.offset_x = x; @@ -283,7 +293,11 @@ private: lgfx::GFXglyph* _adfGlyphBridge = nullptr; uint16_t _adfGlyphCount = 0; const GFXfont* _currentAdfFont = nullptr; - lgfx::Panel_ST7796 _panel_instance; + #if defined(TFT_320x480_ST7796) + lgfx::Panel_ST7796 _panel_instance; + #elif defined(TFT_320x480_ILI9488) + lgfx::Panel_ILI9488 _panel_instance; + #endif }; class LGFXCanvas : public lgfx::LGFX_Sprite { @@ -437,7 +451,7 @@ bool initDisplayScaleBuffer(uint16_t width, uint16_t height); #define PAGE_UPDATE 1 // page wants display to update #define PAGE_HIBERNATE 2 // page wants displey to hibernate -#ifdef DISPLAY_ST7796 +#ifdef TFT_DISPLAY #ifndef OBP_TFT_ENABLE_SCALING #define OBP_TFT_ENABLE_SCALING 1 #endif @@ -487,7 +501,7 @@ inline void drawMonochromeBitmap( bool lsbFirst=false, // true: least significant bit = left/top pixel bool mirrorX=false) // true: bytes run right-to-left within each row { - #ifdef DISPLAY_ST7796 + #ifdef TFT_DISPLAY // TFT converts per‑pixel int bytesPerRow = (w + 7) / 8; for (int yy = 0; yy < h; yy++) { @@ -535,7 +549,7 @@ inline void displayDrawBitmap(int16_t x, int16_t y, const uint8_t *bmp, int16_t w, int16_t h, uint16_t color) { - #ifdef DISPLAY_ST7796 + #ifdef TFT_DISPLAY drawMonochromeBitmap(x, y, bmp, w, h, color); #else getdisplay().drawBitmap(x, y, bmp, w, h, color); @@ -543,7 +557,7 @@ inline void displayDrawBitmap(int16_t x, int16_t y, } inline void displayFirstPage() { - #ifdef DISPLAY_ST7796 + #ifdef TFT_DISPLAY initDisplayShadowBuffer(); #else getdisplay().firstPage(); @@ -551,7 +565,7 @@ inline void displayFirstPage() { } inline void displayNextPage() { - #ifdef DISPLAY_ST7796 + #ifdef TFT_DISPLAY if (initDisplayShadowBuffer()) { LGFXCanvas &src = getdisplay(); LGFX &dst = getpaneldisplay(); @@ -620,7 +634,7 @@ inline void displayNextPage() { } inline void displaySetPartialWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h) { - #ifdef DISPLAY_ST7796 + #ifdef TFT_DISPLAY // TFT LCD doesn't use partial windows (void)x; (void)y; (void)w; (void)h; #else @@ -629,18 +643,18 @@ inline void displaySetPartialWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t } inline void displaySetFullWindow() { - #ifdef DISPLAY_ST7796 + #ifdef TFT_DISPLAY // TFT LCD doesn't need setFullWindow() #else getdisplay().setFullWindow(); #endif } -// replacement for getTextBounds that works with both EPD and ST7796 +// replacement for getTextBounds that works with both EPD and TFT inline void displayGetTextBounds(const String &txt, int16_t x, int16_t y, int16_t *x0, int16_t *y0, uint16_t *w, uint16_t *h) { -#ifdef DISPLAY_ST7796 +#ifdef TFT_DISPLAY getdisplay().getTextBounds(txt, x, y, x0, y0, w, h); #else getdisplay().getTextBounds(txt, x, y, x0, y0, w, h); diff --git a/lib/obp60task/OBPcharts.cpp b/lib/obp60task/OBPcharts.cpp index b245972..9bf8f16 100644 --- a/lib/obp60task/OBPcharts.cpp +++ b/lib/obp60task/OBPcharts.cpp @@ -29,7 +29,7 @@ Chart::Chart(RingBuffer& dataBuf, double dfltRng, CommonData& common, bgColor = commonData->bgcolor; // display dimensions (avoid calling width()/height() on incomplete LGFX type) - #ifdef DISPLAY_ST7796 + #ifdef TFT_DISPLAY dWidth = 480; dHeight = 320; #else diff --git a/lib/obp60task/PageNavigation.cpp b/lib/obp60task/PageNavigation.cpp index 95d0b6a..046886c 100644 --- a/lib/obp60task/PageNavigation.cpp +++ b/lib/obp60task/PageNavigation.cpp @@ -13,7 +13,7 @@ NetworkClient net(JSON_BUFFER); // Define network client ImageDecoder decoder; // Define image decoder -#ifdef DISPLAY_ST7796 +#ifdef TFT_DISPLAY // Set to true to render a generated RGB565 color-bar test image. static constexpr bool kShowRgb565StripeTestImage = false; @@ -82,7 +82,7 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map commonData = &common; common.logger->logDebug(GwLog::LOG,"Instantiate PageNavigation"); imageBackupCapacity = (size_t)GxEPD_WIDTH * (size_t)GxEPD_HEIGHT; - #ifdef DISPLAY_ST7796 + #ifdef TFT_DISPLAY imageBackupCapacity *= 2U; #endif imageBackupData = (uint8_t*)heap_caps_malloc(imageBackupCapacity, MALLOC_CAP_SPIRAM); @@ -415,7 +415,7 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map // 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) - #ifdef DISPLAY_ST7796 + #ifdef TFT_DISPLAY "oformat=3" + // Image output format in JSON: 3=RGB565 format #else "oformat=4" + // Image output format in JSON: 4=b/w 1-Bit format @@ -425,7 +425,7 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map "&lon=" + String(longitude, 6) + // Longitude "&mrot=" + mapRot + // Rotation angle navigation map in degree "&mtype=" + mType + // Default Map: Open Street Map - #ifdef DISPLAY_ST7796 + #ifdef TFT_DISPLAY "&itype=1" + // Image type: 1=Color #else "&itype=4" + // Image type: 4=b/w with dithering @@ -501,7 +501,7 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map if (imgSize < requiredBytesMono){ imgSize = requiredBytesMono; } - #ifdef DISPLAY_ST7796 + #ifdef TFT_DISPLAY if (imgSize < requiredBytesRgb565){ imgSize = requiredBytesRgb565; } @@ -537,11 +537,11 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map } bool imageIsRgb565 = false; - #ifdef DISPLAY_ST7796 + #ifdef TFT_DISPLAY imageIsRgb565 = (decodedSize >= requiredBytesRgb565); #endif - #ifdef DISPLAY_ST7796 + #ifdef TFT_DISPLAY if (kShowRgb565StripeTestImage) { createRgb565StripeImage(reinterpret_cast(imageData), imgWidth, imgHeight); decodedSize = requiredBytesRgb565; @@ -563,7 +563,7 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map lostCounter = 0; // Show image (navigation map) - #ifdef DISPLAY_ST7796 + #ifdef TFT_DISPLAY if (imageIsRgb565) { drawRgb565Image(0, 25, reinterpret_cast(imageData), imgWidth, imgHeight); } else { @@ -594,7 +594,7 @@ bool showValues = false; // Show values HDT, SOG, DBT in navigation map // Show backup image (backup navigation map) if (hasImageBackup) { - #ifdef DISPLAY_ST7796 + #ifdef TFT_DISPLAY if (imageBackupIsRgb565) { drawRgb565Image(0, 25, reinterpret_cast(imageBackupData), imageBackupWidth, imageBackupHeight); } else { diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index 606ca83..2c76b33 100644 --- a/lib/obp60task/obp60task.cpp +++ b/lib/obp60task/obp60task.cpp @@ -284,7 +284,7 @@ void underVoltageError(CommonData &common) { getdisplay().setCursor(65, 175); getdisplay().print("Charge battery and restart system"); displayNextPage(); // Partial update - #ifdef DISPLAY_ST7796 + #ifdef TFT_DISPLAY getpaneldisplay().powerSave(true); // Display power save #else getdisplay().powerOff(); // Display power off @@ -308,7 +308,7 @@ void underVoltageError(CommonData &common) { getdisplay().setCursor(65, 175); getdisplay().print("To wake up repower system"); displayNextPage(); // Partial update - #ifdef DISPLAY_ST7796 + #ifdef TFT_DISPLAY getpaneldisplay().powerSave(true); // Display power save #else getdisplay().powerOff(); // Display power off @@ -382,13 +382,13 @@ void OBP60Task(GwApi *api){ #ifdef DISPLAY_GDEY042T81 getdisplay().init(115200, true, 2, false); // Init for Waveshare boards with "clever" reset circuit, 2ms reset pulse - #elif defined DISPLAY_ST7796 - getpaneldisplay().init(); // Init for ST7796 TFT LCD panel + #elif defined(TFT_DISPLAY) + getpaneldisplay().init(); // Init for TFT LCD panel #else getdisplay().init(115200); // Init for normal displays #endif - #ifdef DISPLAY_ST7796 + #ifdef TFT_DISPLAY getpaneldisplay().setRotation(0); // Set display orientation (horizontal) getpaneldisplay().setPanelOffset(0, 0); // Use full native framebuffer coordinates getpaneldisplay().fillScreen(0x0000); // Initialize full TFT screen to black (native RGB565) @@ -741,7 +741,7 @@ void OBP60Task(GwApi *api){ starttime1 = millis(); starttime2 = millis(); displaySetFullWindow(); // Set full update - #ifdef DISPLAY_ST7796 + #ifdef TFT_DISPLAY // TFT LCD doesn't need refresh operations #else if(fastrefresh == "true"){ @@ -773,7 +773,7 @@ void OBP60Task(GwApi *api){ starttime2 = millis(); LOG_DEBUG(GwLog::DEBUG,"E-Ink full refresh first 5 min"); displaySetFullWindow(); // Set full update - #ifdef DISPLAY_ST7796 + #ifdef TFT_DISPLAY // TFT LCD doesn't need refresh operations #else if(fastrefresh == "true"){ @@ -801,7 +801,7 @@ void OBP60Task(GwApi *api){ if(millis() > starttime2 + fullrefreshtime * 60 * 1000){ starttime2 = millis(); LOG_DEBUG(GwLog::DEBUG,"E-Ink full refresh"); - #ifdef DISPLAY_ST7796 + #ifdef TFT_DISPLAY // TFT LCD: no special refresh #else getdisplay().setFullWindow(); // Set full update @@ -905,7 +905,7 @@ void OBP60Task(GwApi *api){ displayNextPage(); // Partial update (fast) } if (ret & PAGE_HIBERNATE) { - #ifndef DISPLAY_ST7796 + #ifndef TFT_DISPLAY getdisplay().hibernate(); #endif } diff --git a/lib/obp60task/obp60task.h b/lib/obp60task/obp60task.h index 784eb30..af12a23 100644 --- a/lib/obp60task/obp60task.h +++ b/lib/obp60task/obp60task.h @@ -35,19 +35,19 @@ // OBP60 Task void OBP60Task(GwApi *param); DECLARE_USERTASK_PARAM(OBP60Task, 35000); // Need 35k RAM as stack size - #if defined(BOARD_OBP60S3) && defined(HARDWARE_V21) && defined(DISPLAY_ST7796) + #if defined(BOARD_OBP60S3) && defined(HARDWARE_V21) && defined(TFT_DISPLAY) DECLARE_CAPABILITY(obp70,true); #endif - #if defined(BOARD_OBP60S3) && defined(HARDWARE_V21) && !defined(DISPLAY_ST7796) + #if defined(BOARD_OBP60S3) && defined(HARDWARE_V21) && !defined(TFT_DISPLAY) DECLARE_CAPABILITY(obp60,true); #endif #ifdef BOARD_OBP40S3 DECLARE_CAPABILITY(obp40,true) #endif - #if defined(BOARD_OBP60S3) && defined(HARDWARE_V21) && defined(DISPLAY_ST7796) + #if defined(BOARD_OBP60S3) && defined(HARDWARE_V21) && defined(TFT_DISPLAY) DECLARE_STRING_CAPABILITY(HELP_URL, "https://obp60-v2-docu.readthedocs.io/en/latest/"); // Link to help pages #endif - #if defined(BOARD_OBP60S3) && defined(HARDWARE_V21) && !defined(DISPLAY_ST7796) + #if defined(BOARD_OBP60S3) && defined(HARDWARE_V21) && !defined(TFT_DISPLAY) DECLARE_STRING_CAPABILITY(HELP_URL, "https://obp60-v2-docu.readthedocs.io/en/latest/"); // Link to help pages #endif #ifdef BOARD_OBP40S3 diff --git a/lib/obp60task/platformio.ini b/lib/obp60task/platformio.ini index bcb213a..031e68c 100644 --- a/lib/obp60task/platformio.ini +++ b/lib/obp60task/platformio.ini @@ -55,7 +55,9 @@ build_flags= -D BOARD_OBP60S3 #Board OBP60 V2.1 with ESP32S3 # -D HARDWARE_V20 #OBP60 hardware revision V2.0 -D HARDWARE_V21 #OBP60 hardware revision V2.1 - -D DISPLAY_ST7796 #ST7796 TFT LCD display (480x320 color display) + -D TFT_DISPLAY #Enable TFT LCD display path (instead of E-Ink) + -D TFT_320x480_ST7796 #TFT panel type: ST7796 (320x480) +# -D TFT_320x480_ILI9488 #TFT panel type: ILI9488 (320x480), use instead of ST7796 -D OBP_TFT_ENABLE_SCALING=1 #TFT scaling on/off (1=scale to max Y=320, 0=no scaling) -D OBP_TFT_SCALE_ANTIALIAS=1 #Antialiasing for TFT scaling (1=on, 0=off) # -D ENABLE_PATCHES #enable patching of gateway code From 5fcca363f814fb00b0c7b248c5d0bff1f92ee666 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Thu, 19 Mar 2026 17:34:10 +0100 Subject: [PATCH 69/75] Modify TFT display initialisiation --- lib/obp60task/OBP60Extensions.h | 54 +++++++++++++++++++++++++++++---- lib/obp60task/platformio.ini | 4 +-- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index 1dc130f..9e2bc99 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -94,16 +94,13 @@ class LGFX : public lgfx::LGFX_Device { public: lgfx::Bus_SPI _bus_instance; + #ifdef TFT_320x480_ST7796 LGFX(void) { { auto cfg = _bus_instance.config(); cfg.spi_host = SPI2_HOST; cfg.spi_mode = 0; - #if defined(TFT_320x480_ILI9488) - cfg.freq_write = 40000000; - #else - cfg.freq_write = 80000000; - #endif + cfg.freq_write = 80000000; // High speed ST7796 cfg.freq_read = 16000000; cfg.pin_sclk = OBP_SPI_CLK; cfg.pin_mosi = OBP_SPI_DIN; @@ -121,7 +118,7 @@ public: cfg.panel_height = 480; // Native hight resolution cfg.offset_x = 0; // No panel offset: full framebuffer mapping cfg.offset_y = 0; // No panel offset: full framebuffer mapping - cfg.offset_rotation = 3; // Rotate display content conter clock wise 90 deg + cfg.offset_rotation = 3; // Rotate display content conter clock wise 90 deg ST7796 cfg.dummy_read_pixel = 8; cfg.dummy_read_bits = 1; cfg.memory_width = 320; @@ -139,6 +136,51 @@ public: // Match Adafruit GFX cursor semantics: y coordinate is text baseline. setTextDatum(textdatum_t::baseline_left); } + #endif + + #ifdef TFT_320x480_ILI9488 + LGFX(void) { + { + auto cfg = _bus_instance.config(); + cfg.spi_host = SPI2_HOST; + cfg.spi_mode = 0; + cfg.freq_write = 40000000; // Slow speed ILI9488 + cfg.freq_read = 16000000; + cfg.pin_sclk = OBP_SPI_CLK; + cfg.pin_mosi = OBP_SPI_DIN; + cfg.pin_miso = -1; + cfg.pin_dc = OBP_SPI_DC; + _bus_instance.config(cfg); + _panel_instance.setBus(&_bus_instance); + } + { + auto cfg = _panel_instance.config(); + cfg.pin_cs = OBP_SPI_CS; + cfg.pin_rst = OBP_SPI_RST; + cfg.pin_busy = -1; + cfg.panel_width = 320; // Native width resolution + cfg.panel_height = 480; // Native hight resolution + cfg.offset_x = 0; // No panel offset: full framebuffer mapping + cfg.offset_y = 0; // No panel offset: full framebuffer mapping + cfg.offset_rotation = 1; // Rotate display content clock wise 90 deg ILI9488 + cfg.dummy_read_pixel = 8; + cfg.dummy_read_bits = 1; + cfg.memory_width = 320; + cfg.memory_height = 480; + // cfg.pwm_control not available in this LovyanGFX version + cfg.invert = false; + cfg.rgb_order = false; + cfg.dlen_16bit = false; + cfg.bus_shared = true; + _panel_instance.config(cfg); + } + // No dedicated TFT PWM backlight pin configured on this board. + // Keep backlight handling outside LovyanGFX to avoid LEDC init on invalid GPIO. + setPanel(&_panel_instance); + // Match Adafruit GFX cursor semantics: y coordinate is text baseline. + setTextDatum(textdatum_t::baseline_left); + } + #endif // compatibility helpers -------------------------------------------------- using lgfx::LGFX_Device::setFont; diff --git a/lib/obp60task/platformio.ini b/lib/obp60task/platformio.ini index 031e68c..8e0aaab 100644 --- a/lib/obp60task/platformio.ini +++ b/lib/obp60task/platformio.ini @@ -56,8 +56,8 @@ build_flags= # -D HARDWARE_V20 #OBP60 hardware revision V2.0 -D HARDWARE_V21 #OBP60 hardware revision V2.1 -D TFT_DISPLAY #Enable TFT LCD display path (instead of E-Ink) - -D TFT_320x480_ST7796 #TFT panel type: ST7796 (320x480) -# -D TFT_320x480_ILI9488 #TFT panel type: ILI9488 (320x480), use instead of ST7796 +# -D TFT_320x480_ST7796 #TFT panel type: ST7796 (320x480, 80 MHz), best performance + -D TFT_320x480_ILI9488 #TFT panel type: ILI9488 (320x480, 40 MHz), lower performance -D OBP_TFT_ENABLE_SCALING=1 #TFT scaling on/off (1=scale to max Y=320, 0=no scaling) -D OBP_TFT_SCALE_ANTIALIAS=1 #Antialiasing for TFT scaling (1=on, 0=off) # -D ENABLE_PATCHES #enable patching of gateway code From 7be72a8cc4fc662a97f93cd4647ce05d65df2873 Mon Sep 17 00:00:00 2001 From: Norbert Walter Date: Thu, 19 Mar 2026 17:08:23 +0000 Subject: [PATCH 70/75] Fix for I2C initialisation (missing pin definitions) --- 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 8a7b879..e9c5227 100644 --- a/lib/obp60task/OBP60Extensions.cpp +++ b/lib/obp60task/OBP60Extensions.cpp @@ -141,7 +141,7 @@ void hardwareInit(GwApi *api) GwLog *logger = api->getLogger(); GwConfigHandler *config = api->getConfig(); - Wire.begin(); + Wire.begin(OBP_I2C_SDA, OBP_I2C_SCL); // Init PCF8574 digital outputs Wire.setClock(I2C_SPEED_LOW); // Set I2C clock on 10 kHz if(pcf8574_Modul1.begin()){ // Initialize PCF8574 From e309ec2123912e5679b9b2d7f33e1ec5675208f7 Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Thu, 19 Mar 2026 18:11:45 +0100 Subject: [PATCH 71/75] Modify comments --- lib/obp60task/OBP60Hardware.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/obp60task/OBP60Hardware.h b/lib/obp60task/OBP60Hardware.h index 0884b70..6476078 100644 --- a/lib/obp60task/OBP60Hardware.h +++ b/lib/obp60task/OBP60Hardware.h @@ -41,8 +41,8 @@ #define OBP_SPI_BUSY 42 #define OBP_SPI_CLK 38 #define OBP_SPI_DIN 48 - #define OBP_TFT_OFFSET_X 10 // ST7796 operating x-offset for centered 400x300 content - #define OBP_TFT_OFFSET_Y -20 // ST7796 operating y-offset for centered 400x300 content + #define OBP_TFT_OFFSET_X 10 // ST7796, ILI9488 operating x-offset for centered 400x300 content + #define OBP_TFT_OFFSET_Y -20 // ST7796, ILI9488 operating y-offset for centered 400x300 content #define TFT_BLACK 0x0109 // Replacement color for black on TFT (RGB565) #define TFT_WHITE 0xFFFF // Replacement color for white on TFT (RGB565) #define SHOW_TIME 6000 // Show time in [ms] for logo and WiFi QR code From 08153cc5852022f32415d7ef1f440316b3a4ad5b Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 20 Mar 2026 10:41:42 +0100 Subject: [PATCH 72/75] Add new board obp70 --- boards/obp70_s3_n16r8.json | 56 +++++++++++++++++++++++++ variants/obp70s3/pins_arduino.h | 74 +++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 boards/obp70_s3_n16r8.json create mode 100644 variants/obp70s3/pins_arduino.h diff --git a/boards/obp70_s3_n16r8.json b/boards/obp70_s3_n16r8.json new file mode 100644 index 0000000..779b5d4 --- /dev/null +++ b/boards/obp70_s3_n16r8.json @@ -0,0 +1,56 @@ +{ + "build": { + "arduino":{ + "ldscript": "esp32s3_out.ld", + "partitions": "default_16MB.csv", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_ESP32S3_DEV", + "-DARDUINO_USB_MODE=1", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [ + [ + "0x303A", + "0x1001" + ] + ], + "mcu": "esp32s3", + "variant": "obp70s3" + }, + "connectivity": [ + "bluetooth", + "wifi" + ], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": [ + "esp-builtin" + ], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "OBP70 ESP32-S3-N16R8 16 MB QD, 8 MB PSRAM)", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 460800 + }, + "url": "https://open-boat-projects.org/en/diy-multifunktionsdisplay-obp-60/", + "vendor": "Open Boat Projects" +} diff --git a/variants/obp70s3/pins_arduino.h b/variants/obp70s3/pins_arduino.h new file mode 100644 index 0000000..a25aa53 --- /dev/null +++ b/variants/obp70s3/pins_arduino.h @@ -0,0 +1,74 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include +#include "soc/soc_caps.h" + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +/* +#define EXTERNAL_NUM_INTERRUPTS 46 +#define NUM_DIGITAL_PINS 48 +#define NUM_ANALOG_INPUTS 20 + +// Multi Function Display OBP60 V2.0 +static const uint8_t LED_BUILTIN = SOC_GPIO_PIN_COUNT+48; +#define BUILTIN_LED LED_BUILTIN // backward compatibility +#define LED_BUILTIN LED_BUILTIN +#define RGB_BUILTIN LED_BUILTIN +#define RGB_BRIGHTNESS 64 + +#define analogInputToDigitalPin(p) (((p)<20)?(analogChannelToDigitalPin(p)):-1) +#define digitalPinToInterrupt(p) (((p)<48)?(p):-1) +#define digitalPinHasPWM(p) (p < 46) +*/ + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 47; +static const uint8_t SCL = 21; + +static const uint8_t SS = 39; +static const uint8_t MOSI = 48; +static const uint8_t MISO = 11; +static const uint8_t SCK = 38; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 7; +static const uint8_t A7 = 8; +static const uint8_t A8 = 9; +static const uint8_t A9 = 10; +static const uint8_t A10 = 11; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; +static const uint8_t T7 = 7; +static const uint8_t T8 = 8; +static const uint8_t T9 = 9; +static const uint8_t T10 = 10; +static const uint8_t T11 = 11; +static const uint8_t T12 = 12; +static const uint8_t T13 = 13; +static const uint8_t T14 = 14; + +#endif /* Pins_Arduino_h */ From f7b1b5671560216de28931cd92855c306dd5dd2d Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 20 Mar 2026 11:08:02 +0100 Subject: [PATCH 73/75] Change defines --- lib/obp60task/OBP60Hardware.h | 2 +- lib/obp60task/OBP60Keypad.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/obp60task/OBP60Hardware.h b/lib/obp60task/OBP60Hardware.h index 6476078..c420348 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 -#if defined HARDWARE_V20 || HARDWARE_V21 +#if defined BOARD_OBP60S3 || defined BOARD_OBP70S3 // Direction pin for RS485 NMEA0183 #define OBP_DIRECTION_PIN 18 // I2C diff --git a/lib/obp60task/OBP60Keypad.h b/lib/obp60task/OBP60Keypad.h index d669ccb..2a557d7 100644 --- a/lib/obp60task/OBP60Keypad.h +++ b/lib/obp60task/OBP60Keypad.h @@ -58,7 +58,7 @@ void initKeys(CommonData &commonData) { commonData.keydata[5].h = height; } - #if defined HARDWARE_V20 || HARDWARE_V21 + #ifdef BOARD_OBP60S3 // Keypad functions for original OBP60 hardware int readKeypad(GwLog* logger, uint thSensitivity, bool use_syspage) { From ba851d7e1bfab3fc8cfa6bb7bd74e68230b6387e Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 20 Mar 2026 11:43:35 +0100 Subject: [PATCH 74/75] Change defines --- lib/obp60task/OBP60Extensions.cpp | 8 ++++---- lib/obp60task/Pagedata.h | 2 +- lib/obp60task/obp60task.cpp | 7 ++++--- lib/obp60task/obp60task.h | 10 +++++----- lib/obp60task/platformio.ini | 6 +----- 5 files changed, 15 insertions(+), 18 deletions(-) diff --git a/lib/obp60task/OBP60Extensions.cpp b/lib/obp60task/OBP60Extensions.cpp index e9c5227..ccbf343 100644 --- a/lib/obp60task/OBP60Extensions.cpp +++ b/lib/obp60task/OBP60Extensions.cpp @@ -230,7 +230,7 @@ void hardwareInit(GwApi *api) void powerInit(String powermode) { // Max Power | Only 5.0V | Min Power if (powermode == "Max Power" || powermode == "Only 5.0V") { -#ifdef HARDWARE_V21 +#ifdef BOARD_OBP60S3 setPortPin(OBP_POWER_50, true); // Power on 5.0V rail #endif #ifdef BOARD_OBP40S3 @@ -238,7 +238,7 @@ void powerInit(String powermode) { setPortPin(OBP_POWER_SD, true); // Power on SD card #endif } else { // Min Power -#ifdef HARDWARE_V21 +#ifdef BOARD_OBP60S3 setPortPin(OBP_POWER_50, false); // Power off 5.0V rail #endif #ifdef BOARD_OBP40S3 @@ -666,7 +666,7 @@ void displayHeader(CommonData &commonData, GwApi::BoatValue *date, GwApi::BoatVa usbRxOld = commonData.status.usbRx; usbTxOld = commonData.status.usbTx; -#ifdef HARDWARE_V21 +#ifdef BOARD_OBP60S3 // Display key lock status if (commonData.keylock) { getdisplay().drawXBitmap(170, 1, lock_bits, icon_width, icon_height, commonData.fgcolor); @@ -759,7 +759,7 @@ void displayFooter(CommonData &commonData) { getdisplay().setFont(&Atari16px); getdisplay().setTextColor(commonData.fgcolor); -#ifdef HARDWARE_V21 +#ifdef BOARD_OBP60S3 // Frame around key icon area if (! commonData.keylock) { // horizontal elements diff --git a/lib/obp60task/Pagedata.h b/lib/obp60task/Pagedata.h index 1cef664..0fcb304 100644 --- a/lib/obp60task/Pagedata.h +++ b/lib/obp60task/Pagedata.h @@ -126,7 +126,7 @@ class Page{ virtual void displayNew(PageData &pageData){} virtual void leavePage(PageData &pageData){} virtual void setupKeys() { -#ifdef HARDWARE_V21 +#ifdef BOARD_OBP60S3 commonData->keydata[0].label = ""; commonData->keydata[1].label = ""; commonData->keydata[2].label = "#LEFT"; diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index 2c76b33..e0ef72f 100644 --- a/lib/obp60task/obp60task.cpp +++ b/lib/obp60task/obp60task.cpp @@ -323,7 +323,7 @@ inline bool underVoltageDetection(float voffset, float vslope) { // Read supply voltage #if defined VOLTAGE_SENSOR && defined LIPO_ACCU_1200 float actVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.53) * 2; // Vin = 1/2 for OBP40 - float minVoltage = 3.65; // Absolut minimum volatge for 3,7V LiPo accu + float minVoltage = 3.65; // Absolut minimum voltage for 3,7V LiPo accu #else float actVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.17) * 20; // Vin = 1/20 for OBP60 float minVoltage = MIN_VOLTAGE; @@ -332,6 +332,7 @@ inline bool underVoltageDetection(float voffset, float vslope) { return (calVoltage < minVoltage); } + // OBP60 Task //#################################################################################### void OBP60Task(GwApi *api){ @@ -339,7 +340,7 @@ void OBP60Task(GwApi *api){ // return; GwLog *logger=api->getLogger(); GwConfigHandler *config=api->getConfig(); -#if defined HARDWARE_V20 || HARDWARE_V21 +#ifdef BOARD_OBP60S3 startLedTask(api); #endif PageList allPages; @@ -348,7 +349,7 @@ void OBP60Task(GwApi *api){ commonData.logger=logger; commonData.config=config; -#if defined HARDWARE_V20 || HARDWARE_V21 +#ifdef BOARD_OBP60S3 // Keyboard coordinates for page footer initKeys(commonData); #endif diff --git a/lib/obp60task/obp60task.h b/lib/obp60task/obp60task.h index af12a23..6091141 100644 --- a/lib/obp60task/obp60task.h +++ b/lib/obp60task/obp60task.h @@ -3,7 +3,7 @@ //we only compile for some boards #if defined BOARD_OBP60S3 || defined BOARD_OBP40S3 #define USBSerial Serial - #ifdef HARDWARE_V21 + #ifdef BOARD_OBP60S3 // CAN NMEA2000 #define ESP32_CAN_TX_PIN 46 #define ESP32_CAN_RX_PIN 3 @@ -35,19 +35,19 @@ // OBP60 Task void OBP60Task(GwApi *param); DECLARE_USERTASK_PARAM(OBP60Task, 35000); // Need 35k RAM as stack size - #if defined(BOARD_OBP60S3) && defined(HARDWARE_V21) && defined(TFT_DISPLAY) + #if defined(BOARD_OBP60S3) && defined(TFT_DISPLAY) DECLARE_CAPABILITY(obp70,true); #endif - #if defined(BOARD_OBP60S3) && defined(HARDWARE_V21) && !defined(TFT_DISPLAY) + #if defined(BOARD_OBP60S3) && !defined(TFT_DISPLAY) DECLARE_CAPABILITY(obp60,true); #endif #ifdef BOARD_OBP40S3 DECLARE_CAPABILITY(obp40,true) #endif - #if defined(BOARD_OBP60S3) && defined(HARDWARE_V21) && defined(TFT_DISPLAY) + #if defined(BOARD_OBP60S3) && defined(TFT_DISPLAY) DECLARE_STRING_CAPABILITY(HELP_URL, "https://obp60-v2-docu.readthedocs.io/en/latest/"); // Link to help pages #endif - #if defined(BOARD_OBP60S3) && defined(HARDWARE_V21) && !defined(TFT_DISPLAY) + #if defined(BOARD_OBP60S3) && !defined(TFT_DISPLAY) DECLARE_STRING_CAPABILITY(HELP_URL, "https://obp60-v2-docu.readthedocs.io/en/latest/"); // Link to help pages #endif #ifdef BOARD_OBP40S3 diff --git a/lib/obp60task/platformio.ini b/lib/obp60task/platformio.ini index 8e0aaab..e7e5472 100644 --- a/lib/obp60task/platformio.ini +++ b/lib/obp60task/platformio.ini @@ -11,11 +11,7 @@ default_envs = [env:obp70_s3] platform = espressif32@6.8.1 board_build.variants_dir = variants -#board = obp60_s3_n8 #ESP32-S3 N8, 8MB flash, no PSRAM -#board = obp60_s3_n16 #ESP32-S3 N16,16MB flash, no PSRAM, zero series -#board = obp60_s3_n8r8 #ESP32-S3 N8R8, 8MB flash, 8MB PSRAM -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 = obp70_s3_n16r8 #ESP32-S3 N16R8, 16MB flash, 8MB PSRAM, production series board_build.partitions = default_16MB.csv #ESP32-S3 N16, 16MB flash custom_config = lib/obp60task/config_obp70.json custom_script = lib/obp60task/extra_task.py From 0dfb1f8535fd135c921d2cd92127bd348eb0dedf Mon Sep 17 00:00:00 2001 From: norbert-walter Date: Fri, 20 Mar 2026 11:45:58 +0100 Subject: [PATCH 75/75] Code cleaning --- lib/obp60task/platformio.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/obp60task/platformio.ini b/lib/obp60task/platformio.ini index e7e5472..c149f68 100644 --- a/lib/obp60task/platformio.ini +++ b/lib/obp60task/platformio.ini @@ -53,7 +53,7 @@ build_flags= -D HARDWARE_V21 #OBP60 hardware revision V2.1 -D TFT_DISPLAY #Enable TFT LCD display path (instead of E-Ink) # -D TFT_320x480_ST7796 #TFT panel type: ST7796 (320x480, 80 MHz), best performance - -D TFT_320x480_ILI9488 #TFT panel type: ILI9488 (320x480, 40 MHz), lower performance + -D TFT_320x480_ILI9488 #TFT panel type: ILI9488 (320x480, 40 MHz), lower performance -D OBP_TFT_ENABLE_SCALING=1 #TFT scaling on/off (1=scale to max Y=320, 0=no scaling) -D OBP_TFT_SCALE_ANTIALIAS=1 #Antialiasing for TFT scaling (1=on, 0=off) # -D ENABLE_PATCHES #enable patching of gateway code @@ -111,7 +111,7 @@ build_flags= # -D HARDWARE_V20 #OBP60 hardware revision V2.0 -D HARDWARE_V21 #OBP60 hardware revision V2.1 # -D DISPLAY_GDEW042T2 #old E-Ink display from GoodDisplay (Waveshare), R10 0.47 ohm - very good - -D DISPLAY_GDEY042T81 #new E-Ink display from GoodDisplay (Waveshare), R10 2.2 ohm - good (contast lost by shunshine) + -D DISPLAY_GDEY042T81 #new E-Ink display from GoodDisplay (Waveshare), R10 2.2 ohm - good (contast lost by shunshine) # -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