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/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/Changes_to_original.txt b/lib/obp60task/Changes_to_original.txt new file mode 100644 index 0000000..1b59dab --- /dev/null +++ b/lib/obp60task/Changes_to_original.txt @@ -0,0 +1,14 @@ +Changes to original project (wellenvogel) + +* esp32-nmea2000-obp60/gwwifi/GwWifi.cpp + - 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 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 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 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/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); diff --git a/lib/obp60task/NetworkClient.cpp b/lib/obp60task/NetworkClient.cpp index c6a7962..d4a1605 100644 --- a/lib/obp60task/NetworkClient.cpp +++ b/lib/obp60task/NetworkClient.cpp @@ -7,13 +7,134 @@ 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) + _valid(false), + _jsonRaw(nullptr), + _jsonRawLen(0), + _imageWidth(0), + _imageHeight(0), + _numberPixels(0), + _pictureBase64(nullptr), + _pictureBase64Len(0) { } +NetworkClient::~NetworkClient() { + if (_jsonRaw != nullptr) { + free(_jsonRaw); + _jsonRaw = nullptr; + _jsonRawLen = 0; + } +} + +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; @@ -51,14 +172,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 +194,50 @@ 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,55 +246,251 @@ 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: 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 - if (avail == 0) { - if (millis() - lastData > READ_TIMEOUT) { - Serial.println("TIMEOUT waiting for data!"); - break; - } - delay(1); - continue; - } - - if (len + avail > capacity) - avail = capacity - len; - - int read = stream->readBytes(buffer + len, avail); - len += read; - lastData = millis(); - - if (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) continue; - - 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 = testLen; - complete = true; - break; - } - - free(test); + // 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; } - // --- Added: Force-close connection in all cases to avoid stuck TCP sockets --- - if (stream) stream->stop(); + // 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 && !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."); + } + + // --- Plain-body handling (recommended): read full body into outData as-is --- + // NEW: try to read Content-Length bytes if available (more robust) + 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(); + } + + // NEW: stop reading as soon as we have the full response + 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; + } + + 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 + 1); + 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); + outData[len] = 0; + outLen = len; + + http.end(); + free(buffer); + return true; + } + + // --- GZIP path (only if Content-Encoding is gzip) --- + if (!aborting) { + + // NEW: read exactly Content-Length bytes when available (prevents partial-body timeout loops) + while ((http.connected() || (stream && stream->available())) && !complete && !aborting) { + + 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) { + 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; + } + } + + // 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; + } else { + int headerOffset = skipGzipHeader(buffer, len); + if (headerOffset < 0) { + aborting = true; + } else { + 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; + } + + 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 { + uint8_t* test = (uint8_t*)malloc((size_t)outNeeded + 1); + if (!test) { + Serial.println("Malloc failed test buffer, aborting."); + aborting = true; + } else { + unsigned long srcLen = (unsigned long)deflateLen; + unsigned long testLen = outNeeded; + int res = puff(test, &testLen, buffer + headerOffset, &srcLen); + + if (res == 0) { + 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); + } + free(test); + aborting = true; + } + } + } + } + } + } + } + + // --- 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); @@ -158,6 +507,18 @@ bool NetworkClient::httpGetGzip(const String& url, uint8_t*& outData, size_t& ou 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); + _jsonRaw = nullptr; + _jsonRawLen = 0; + } uint8_t* raw = nullptr; size_t rawLen = 0; @@ -167,15 +528,38 @@ bool NetworkClient::fetchAndDecompressJson(const String& url) { return false; } - DeserializationError err = deserializeJson(_doc, raw, rawLen); - free(raw); + 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 (DEBUGING) {Serial.println("JSON OK!");} + 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; + } + + _jsonRaw = raw; + _jsonRawLen = rawLen; + + 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; } @@ -184,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 03e7f83..a04bf1f 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 @@ -12,16 +12,31 @@ class NetworkClient { public: NetworkClient(size_t reserveSize = 0); + ~NetworkClient(); 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: DynamicJsonDocument _doc; 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/OBP60Extensions.cpp b/lib/obp60task/OBP60Extensions.cpp index 3b3ee88..ccbf343 100644 --- a/lib/obp60task/OBP60Extensions.cpp +++ b/lib/obp60task/OBP60Extensions.cpp @@ -56,6 +56,61 @@ GxEPD2_BW display(GxEPD2_4 GxEPD2_BW & getdisplay(){return display;} #endif +#ifdef TFT_DISPLAY +// 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; + + 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; +} + +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 PCF8574 pcf8574_Modul1(PCF8574_I2C_ADDR1); // First digital IO modul PCF8574 from Horter @@ -86,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 @@ -175,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 @@ -183,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 @@ -251,8 +306,12 @@ 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 + #ifdef TFT_DISPLAY + getpaneldisplay().powerSave(true); // Display power save + #else 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 +335,12 @@ 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 + #ifdef TFT_DISPLAY + getpaneldisplay().powerSave(true); // Display power save + #else 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,8 +542,10 @@ 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; - 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); } @@ -488,19 +553,20 @@ void drawTextCenter(int16_t cx, int16_t cy, String text) { void drawButtonCenter(int16_t cx, int16_t cy, int8_t sx, int8_t sy, String text, uint16_t fg, uint16_t bg, bool inverted) { int16_t x1, y1; uint16_t w, h; - uint16_t color; - - getdisplay().getTextBounds(text, cx, cy, &x1, &y1, &w, &h); // Find text center - getdisplay().setCursor(cx - w/2, cy + h/2); // Set cursor to center + 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(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(cursorX, cursorY); // Set cursor to center getdisplay().print(text); // Draw text } } @@ -509,7 +575,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 TFT_DISPLAY + 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); } @@ -595,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); @@ -688,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 @@ -1000,7 +1071,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 TFT_DISPLAY + // 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 +1097,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 TFT_DISPLAY + 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..9e2bc99 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -5,14 +5,28 @@ #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 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 + #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 @@ -74,11 +88,621 @@ GxEPD2_BW & getdisplay(); GxEPD2_BW & getdisplay(); #endif +#ifdef TFT_DISPLAY +// LovyanGFX based display wrapper for TFT panels +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; + cfg.freq_write = 80000000; // High speed ST7796 + 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 = 3; // Rotate display content conter clock wise 90 deg ST7796 + 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 + + #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; + 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; + } + + 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), + _adfGlyphBridge, + font->first, + 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 */ } + + // Runtime panel offset control for TFT panels + 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; + uint16_t _adfGlyphCount = 0; + const GFXfont* _currentAdfFont = nullptr; + #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 { +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(); +LGFXCanvas & getscaleddisplay(); +bool initDisplayShadowBuffer(); +bool initDisplayScaleBuffer(uint16_t width, uint16_t height); +#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 +#ifdef TFT_DISPLAY +#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; + 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 +#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( + int16_t x, int16_t y, + const uint8_t *bmp, + 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 mirrorX=false) // true: bytes run right-to-left within each row +{ + #ifdef TFT_DISPLAY + // 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) { + // vertical packing: column-major bytes + int col = mirrorX ? (w - 1 - xx) : xx; + byteIdx = col * ((h + 7) / 8) + (yy / 8); + bitIdx = yy % 8; + } else { + // 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; + if (lsbFirst) { + pix = b & (1 << bitIdx); + } else { + pix = b & (1 << (7 - bitIdx)); + } + if (pix) { + getdisplay().drawPixel(x + xx, y + yy, color); + } + } + if ((yy & 0x0F) == 0) { + yield(); + } + } + #else + // E‑Paper: just hand over to driver (expects MSB‑first horizontal) + getdisplay().drawBitmap(x, y, bmp, w, h, color); + #endif +} + + +// 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 TFT_DISPLAY + drawMonochromeBitmap(x, y, bmp, w, h, color); + #else + getdisplay().drawBitmap(x, y, bmp, w, h, color); + #endif +} + +inline void displayFirstPage() { + #ifdef TFT_DISPLAY + initDisplayShadowBuffer(); + #else + getdisplay().firstPage(); + #endif +} + +inline void displayNextPage() { + #ifdef TFT_DISPLAY + if (initDisplayShadowBuffer()) { + 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()); + + #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); + + const uint16_t drawX = static_cast((dstW - targetW) / 2U); + const uint16_t drawY = static_cast((dstH - targetH) / 2U); + + const uint16_t borderColor = src.readPixel(0, 0); + 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) + : 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 + + scaled.drawPixel(drawX + x, drawY + y, color); + } + if ((y & 0x0F) == 0) { + yield(); + } + } + + scaled.pushSprite(0, 0); + } else { + src.pushSprite(drawX, drawY); + } + #endif + } + #else + getdisplay().nextPage(); + #endif +} + +inline void displaySetPartialWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h) { + #ifdef TFT_DISPLAY + // 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 TFT_DISPLAY + // TFT LCD doesn't need setFullWindow() + #else + getdisplay().setFullWindow(); + #endif +} + +// 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 TFT_DISPLAY + getdisplay().getTextBounds(txt, x, y, x0, y0, w, h); +#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/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; diff --git a/lib/obp60task/OBP60Hardware.h b/lib/obp60task/OBP60Hardware.h index 6d038d3..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 @@ -34,13 +34,17 @@ #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 #define OBP_SPI_BUSY 42 #define OBP_SPI_CLK 38 #define OBP_SPI_DIN 48 + #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 #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/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) { 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 c393b7f..071007a 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 TFT_DISPLAY + 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..046886c 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" @@ -12,6 +13,54 @@ NetworkClient net(JSON_BUFFER); // Define network client ImageDecoder decoder; // Define image decoder +#ifdef TFT_DISPLAY +// Set to true to render a generated RGB565 color-bar test image. +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) { + 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 @@ -24,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 TFT_DISPLAY + imageBackupCapacity *= 2U; + #endif + imageBackupData = (uint8_t*)heap_caps_malloc(imageBackupCapacity, MALLOC_CAP_SPIRAM); } // Set botton labels @@ -295,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; @@ -347,22 +414,33 @@ 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) + #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 + #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 - "&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 + #ifdef TFT_DISPLAY + "&itype=1" + // Image type: 1=Color + #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 + "&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 @@ -371,19 +449,45 @@ 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); + // 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)) { - 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 + // NEW: reset backoff on success + failCount = 0; + nextAllowedMs = now + 1000; // keep 1 Hz on success - const char* b64src = json["picture_base64"].as(); // Read picture as Base64 content - size_t b64len = strlen(b64src); // Calculate length of Base64 content + 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){ + requiredBytesMono = (size_t)((imgWidth + 7) / 8) * (size_t)imgHeight; + requiredBytesRgb565 = (size_t)imgWidth * (size_t)imgHeight * 2U; + } + if (requiredBytesMono == 0){ + LOG_DEBUG(GwLog::ERROR,"Error PageNavigation: invalid image geometry w=%d h=%d",imgWidth,imgHeight); + return PAGE_UPDATE; + } + + 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 = 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) { @@ -393,32 +497,81 @@ 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; // Calculate image size + size_t imgSize = (numPix > 0) ? (size_t)numPix : requiredBytesMono; // Calculate image size + if (imgSize < requiredBytesMono){ + imgSize = requiredBytesMono; + } + #ifdef TFT_DISPLAY + 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: PPSRAM alloc image buffer failed"); + LOG_DEBUG(GwLog::ERROR,"Error PageNavigation: PSRAM alloc image buffer failed"); free(b64); return PAGE_UPDATE; } // Decode Base64 content to image size_t decodedSize = 0; - decoder.decodeBase64(b64, imageData, imgSize, decodedSize); + bool decodeOk = decoder.decodeBase64(b64, b64len, imageData, imgSize, decodedSize); + if (!decodeOk || decodedSize < requiredBytesMono){ + 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, b64ret=%d)", + decodeOk ? 1 : 0, + (unsigned int)decodedSize, + (unsigned int)requiredBytesMono, + base64Ret + ); + free(b64); + free(imageData); + return PAGE_UPDATE; + } - // Copy actual navigation man to ackup map + bool imageIsRgb565 = false; + #ifdef TFT_DISPLAY + imageIsRgb565 = (decodedSize >= requiredBytesRgb565); + #endif + + #ifdef TFT_DISPLAY + if (kShowRgb565StripeTestImage) { + createRgb565StripeImage(reinterpret_cast(imageData), imgWidth, imgHeight); + decodedSize = requiredBytesRgb565; + imageIsRgb565 = true; + } + #endif + + // Copy actual navigation map to backup map imageBackupWidth = imgWidth; imageBackupHeight = imgHeight; imageBackupSize = imgSize; - if (decodedSize > 0) { - memcpy(imageBackupData, imageData, decodedSize); - imageBackupSize = decodedSize; + if (decodedSize > 0 && imageBackupData != nullptr) { + size_t copySize = (decodedSize > imageBackupCapacity) ? imageBackupCapacity : decodedSize; + memcpy(imageBackupData, imageData, copySize); + imageBackupSize = copySize; } - hasImageBackup = true; + imageBackupIsRgb565 = imageIsRgb565; + hasImageBackup = (imageBackupData != nullptr); lostCounter = 0; // Show image (navigation map) - getdisplay().drawBitmap(0, 25, imageData, imgWidth, imgHeight, commonData->fgcolor); + #ifdef TFT_DISPLAY + 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); @@ -426,12 +579,33 @@ 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); + #ifdef TFT_DISPLAY + 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 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 +618,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/lib/obp60task/PageOneValue.cpp b/lib/obp60task/PageOneValue.cpp index fd9509a..f41ea09 100644 --- a/lib/obp60task/PageOneValue.cpp +++ b/lib/obp60task/PageOneValue.cpp @@ -274,7 +274,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 92af7b2..45c0b49 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" @@ -37,9 +38,11 @@ private: String buzzer_mode; uint8_t buzzer_power; String cpuspeed; + String powermode; String rtc_module; String gps_module; String env_module; + String flashLED; String batt_sensor; String solar_sensor; @@ -48,15 +51,445 @@ 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 { + uint64_t NAME; + uint8_t id; + char hex_name[17]; + uint16_t manuf_code; + const char *model; + }; + 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); @@ -67,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); @@ -76,6 +510,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() { @@ -92,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; } @@ -129,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 @@ -168,309 +596,68 @@ public: } } - 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"){ + void displayNew(PageData &pageData) { +#ifdef BOARD_OBP60S3 + // Clear optical warning + if (flashLED == "Limit Violation") { setBlinkingLED(false); - setFlashLED(false); + setFlashLED(false); } +#endif - // Logging boat values - logger->logDebug(GwLog::LOG, "Drawing at PageSystem, Mode=%c", mode); +#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 + }; - // Draw page - //*********************************************************** + int displayPage(PageData &pageData){ - 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 + displaySetPartialWindow(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, 80); - getdisplay().print("RxD: "); - getdisplay().print(String(commonData->status.n2kRx)); - getdisplay().setCursor(20, 100); - getdisplay().print("TxD: "); - getdisplay().print(String(commonData->status.n2kTx)); + // 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 - 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 eaf25d3..82e9d32 100644 --- a/lib/obp60task/PageTwoValues.cpp +++ b/lib/obp60task/PageTwoValues.cpp @@ -281,7 +281,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 9e6e879..48b8d78 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/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/code_size.ps1 b/lib/obp60task/code_size.ps1 new file mode 100644 index 0000000..3c1a0c7 --- /dev/null +++ b/lib/obp60task/code_size.ps1 @@ -0,0 +1,16 @@ +param( + [string]$dir = "." +) + +$total = 0 + +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 "Over all files: $total" \ No newline at end of file diff --git a/lib/obp60task/config_obp70.json b/lib/obp60task/config_obp70.json new file mode 100644 index 0000000..b9b7a99 --- /dev/null +++ b/lib/obp60task/config_obp70.json @@ -0,0 +1,4144 @@ +[ + { + "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", + "C-Map", + "Garmin Fish", + "Garmin Nav" + ], + "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/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 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/obp60task.cpp b/lib/obp60task/obp60task.cpp index ec95b6d..feba029 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 @@ -284,8 +283,12 @@ 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 + #ifdef TFT_DISPLAY + getpaneldisplay().powerSave(true); // Display power save + #else 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 +298,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 +307,12 @@ 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 + #ifdef TFT_DISPLAY + getpaneldisplay().powerSave(true); // Display power save + #else getdisplay().powerOff(); // Display power off + #endif #endif while (true) { esp_deep_sleep_start(); // Deep Sleep without wakeup. Wakeup only after power cycle (restart). @@ -316,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; @@ -325,6 +332,7 @@ inline bool underVoltageDetection(float voffset, float vslope) { return (calVoltage < minVoltage); } + // OBP60 Task //#################################################################################### void OBP60Task(GwApi *api){ @@ -332,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; @@ -341,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 @@ -375,36 +383,47 @@ 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(TFT_DISPLAY) + getpaneldisplay().init(); // Init for TFT LCD panel #else getdisplay().init(115200); // Init for normal displays #endif + #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) + getpaneldisplay().setPanelOffset(OBP_TFT_OFFSET_X, OBP_TFT_OFFSET_Y); // Restore configured operating panel offset + #else getdisplay().setRotation(0); // Set display orientation (horizontal) - getdisplay().setFullWindow(); // Set full Refresh - getdisplay().firstPage(); // set first page + #endif + 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 + // 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 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 @@ -432,7 +451,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.) @@ -723,25 +742,29 @@ void OBP60Task(GwApi *api){ if(millis() > starttime4 + 8000 && delayedDisplayUpdate == true){ starttime1 = millis(); starttime2 = millis(); - getdisplay().setFullWindow(); // Set full update + displaySetFullWindow(); // Set full update + #ifdef TFT_DISPLAY + // 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 #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 #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; } @@ -751,31 +774,38 @@ 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 TFT_DISPLAY + // 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 #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 #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 TFT_DISPLAY + // TFT LCD: no special refresh + #else getdisplay().setFullWindow(); // Set full update if(fastrefresh == "true"){ getdisplay().nextPage(); // Full update @@ -783,7 +813,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 @@ -795,6 +825,7 @@ void OBP60Task(GwApi *api){ // getdisplay().nextPage(); // Partial update // getdisplay().nextPage(); // Partial update } + #endif } // Refresh display data, default all 1s @@ -807,6 +838,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,16 +873,16 @@ 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){ + 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 @@ -870,10 +902,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 TFT_DISPLAY getdisplay().hibernate(); + #endif } } diff --git a/lib/obp60task/obp60task.h b/lib/obp60task/obp60task.h index b4e1400..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,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(TFT_DISPLAY) + DECLARE_CAPABILITY(obp70,true); + #endif + #if defined(BOARD_OBP60S3) && !defined(TFT_DISPLAY) DECLARE_CAPABILITY(obp60,true); #endif #ifdef BOARD_OBP40S3 DECLARE_CAPABILITY(obp40,true) #endif - #ifdef BOARD_OBP60S3 + #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(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/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(); diff --git a/lib/obp60task/platformio.ini b/lib/obp60task/platformio.ini index 03a5463..c149f68 100644 --- a/lib/obp60task/platformio.ini +++ b/lib/obp60task/platformio.ini @@ -4,9 +4,66 @@ #by uncommenting the next line default_envs = + obp70_s3 obp60_s3 obp40_s3 +[env:obp70_s3] +platform = espressif32@6.8.1 +board_build.variants_dir = variants +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 +framework = arduino +lib_deps = + ${basedeps.lib_deps} + Wire + SPI + ESP32time + HTTPClient + WiFiClientSecure + esphome/AsyncTCP-esphome@2.1.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 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 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 @@ -26,7 +83,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 @@ -82,7 +139,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 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 new file mode 100755 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 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 +#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 */