From 44cb8d35ce5dedf2c0bfbd8de055b2b599ffbe13 Mon Sep 17 00:00:00 2001
From: Thomas Hooge <thomas@hoogi.de>
Date: Wed, 22 Jan 2025 12:07:00 +0100
Subject: [PATCH 01/17] Improved page refresh possibilities and page white

---
 lib/obp60task/PageWhite.cpp | 12 ++++++++++--
 lib/obp60task/Pagedata.h    |  1 +
 lib/obp60task/obp60task.cpp | 17 +++++++++++++----
 3 files changed, 24 insertions(+), 6 deletions(-)

diff --git a/lib/obp60task/PageWhite.cpp b/lib/obp60task/PageWhite.cpp
index a2b6737..e40a3ac 100644
--- a/lib/obp60task/PageWhite.cpp
+++ b/lib/obp60task/PageWhite.cpp
@@ -13,6 +13,7 @@ public:
     PageWhite(CommonData &common){
         commonData = &common;
         common.logger->logDebug(GwLog::LOG,"Instantiate PageWhite");
+        refreshtime = 15000;
     }
 
     virtual int handleKey(int key) {
@@ -53,7 +54,11 @@ public:
         int bgcolor = GxEPD_WHITE;
 
         // Set display in partial refresh mode
-        getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update
+        if (mode == 'W') {
+            getdisplay().setFullWindow();
+        } else {
+            getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update
+        }
 
         if (mode == 'L') {
             getdisplay().drawBitmap(0, 0, gImage_Logo_OBP_400x300_sw, getdisplay().width(), getdisplay().height(), commonData->fgcolor);
@@ -62,7 +67,10 @@ public:
         }
 
         // Update display
-        getdisplay().nextPage();    // Partial update (fast)
+        getdisplay().nextPage();
+        if (mode == 'W') {
+            getdisplay().hibernate();
+        }
 
     };
 };
diff --git a/lib/obp60task/Pagedata.h b/lib/obp60task/Pagedata.h
index cc4c45b..b385b5c 100644
--- a/lib/obp60task/Pagedata.h
+++ b/lib/obp60task/Pagedata.h
@@ -100,6 +100,7 @@ class Page{
   protected:
     CommonData *commonData;
   public:
+    int refreshtime = 1000;
     virtual void displayPage(PageData &pageData)=0;
     virtual void displayNew(PageData &pageData){}
     virtual void setupKeys() {
diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp
index 662942e..e56aaaf 100644
--- a/lib/obp60task/obp60task.cpp
+++ b/lib/obp60task/obp60task.cpp
@@ -550,8 +550,10 @@ void OBP60Task(GwApi *api){
     //####################################################################################
 
     bool systemPage = false;
+    Page *currentPage;
     while (true){
         delay(100);     // Delay 100ms (loop time)
+        bool keypressed = false;
 
         // Undervoltage detection
         if(uvoltage == true){
@@ -593,8 +595,8 @@ void OBP60Task(GwApi *api){
             int keyboardMessage=0;
             while (xQueueReceive(allParameters.queue,&keyboardMessage,0)){
                 LOG_DEBUG(GwLog::LOG,"new key from keyboard %d",keyboardMessage);
+                keypressed = true;
 
-                Page *currentPage;
                 if (keyboardMessage == 12) {
                     LOG_DEBUG(GwLog::LOG, "Calling system page");
                     systemPage = true; // System page is out of band
@@ -725,9 +727,17 @@ void OBP60Task(GwApi *api){
                 }
             }
             
-            // Refresh display data all 1s
-            if(millis() > starttime3 + 1000){
+            // Refresh display data, default all 1s
+            currentPage = pages[pageNumber].page;
+            int pagetime = 1000;
+            if ((lastPage == pageNumber) and (!keypressed)) {
+                // same page we use page defined time
+                pagetime = currentPage->refreshtime;
+            }
+            if(millis() > starttime3 + pagetime){
+                LOG_DEBUG(GwLog::DEBUG,"Page with refreshtime=%d", pagetime);
                 starttime3 = millis();
+
                 //refresh data from api
                 api->getBoatDataValues(boatValues.numValues,boatValues.allBoatValues);
                 api->getStatus(commonData.status);
@@ -749,7 +759,6 @@ void OBP60Task(GwApi *api){
                     syspage->displayPage(sysparams);
                 }
                 else {
-                    Page *currentPage = pages[pageNumber].page;
                     if (currentPage == NULL){
                         LOG_DEBUG(GwLog::ERROR,"page number %d not found", pageNumber);
                         // Error handling for missing page

From a42d31ff49744d1a7b02a7b8801f907e88bc5d88 Mon Sep 17 00:00:00 2001
From: Thomas Hooge <thomas@hoogi.de>
Date: Wed, 22 Jan 2025 20:14:55 +0100
Subject: [PATCH 02/17] System page inprovements, e.g. soft reset

---
 lib/obp60task/PageSystem.cpp | 32 +++++++++++++++++++++++++-------
 1 file changed, 25 insertions(+), 7 deletions(-)

diff --git a/lib/obp60task/PageSystem.cpp b/lib/obp60task/PageSystem.cpp
index 7beb13d..58d3788 100644
--- a/lib/obp60task/PageSystem.cpp
+++ b/lib/obp60task/PageSystem.cpp
@@ -48,7 +48,7 @@ public:
         commonData->keydata[0].label = "EXIT";
         commonData->keydata[1].label = "MODE";
         commonData->keydata[2].label = "";
-        commonData->keydata[3].label = "";
+        commonData->keydata[3].label = "RST";
         commonData->keydata[4].label = "STBY";
         commonData->keydata[5].label = "ILUM";
     }
@@ -67,9 +67,13 @@ public:
             return 0;
         }
         // grab cursor keys to disable page navigation
-        if (key == 3 or key == 4) {
+        if (key == 3) {
             return 0;
         }
+        // soft reset
+        if (key == 4) {
+            ESP.restart();
+        }
         // Code for keylock
         if (key == 11) {
             commonData->keylock = !commonData->keylock;
@@ -133,11 +137,25 @@ public:
             getdisplay().setCursor(120, y0 + 16);
             getdisplay().print(env_module);
 
+            // 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));
+
             getdisplay().setCursor(2, y0 + 32);
             getdisplay().print("Buzzer:");
             getdisplay().setCursor(120, y0 + 32);
             getdisplay().print(buzzer_mode);
 
+            // 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));
+
             getdisplay().setCursor(2, y0 + 48);
             getdisplay().print("CPU speed:");
             getdisplay().setCursor(120, y0 + 48);
@@ -146,11 +164,6 @@ public:
             int cpu_freq = esp_clk_cpu_freq() / 1000000;
             getdisplay().print(String(cpu_freq));
 
-            getdisplay().setCursor(2, y0 + 64);
-            getdisplay().print("RTC:");
-            getdisplay().setCursor(120, y0 + 64);
-            getdisplay().print(rtc_module);
-
             getdisplay().setCursor(202, y0 + 64);
             getdisplay().print("GPS:");
             getdisplay().setCursor(300, y0 + 64);
@@ -161,6 +174,11 @@ public:
             getdisplay().setCursor(120, y0 + 80);
             getdisplay().print(hasFRAM ? "available" : "not found");
 
+            getdisplay().setCursor(202, y0 + 80);
+            getdisplay().print("RTC:");
+            getdisplay().setCursor(300, y0 + 80);
+            getdisplay().print(rtc_module);
+
             getdisplay().setCursor(2, y0 + 120);
             getdisplay().print("Firmware Version: ");
             getdisplay().print(VERSINFO);

From 1ff0de5d2483863e58c858810d9c51c11aff6499 Mon Sep 17 00:00:00 2001
From: Thomas Hooge <thomas@hoogi.de>
Date: Thu, 23 Jan 2025 19:49:44 +0100
Subject: [PATCH 03/17] Deep sleep for OBP60 and small fix for BMP180

---
 lib/obp60task/OBP60Extensions.cpp | 32 +++++++++++++++++++++++++++++++
 lib/obp60task/OBP60Extensions.h   |  6 ++++++
 lib/obp60task/PageBME280.cpp      |  6 +++---
 lib/obp60task/PageSystem.cpp      |  4 ++++
 lib/obp60task/obp60task.cpp       | 24 +++++++++++++++++++++++
 5 files changed, 69 insertions(+), 3 deletions(-)

diff --git a/lib/obp60task/OBP60Extensions.cpp b/lib/obp60task/OBP60Extensions.cpp
index 8c4ec27..a98cba5 100644
--- a/lib/obp60task/OBP60Extensions.cpp
+++ b/lib/obp60task/OBP60Extensions.cpp
@@ -70,6 +70,9 @@ bool statusBacklightLED = false;// Actual status of flash LED on/off
 
 int uvDuration = 0;             // Under voltage duration in n x 100ms
 
+RTC_DATA_ATTR uint8_t RTC_lastpage; // Remember last page while deep sleeping
+
+
 LedTaskData *ledTaskData=nullptr;
 
 void hardwareInit(GwApi *api)
@@ -118,6 +121,35 @@ void startLedTask(GwApi *api){
     createSpiLedTask(ledTaskData);
 }
 
+uint8_t getLastPage() {
+    return RTC_lastpage;
+}
+
+#ifdef BOARD_OBP60S3
+void deepSleep(CommonData &common){
+    RTC_lastpage = common.data.actpage - 1;
+    // Switch off all power lines
+    setPortPin(OBP_BACKLIGHT_LED, false);   // Backlight Off
+    setFlashLED(false);                     // Flash LED Off
+    buzzer(TONE4, 20);                      // Buzzer tone 4kHz 20ms
+    // Shutdown EInk display
+    getdisplay().setFullWindow();               // Set full Refresh
+    getdisplay().fillScreen(common.bgcolor);    // Clear screen
+    getdisplay().setTextColor(common.fgcolor);
+    getdisplay().setFont(&Ubuntu_Bold20pt7b);
+    getdisplay().setCursor(85, 150);
+    getdisplay().print("Sleep Mode");
+    getdisplay().setFont(&Ubuntu_Bold8pt7b);
+    getdisplay().setCursor(65, 175);
+    getdisplay().print("For wakeup press key and wait 5s");
+    getdisplay().nextPage();                // Update display contents
+    getdisplay().powerOff();                // Display power off
+    setPortPin(OBP_POWER_50, false);        // Power off ePaper display
+    // Stop system
+    esp_deep_sleep_start();             // Deep Sleep with weakup via GPIO pin
+}
+#endif
+
 // Valid colors see hue
 Color colorMapping(const String &colorString){
     Color color = COLOR_RED;
diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h
index fa80cc9..727f912 100644
--- a/lib/obp60task/OBP60Extensions.h
+++ b/lib/obp60task/OBP60Extensions.h
@@ -64,6 +64,12 @@ Point rotatePoint(const Point& origin, const Point& p, double angle);
 std::vector<Point> rotatePoints(const Point& origin, const std::vector<Point>& pts, double angle);
 void fillPoly4(const std::vector<Point>& p4, uint16_t color);
 
+#ifdef BOARD_OBP60S3
+void deepSleep(CommonData &common);
+#endif
+
+uint8_t getLastPage();
+
 void hardwareInit(GwApi *api);
 
 void setPortPin(uint pin, bool value);          // Set port pin for extension port
diff --git a/lib/obp60task/PageBME280.cpp b/lib/obp60task/PageBME280.cpp
index d731005..9bf0df2 100644
--- a/lib/obp60task/PageBME280.cpp
+++ b/lib/obp60task/PageBME280.cpp
@@ -48,7 +48,7 @@ class PageBME280 : public Page
             value1 = 23.0 + float(random(0, 10)) / 10.0;
         }
         // Display data when sensor activated
-        if((String(useenvsensor) == "BME280") or (String(useenvsensor) == "BMP280")){
+        if((useenvsensor == "BME280") or (useenvsensor == "BMP280") or (useenvsensor == "BMP180")){
             svalue1 = String(value1, 1);                // Formatted value as string including unit conversion and switching decimal places
         }
         else{
@@ -66,7 +66,7 @@ class PageBME280 : public Page
             value2 = 43 + float(random(0, 4));
         }
         // Display data when sensor activated
-        if(String(useenvsensor) == "BME280"){
+        if(useenvsensor == "BME280"){
             svalue2 = String(value2, 0);                // Formatted value as string including unit conversion and switching decimal places
         }
         else{
@@ -84,7 +84,7 @@ class PageBME280 : public Page
             value3 = 1006 + float(random(0, 5));
         }
         // Display data when sensor activated
-        if((String(useenvsensor) == "BME280") or (String(useenvsensor) == "BMP280")){
+        if((useenvsensor == "BME280") or (useenvsensor == "BMP280") or (useenvsensor == "BMP180")){
             svalue3 = String(value3 / 100, 1);          // Formatted value as string including unit conversion and switching decimal places
         }
         else{
diff --git a/lib/obp60task/PageSystem.cpp b/lib/obp60task/PageSystem.cpp
index 58d3788..64b08d7 100644
--- a/lib/obp60task/PageSystem.cpp
+++ b/lib/obp60task/PageSystem.cpp
@@ -74,6 +74,10 @@ public:
         if (key == 4) {
             ESP.restart();
         }
+        // standby / deep sleep
+        if (key == 5) {
+           deepSleep(*commonData);
+        }
         // Code for keylock
         if (key == 11) {
             commonData->keylock = !commonData->keylock;
diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp
index 662942e..8678402 100644
--- a/lib/obp60task/obp60task.cpp
+++ b/lib/obp60task/obp60task.cpp
@@ -117,6 +117,16 @@ void OBP60Init(GwApi *api){
     }
     #endif
 
+    #ifdef BOARD_OBP60S3
+    touchSleepWakeUpEnable(TP1, 45);
+    touchSleepWakeUpEnable(TP2, 45);
+    touchSleepWakeUpEnable(TP3, 45);
+    touchSleepWakeUpEnable(TP4, 45);
+    touchSleepWakeUpEnable(TP5, 45);
+    touchSleepWakeUpEnable(TP6, 45);
+    esp_sleep_enable_touchpad_wakeup();
+    #endif
+
     // Get CPU speed
     int freq = getCpuFrequencyMhz();
     api->getLogger()->logDebug(GwLog::LOG,"CPU speed at boot: %i MHz", freq);
@@ -356,6 +366,8 @@ void deepSleep(CommonData &common){
 }
 #endif
 
+
+
 // OBP60 Task
 //####################################################################################
 void OBP60Task(GwApi *api){
@@ -440,6 +452,18 @@ void OBP60Task(GwApi *api){
     PageStruct pages[MAX_PAGE_NUMBER];
     // Set start page
     int pageNumber = int(api->getConfig()->getConfigItem(api->getConfig()->startPage,true)->asInt()) - 1;
+
+#ifdef BOARD_OBP60S3
+    LOG_DEBUG(GwLog::LOG,"Checking wakeup...");
+    if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_TOUCHPAD) {
+        LOG_DEBUG(GwLog::LOG,"Wake up by touch pad %d",esp_sleep_get_touchpad_wakeup_status());
+        pageNumber = getLastPage();
+    } else {
+        LOG_DEBUG(GwLog::LOG,"Other wakeup reason");
+    }
+    LOG_DEBUG(GwLog::LOG,"...done");
+#endif
+
     int lastPage=pageNumber;
 
     BoatValueList boatValues; //all the boat values for the api query

From 78b5861da42169f10724fc6920f85dabc4a93ac5 Mon Sep 17 00:00:00 2001
From: norbert-walter <norbert-walter@web.de>
Date: Thu, 23 Jan 2025 22:55:35 +0100
Subject: [PATCH 04/17] Typo

---
 lib/obp60task/obp60task.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp
index 662942e..b2cbe2e 100644
--- a/lib/obp60task/obp60task.cpp
+++ b/lib/obp60task/obp60task.cpp
@@ -9,7 +9,7 @@
 #include <NMEA0183.h>                   // NMEA0183
 #include <NMEA0183Msg.h>
 #include <NMEA0183Messages.h>
-#include <GxEPD2_BW.h>                  // GxEPD2 lib for black 6 white E-Ink displays
+#include <GxEPD2_BW.h>                  // GxEPD2 lib for b/w E-Ink displays
 #include "OBP60Extensions.h"            // Functions lib for extension board
 #include "OBP60Keypad.h"                // Functions for keypad
 

From 46af8916e7ebd2e1a1b3ac4fb465ec70338d990c Mon Sep 17 00:00:00 2001
From: Thomas Hooge <thomas@hoogi.de>
Date: Fri, 24 Jan 2025 08:06:26 +0100
Subject: [PATCH 05/17] Fix for OBP40 in page system and deepSleep

---
 lib/obp60task/PageSystem.cpp | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/lib/obp60task/PageSystem.cpp b/lib/obp60task/PageSystem.cpp
index 64b08d7..e56d94b 100644
--- a/lib/obp60task/PageSystem.cpp
+++ b/lib/obp60task/PageSystem.cpp
@@ -74,10 +74,12 @@ public:
         if (key == 4) {
             ESP.restart();
         }
+#ifdef BOARD_OBP60S3
         // standby / deep sleep
         if (key == 5) {
            deepSleep(*commonData);
         }
+#endif
         // Code for keylock
         if (key == 11) {
             commonData->keylock = !commonData->keylock;

From 28e4fc0643336b33df3c9ef1c060877663c6ecc7 Mon Sep 17 00:00:00 2001
From: free-x <oroitburd@gmail.com>
Date: Fri, 24 Jan 2025 12:22:26 +0100
Subject: [PATCH 06/17] extend CI for OBP boards

---
 lib/obp60task/platformio.ini | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/lib/obp60task/platformio.ini b/lib/obp60task/platformio.ini
index da78f39..16f3269 100644
--- a/lib/obp60task/platformio.ini
+++ b/lib/obp60task/platformio.ini
@@ -2,7 +2,9 @@
 #if you want a pio run to only build
 #your special environments you can set this here
 #by uncommenting the next line
-default_envs = obp60_s3
+default_envs = 
+    obp60_s3
+    obp40_s3
 
 [env:obp60_s3]
 platform = espressif32@6.8.1

From 1174622b4a95285fff8f425480752287c38f5332 Mon Sep 17 00:00:00 2001
From: Thomas Hooge <thomas@hoogi.de>
Date: Fri, 24 Jan 2025 12:42:07 +0100
Subject: [PATCH 07/17] System page for OBP40 and deep sleep improvements

---
 lib/obp60task/OBP60Extensions.cpp |  26 +++++++
 lib/obp60task/OBP60Extensions.h   |   2 -
 lib/obp60task/OBP60Keypad.h       |  10 ++-
 lib/obp60task/PageSystem.cpp      | 109 +++++++++++++++++++++---------
 lib/obp60task/config_obp40.json   |  11 +++
 lib/obp60task/obp60task.cpp       |  61 +++++++----------
 6 files changed, 144 insertions(+), 75 deletions(-)

diff --git a/lib/obp60task/OBP60Extensions.cpp b/lib/obp60task/OBP60Extensions.cpp
index a98cba5..9f7df24 100644
--- a/lib/obp60task/OBP60Extensions.cpp
+++ b/lib/obp60task/OBP60Extensions.cpp
@@ -146,6 +146,32 @@ void deepSleep(CommonData &common){
     getdisplay().powerOff();                // Display power off
     setPortPin(OBP_POWER_50, false);        // Power off ePaper display
     // Stop system
+    esp_deep_sleep_start();             // Deep Sleep with weakup via touch pin
+}
+#endif
+#ifdef BOARD_OBP40S3
+// Deep sleep funktion
+void deepSleep(CommonData &common){
+    RTC_lastpage = common.data.actpage - 1;
+    // Switch off all power lines
+    setPortPin(OBP_BACKLIGHT_LED, false);   // Backlight Off
+    setFlashLED(false);                     // Flash LED Off
+    // Shutdown EInk display
+    getdisplay().setFullWindow();               // Set full Refresh
+    //getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update
+    getdisplay().fillScreen(common.bgcolor);    // Clear screen
+    getdisplay().setTextColor(common.fgcolor);
+    getdisplay().setFont(&Ubuntu_Bold20pt7b);
+    getdisplay().setCursor(85, 150);
+    getdisplay().print("Sleep Mode");
+    getdisplay().setFont(&Ubuntu_Bold8pt7b);
+    getdisplay().setCursor(65, 175);
+    getdisplay().print("For wakeup press wheel and wait 5s");
+    getdisplay().nextPage();                // Partial update
+    getdisplay().powerOff();                // Display power off
+    setPortPin(OBP_POWER_EPD, false);       // Power off ePaper display
+    setPortPin(OBP_POWER_SD, false);        // Power off SD card
+    // Stop system
     esp_deep_sleep_start();             // Deep Sleep with weakup via GPIO pin
 }
 #endif
diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h
index 727f912..b6f653f 100644
--- a/lib/obp60task/OBP60Extensions.h
+++ b/lib/obp60task/OBP60Extensions.h
@@ -64,9 +64,7 @@ Point rotatePoint(const Point& origin, const Point& p, double angle);
 std::vector<Point> rotatePoints(const Point& origin, const std::vector<Point>& pts, double angle);
 void fillPoly4(const std::vector<Point>& p4, uint16_t color);
 
-#ifdef BOARD_OBP60S3
 void deepSleep(CommonData &common);
-#endif
 
 uint8_t getLastPage();
 
diff --git a/lib/obp60task/OBP60Keypad.h b/lib/obp60task/OBP60Keypad.h
index 122355c..937f44e 100644
--- a/lib/obp60task/OBP60Keypad.h
+++ b/lib/obp60task/OBP60Keypad.h
@@ -60,7 +60,7 @@ void initKeys(CommonData &commonData) {
 
   #ifdef HARDWARE_V21
   // Keypad functions for original OBP60 hardware
-  int readKeypad(GwLog* logger, uint thSensitivity) {
+  int readKeypad(GwLog* logger, uint thSensitivity, bool use_syspage) {
     
     // Touch sensor values
     // 35000 - Not touched
@@ -261,7 +261,7 @@ void initKeys(CommonData &commonData) {
     }
 
   // Keypad functions for OBP60 clone (thSensitivity is inactiv)
-  int readKeypad(GwLog* logger, uint thSensitivity) {
+  int readKeypad(GwLog* logger, uint thSensitivity, bool use_syspage) {
     pinMode(UP, INPUT);
     pinMode(DOWN, INPUT);
     pinMode(CONF, INPUT);
@@ -279,7 +279,11 @@ void initKeys(CommonData &commonData) {
       }
       // If key pressed longer than 200ms
       if(millis() > starttime + 200 && keycode == keycodeold) {
-        keystatus = keycode;
+        if (use_syspage and keycode == 3) {
+            keystatus = 12;
+        } else {
+            keystatus = keycode;
+        }
         // Copy keycode
         keycodeold = keycode;
         while(readSensorpads() > 0){} // Wait for pad release
diff --git a/lib/obp60task/PageSystem.cpp b/lib/obp60task/PageSystem.cpp
index e56d94b..7f601a1 100644
--- a/lib/obp60task/PageSystem.cpp
+++ b/lib/obp60task/PageSystem.cpp
@@ -4,6 +4,7 @@
 #include "OBP60Extensions.h"
 #include "images/logo64.xbm"
 #include <esp32/clk.h>
+#include "qrcode.h"
 
 #define STRINGIZE_IMPL(x) #x
 #define STRINGIZE(x) STRINGIZE_IMPL(x)
@@ -66,7 +67,8 @@ public:
             if (hasFRAM) fram.write(FRAM_SYSTEM_MODE, mode);
             return 0;
         }
-        // grab cursor keys to disable page navigation
+#ifdef BOARD_OBP60S3
+        // grab cursor key to disable page navigation
         if (key == 3) {
             return 0;
         }
@@ -74,20 +76,55 @@ public:
         if (key == 4) {
             ESP.restart();
         }
-#ifdef BOARD_OBP60S3
         // standby / deep sleep
         if (key == 5) {
            deepSleep(*commonData);
         }
-#endif
         // Code for keylock
         if (key == 11) {
             commonData->keylock = !commonData->keylock;
             return 0;
         }
+#endif
+#ifdef BOARD_OBP40S3
+        // grab cursor keys to disable page navigation
+        if (key == 9 or key == 10) {
+            return 0;
+        }
+        // standby / deep sleep
+        if (key == 12) {
+            deepSleep(*commonData);
+        }
+#endif
         return key;
     }
 
+    void displayBarcode(String serialno, uint16_t x, uint16_t y, uint16_t s) {
+        // Barcode with serial number
+        // x, y is top left corner
+        // s is pixel size of a single box
+        QRCode qrcode;
+        uint8_t qrcodeData[qrcode_getBufferSize(4)];
+        #ifdef BOARD_OBP40S3
+        String prefix = "OBP40:SN:";
+        #endif
+        #ifdef BOARD_OBP60S3
+        String prefix = "OBP60:SN:";
+        #endif
+        qrcode_initText(&qrcode, qrcodeData, 4, 0, (prefix + serialno).c_str());
+        int16_t x0 = x;
+        for (uint8_t j = 0; j < qrcode.size; j++) {
+            for (uint8_t i = 0; i < qrcode.size; i++) {
+                if (qrcode_getModule(&qrcode, i, j)) {
+                    getdisplay().fillRect(x, y, s, s, commonData->fgcolor);
+                }
+                x += s;
+            }
+            y += s;
+            x = x0;
+        }
+    }
+
     virtual void displayPage(PageData &pageData){
         GwConfigHandler *config = commonData->config;
         GwLog *logger = commonData->logger;
@@ -114,30 +151,36 @@ public:
 
         if (mode == 'N') {
             getdisplay().setFont(&Ubuntu_Bold12pt7b);
-            getdisplay().setCursor(20, 50);
+            getdisplay().setCursor(8, 50);
             getdisplay().print("System Information");
 
             getdisplay().drawXBitmap(320, 25, logo64_bits, logo64_width, logo64_height, commonData->fgcolor);
 
             getdisplay().setFont(&Ubuntu_Bold8pt7b);
 
-            char ssid[23];
-            snprintf(ssid, 23, "MCUDEVICE-%04X%08X", (uint16_t)(chipid >> 32), (uint32_t)chipid);
-            getdisplay().setCursor(20, 70);
-            getdisplay().print(ssid);
-            getdisplay().setCursor(20, 100);
-            getdisplay().print("Press STBY for white page and standby");
+            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("MUDEVICE-") + String(ssid));
+
+            getdisplay().setCursor(8, 90);
+            getdisplay().print("Firmware Version: ");
+            getdisplay().print(VERSINFO);
+
+            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
 
             getdisplay().setCursor(2, y0);
             getdisplay().print("Simulation:");
             getdisplay().setCursor(120, y0);
             getdisplay().print(simulation ? "on" : "off");
 
-            getdisplay().setCursor(202, y0);
-            getdisplay().print("Wifi:");
-            getdisplay().setCursor(300, y0);
-            getdisplay().print(commonData->status.wifiApOn ? "On" : "Off");
-
             getdisplay().setCursor(2, y0 + 16);
             getdisplay().print("Environment:");
             getdisplay().setCursor(120, y0 + 16);
@@ -145,9 +188,9 @@ public:
 
             // total RAM free
             int Heap_free = esp_get_free_heap_size();
-            getdisplay().setCursor(202, y0 + 16);
+            getdisplay().setCursor(202, y0);
             getdisplay().print("Total free:");
-            getdisplay().setCursor(300, y0 + 16);
+            getdisplay().setCursor(300, y0);
             getdisplay().print(String(Heap_free));
 
             getdisplay().setCursor(2, y0 + 32);
@@ -157,37 +200,39 @@ public:
 
             // RAM free for task
             int RAM_free = uxTaskGetStackHighWaterMark(NULL);
-            getdisplay().setCursor(202, y0 + 32);
+            getdisplay().setCursor(202, y0 + 16);
             getdisplay().print("Task free:");
-            getdisplay().setCursor(300, y0 + 32);
+            getdisplay().setCursor(300, y0 + 16);
             getdisplay().print(String(RAM_free));
 
-            getdisplay().setCursor(2, y0 + 48);
+            // FRAM available / status
+            getdisplay().setCursor(202, y0 + 32);
+            getdisplay().print("FRAM:");
+            getdisplay().setCursor(300, y0 + 32);
+            getdisplay().print(hasFRAM ? "available" : "not found");
+
+            getdisplay().setCursor(202, y0 + 64);
             getdisplay().print("CPU speed:");
-            getdisplay().setCursor(120, y0 + 48);
+            getdisplay().setCursor(300, y0 + 64);
             getdisplay().print(cpuspeed);
             getdisplay().print(" / ");
             int cpu_freq = esp_clk_cpu_freq() / 1000000;
             getdisplay().print(String(cpu_freq));
 
-            getdisplay().setCursor(202, y0 + 64);
+            getdisplay().setCursor(2, y0 + 64);
             getdisplay().print("GPS:");
-            getdisplay().setCursor(300, y0 + 64);
+            getdisplay().setCursor(120, y0 + 64);
             getdisplay().print(gps_module);
 
             getdisplay().setCursor(2, y0 + 80);
-            getdisplay().print("FRAM:");
-            getdisplay().setCursor(120, y0 + 80);
-            getdisplay().print(hasFRAM ? "available" : "not found");
-
-            getdisplay().setCursor(202, y0 + 80);
             getdisplay().print("RTC:");
-            getdisplay().setCursor(300, y0 + 80);
+            getdisplay().setCursor(120, y0 + 80);
             getdisplay().print(rtc_module);
 
-            getdisplay().setCursor(2, y0 + 120);
-            getdisplay().print("Firmware Version: ");
-            getdisplay().print(VERSINFO);
+            getdisplay().setCursor(2, y0 + 96);
+            getdisplay().print("Wifi:");
+            getdisplay().setCursor(120, y0 + 96);
+            getdisplay().print(commonData->status.wifiApOn ? "On" : "Off");
 
 
         } else {
diff --git a/lib/obp60task/config_obp40.json b/lib/obp60task/config_obp40.json
index d18533c..3a4b6bf 100644
--- a/lib/obp60task/config_obp40.json
+++ b/lib/obp60task/config_obp40.json
@@ -913,6 +913,17 @@
             "obp40": "true"
         }
     },
+    {
+        "name": "systemPage",
+        "label": "System Page",
+        "type": "boolean",
+        "default": "false",
+        "description": "Use wheel button for system page or direct deep sleep mode",
+        "category":"OBP40 Pages",
+        "capabilities": {
+            "obp40": "true"
+        }
+    },
     {
         "name": "imageFormat",
         "label": "Screenshot Format",
diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp
index 3defb2b..a55e62b 100644
--- a/lib/obp60task/obp60task.cpp
+++ b/lib/obp60task/obp60task.cpp
@@ -118,7 +118,7 @@ void OBP60Init(GwApi *api){
     #endif
 
     #ifdef BOARD_OBP60S3
-    touchSleepWakeUpEnable(TP1, 45);
+    touchSleepWakeUpEnable(TP1, 45); // TODO sensitivity should be configurable via web interface
     touchSleepWakeUpEnable(TP2, 45);
     touchSleepWakeUpEnable(TP3, 45);
     touchSleepWakeUpEnable(TP4, 45);
@@ -169,6 +169,7 @@ typedef struct {
         GwLog* logger = NULL;
 //        GwApi* api = NULL;
         uint sensitivity = 100;
+        bool use_syspage = true;
     } MyData;
 
 // Keyboard Task
@@ -180,7 +181,7 @@ void keyboardTask(void *param){
 
     // Loop for keyboard task
     while (true){
-        keycode = readKeypad(data->logger, data->sensitivity);
+        keycode = readKeypad(data->logger, data->sensitivity, data->use_syspage);
         //send a key event
         if(keycode != 0){
             xQueueSend(data->queue, &keycode, 0);
@@ -336,38 +337,6 @@ void underVoltageDetection(GwApi *api, CommonData &common){
     }
 }
 
-#ifdef BOARD_OBP40S3
-// Deep sleep funktion 
-void deepSleep(CommonData &common){
-    // Switch off all power lines
-    setPortPin(OBP_BACKLIGHT_LED, false);   // Backlight Off
-    setFlashLED(false);                     // Flash LED Off            
-    buzzer(TONE4, 20);                      // Buzzer tone 4kHz 20ms
-    // Shutdown EInk display
-    getdisplay().setFullWindow();               // Set full Refresh
-    //getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update
-    getdisplay().fillScreen(common.bgcolor);    // Clear screen
-    getdisplay().setTextColor(common.fgcolor);
-    getdisplay().setFont(&Ubuntu_Bold20pt7b);
-    getdisplay().setCursor(85, 150);
-    getdisplay().print("Sleep Mode");
-    getdisplay().setFont(&Ubuntu_Bold8pt7b);
-    getdisplay().setCursor(65, 175);
-    getdisplay().print("For wakeup press wheel and wait 5s");
-    getdisplay().nextPage();                // Partial update
-    getdisplay().powerOff();                // Display power off
-    setPortPin(OBP_POWER_EPD, false);       // Power off ePaper display
-    setPortPin(OBP_POWER_SD, false);        // Power off SD card
-    // Stop system
-    while(true){
-        esp_deep_sleep_start();             // Deep Sleep with weakup via GPIO pin
-    }
-
-}
-#endif
-
-
-
 // OBP60 Task
 //####################################################################################
 void OBP60Task(GwApi *api){
@@ -412,6 +381,9 @@ void OBP60Task(GwApi *api){
     bool refreshmode = api->getConfig()->getConfigItem(api->getConfig()->refresh,true)->asBoolean();
     String fastrefresh = api->getConfig()->getConfigItem(api->getConfig()->fastRefresh,true)->asString();
     uint fullrefreshtime = uint(api->getConfig()->getConfigItem(api->getConfig()->fullRefreshTime,true)->asInt());
+    #ifdef BOARD_OBP40S3
+    bool syspage_enabled = config->getBool(config->systemPage);
+    #endif
 
     #ifdef DISPLAY_GDEY042T81
         getdisplay().init(115200, true, 2, false);  // Use this for Waveshare boards with "clever" reset circuit, 2ms reset pulse
@@ -453,16 +425,24 @@ void OBP60Task(GwApi *api){
     // Set start page
     int pageNumber = int(api->getConfig()->getConfigItem(api->getConfig()->startPage,true)->asInt()) - 1;
 
-#ifdef BOARD_OBP60S3
     LOG_DEBUG(GwLog::LOG,"Checking wakeup...");
+#ifdef BOARD_OBP60S3
     if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_TOUCHPAD) {
         LOG_DEBUG(GwLog::LOG,"Wake up by touch pad %d",esp_sleep_get_touchpad_wakeup_status());
         pageNumber = getLastPage();
     } else {
         LOG_DEBUG(GwLog::LOG,"Other wakeup reason");
     }
-    LOG_DEBUG(GwLog::LOG,"...done");
 #endif
+#ifdef BOARD_OBP40S3
+    if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_EXT0) {
+        LOG_DEBUG(GwLog::LOG,"Wake up by key");
+        pageNumber = getLastPage();
+    } else {
+        LOG_DEBUG(GwLog::LOG,"Other wakeup reason");
+    }
+#endif
+    LOG_DEBUG(GwLog::LOG,"...done");
 
     int lastPage=pageNumber;
 
@@ -525,6 +505,9 @@ void OBP60Task(GwApi *api){
     allParameters.page0=3;
     allParameters.queue=xQueueCreate(10,sizeof(int));
     allParameters.sensitivity= api->getConfig()->getInt(GwConfigDefinitions::tSensitivity);
+    #ifdef BOARD_OBP40S3
+    allParameters.use_syspage = syspage_enabled;
+    #endif
     xTaskCreate(keyboardTask,"keyboard",2000,&allParameters,configMAX_PRIORITIES-1,NULL);
     SharedData *shared=new SharedData(api);
     createSensorTask(shared);
@@ -621,10 +604,11 @@ void OBP60Task(GwApi *api){
                 LOG_DEBUG(GwLog::LOG,"new key from keyboard %d",keyboardMessage);
                 keypressed = true;
 
-                if (keyboardMessage == 12) {
+                if (keyboardMessage == 12 and !systemPage) {
                     LOG_DEBUG(GwLog::LOG, "Calling system page");
                     systemPage = true; // System page is out of band
                     syspage->setupKeys();
+                    keyboardMessage = 0;
                 }
                 else {
                     currentPage = pages[pageNumber].page;
@@ -632,6 +616,7 @@ void OBP60Task(GwApi *api){
                         // exit system mode with exit key number 1
                         systemPage = false;
                         currentPage->setupKeys();
+                        keyboardMessage = 0;
                     }
                 }
                 if (systemPage) {
@@ -652,7 +637,7 @@ void OBP60Task(GwApi *api){
                     }
                     #ifdef BOARD_OBP40S3
                     // #3 Deep sleep mode for OBP40
-                    if (keyboardMessage == 3){
+                    if ((keyboardMessage == 3) and !syspage_enabled){
                         deepSleep(commonData);
                     }
                     #endif

From 7afcb864042343dd863439f07793d678309d1370 Mon Sep 17 00:00:00 2001
From: norbert-walter <norbert-walter@web.de>
Date: Fri, 24 Jan 2025 15:23:02 +0100
Subject: [PATCH 08/17] OBP40 Battery voltage measuring and capacity
 calculation

---
 lib/obp60task/OBPSensorTask.cpp | 33 +++++++++++++++++++++++++++++++--
 lib/obp60task/Pagedata.h        |  2 ++
 lib/obp60task/platformio.ini    |  6 +++++-
 3 files changed, 38 insertions(+), 3 deletions(-)

diff --git a/lib/obp60task/OBPSensorTask.cpp b/lib/obp60task/OBPSensorTask.cpp
index 99071ab..cd488ab 100644
--- a/lib/obp60task/OBPSensorTask.cpp
+++ b/lib/obp60task/OBPSensorTask.cpp
@@ -88,8 +88,16 @@ void sensorTask(void *param){
     double voffset = (api->getConfig()->getConfigItem(api->getConfig()->vOffset,true)->asString()).toFloat();
     double vslope = (api->getConfig()->getConfigItem(api->getConfig()->vSlope,true)->asString()).toFloat();
     if(String(powsensor1) == "off"){
-        sensors.batteryVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.17) * 20;   // Vin = 1/20
+        #ifdef VOLTAGE_SENSOR
+        sensors.batteryVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.53) * 2;   // Vin = 1/2 for OBP40
+        #else
+        sensors.batteryVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.17) * 20;   // Vin = 1/20 for OBP60    
+        #endif
         sensors.batteryVoltage = sensors.batteryVoltage * vslope + voffset; // Calibration
+        #ifdef LIPO_ACCU_1200
+        sensors.BatteryChargeStatus = 0;    // Set to discharging
+        sensors.batteryLevelLiPo = 0;       // Level 0...100%
+        #endif
         sensors.batteryCurrent = 0;
         sensors.batteryPower = 0;
         // Fill average arrays with start values
@@ -459,8 +467,29 @@ void sensorTask(void *param){
         // Send supply voltage value all 1s
         if(millis() > starttime5 + 1000 && String(powsensor1) == "off"){
             starttime5 = millis();
-            sensors.batteryVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.17) * 20;   // Vin = 1/20
+            #ifdef VOLTAGE_SENSOR
+            sensors.batteryVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.53) * 2;   // Vin = 1/2 for OBP40
+            #else
+            sensors.batteryVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.17) * 20;   // Vin = 1/20 for OBP60    
+            #endif
             sensors.batteryVoltage = sensors.batteryVoltage * vslope + voffset; // Calibration
+            #ifdef LIPO_ACCU_1200
+            if(sensors.batteryVoltage > 4.1){
+                sensors.BatteryChargeStatus = 1;    // Charging active
+            }
+            else{
+                sensors.BatteryChargeStatus = 0;    // Discharging
+            }
+            // Polynomfit for LiPo capacity calculation for 3,7V LiPo accus, 0...100%
+            sensors.batteryLevelLiPo = sensors.batteryVoltage * sensors.batteryVoltage * 174.9513 + sensors.batteryVoltage * 1147,7686 + 1868.5120;
+            // Limiter
+            if(sensors.batteryLevelLiPo > 100){
+                sensors.batteryLevelLiPo = 100;
+            }
+            if(sensors.batteryLevelLiPo < 0){
+                sensors.batteryLevelLiPo = 0;
+            }
+            #endif
             // Save new data in average array
             batV.reading(int(sensors.batteryVoltage * 100));
             // Calculate the average values for different time lines from integer values
diff --git a/lib/obp60task/Pagedata.h b/lib/obp60task/Pagedata.h
index b385b5c..d511aee 100644
--- a/lib/obp60task/Pagedata.h
+++ b/lib/obp60task/Pagedata.h
@@ -31,6 +31,8 @@ typedef struct{
   double batteryVoltage300 = 0; // Sliding average over 300 values
   double batteryCurrent300 = 0;
   double batteryPower300 = 0;
+  double batteryLevelLiPo = 0;  // Battery level for OBP40 LiPo accu
+  int BatteryChargeStatus = 0;  // LiPo charge status: 0 = discharge, 1 = loading activ
   double solarVoltage = 0;
   double solarCurrent = 0;
   double solarPower = 0;
diff --git a/lib/obp60task/platformio.ini b/lib/obp60task/platformio.ini
index da78f39..662032a 100644
--- a/lib/obp60task/platformio.ini
+++ b/lib/obp60task/platformio.ini
@@ -2,7 +2,9 @@
 #if you want a pio run to only build
 #your special environments you can set this here
 #by uncommenting the next line
-default_envs = obp60_s3
+default_envs =
+    obp60_s3
+    obp40_s3
 
 [env:obp60_s3]
 platform = espressif32@6.8.1
@@ -91,6 +93,8 @@ build_flags=
     -D DISABLE_DIAGNOSTIC_OUTPUT    #Disable diagnostic output for GxEPD2 lib
     -D BOARD_OBP40S3                #Board OBP40 V1.0 with ESP32S3 SKU:DIE07300S (CrowPanel 4.2)
     -D DISPLAY_GDEY042T81           #new E-Ink display from Waveshare, R10 2.2 ohm
+    -D LIPO_ACCU_1200               #Hardware extension, LiPo accu 3,7V 1200mAh
+    -D VOLTAGE_SENSOR               #Hardware extension, LiPo voltage sensor with two resistors 
     ${env.build_flags}
 upload_port = /dev/ttyUSB0          #OBP40 download via external USB/Serail converter
 upload_protocol = esptool           #firmware upload via USB OTG seriell, by first upload need to set the ESP32-S3 in the upload mode with shortcut GND to Pin27

From e9ee49a6ef923a6e888a40364acb109c1650c8bb Mon Sep 17 00:00:00 2001
From: norbert-walter <norbert-walter@web.de>
Date: Fri, 24 Jan 2025 17:33:34 +0100
Subject: [PATCH 09/17] Add undervoltage for LiPo accu for OBP40

---
 lib/obp60task/obp60task.cpp | 39 ++++++++++++++++++++++++++++++++++---
 1 file changed, 36 insertions(+), 3 deletions(-)

diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp
index 3defb2b..b87c4bd 100644
--- a/lib/obp60task/obp60task.cpp
+++ b/lib/obp60task/obp60task.cpp
@@ -308,13 +308,42 @@ void registerAllPages(PageList &list){
 
 // Undervoltage detection for shutdown display
 void underVoltageDetection(GwApi *api, CommonData &common){
+    float actVoltage = 0;
+    float minVoltage = 0;
     // Read settings
     float vslope = uint(api->getConfig()->getConfigItem(api->getConfig()->vSlope,true)->asFloat());
     float voffset = uint(api->getConfig()->getConfigItem(api->getConfig()->vOffset,true)->asFloat());
     // Read supply voltage
-    float actVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.17) * 20;   // V = 1/20 * Vin
+    #if defined VOLTAGE_SENSOR && defined LIPO_ACCU_1200
+    actVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.53) * 2;   // Vin = 1/2 for OBP40
+    minVoltage = 3.65;  // Absolut minimum volatge for 3,7V LiPo accu
+    #else
+    actVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.17) * 20;   // Vin = 1/20 for OBP60
+    minVoltage = MIN_VOLTAGE;
+    #endif
     actVoltage = actVoltage * vslope + voffset;
-    if(actVoltage < MIN_VOLTAGE){
+    if(actVoltage < minVoltage){
+        #if defined VOLTAGE_SENSOR && defined LIPO_ACCU_1200
+        // Switch off all power lines
+        setPortPin(OBP_BACKLIGHT_LED, false);   // Backlight Off
+        setFlashLED(false);                     // Flash LED Off            
+        buzzer(TONE4, 20);                      // Buzzer tone 4kHz 20ms
+        // Shutdown EInk display
+        getdisplay().setFullWindow();               // Set full Refresh
+        //getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update
+        getdisplay().fillScreen(common.bgcolor);    // Clear screen
+        getdisplay().setTextColor(common.fgcolor);
+        getdisplay().setFont(&Ubuntu_Bold20pt7b);
+        getdisplay().setCursor(65, 150);
+        getdisplay().print("Undervoltage");
+        getdisplay().setFont(&Ubuntu_Bold8pt7b);
+        getdisplay().setCursor(65, 175);
+        getdisplay().print("To wake up press reset");
+        getdisplay().nextPage();                // Partial update
+        getdisplay().powerOff();                // Display power off
+        setPortPin(OBP_POWER_EPD, false);       // Power off ePaper display
+        setPortPin(OBP_POWER_SD, false);        // Power off SD card
+        #else
         // Switch off all power lines
         setPortPin(OBP_BACKLIGHT_LED, false);   // Backlight Off
         setFlashLED(false);                     // Flash LED Off            
@@ -327,8 +356,12 @@ void underVoltageDetection(GwApi *api, CommonData &common){
         getdisplay().setFont(&Ubuntu_Bold20pt7b);
         getdisplay().setCursor(65, 150);
         getdisplay().print("Undervoltage");
+        getdisplay().setFont(&Ubuntu_Bold8pt7b);
+        getdisplay().setCursor(65, 175);
+        getdisplay().print("To wake up repower system");
         getdisplay().nextPage();                // Partial update
         getdisplay().powerOff();                // Display power off
+        #endif
         // Stop system
         while(true){
             esp_deep_sleep_start();             // Deep Sleep without weakup. Weakup only after power cycle (restart).
@@ -353,7 +386,7 @@ void deepSleep(CommonData &common){
     getdisplay().print("Sleep Mode");
     getdisplay().setFont(&Ubuntu_Bold8pt7b);
     getdisplay().setCursor(65, 175);
-    getdisplay().print("For wakeup press wheel and wait 5s");
+    getdisplay().print("To wake up press wheel and wait 5s");
     getdisplay().nextPage();                // Partial update
     getdisplay().powerOff();                // Display power off
     setPortPin(OBP_POWER_EPD, false);       // Power off ePaper display

From 0b7863cb86785766929a184da43481a85e8a7061 Mon Sep 17 00:00:00 2001
From: norbert-walter <norbert-walter@web.de>
Date: Fri, 24 Jan 2025 18:41:48 +0100
Subject: [PATCH 10/17] Typo

---
 lib/obp60task/obp60task.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp
index b87c4bd..4c0ff64 100644
--- a/lib/obp60task/obp60task.cpp
+++ b/lib/obp60task/obp60task.cpp
@@ -370,7 +370,7 @@ void underVoltageDetection(GwApi *api, CommonData &common){
 }
 
 #ifdef BOARD_OBP40S3
-// Deep sleep funktion 
+// Deep sleep function 
 void deepSleep(CommonData &common){
     // Switch off all power lines
     setPortPin(OBP_BACKLIGHT_LED, false);   // Backlight Off

From f116e41964308ffea52c9d0e6158b190167355b6 Mon Sep 17 00:00:00 2001
From: norbert-walter <norbert-walter@web.de>
Date: Fri, 24 Jan 2025 22:18:38 +0100
Subject: [PATCH 11/17] Fix for config.json, depensencies to other ode

---
 lib/obp60task/OBP60Extensions.cpp | 4 ++--
 lib/obp60task/obp60task.cpp       | 5 +++--
 2 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/lib/obp60task/OBP60Extensions.cpp b/lib/obp60task/OBP60Extensions.cpp
index 9f7df24..3407434 100644
--- a/lib/obp60task/OBP60Extensions.cpp
+++ b/lib/obp60task/OBP60Extensions.cpp
@@ -146,7 +146,7 @@ void deepSleep(CommonData &common){
     getdisplay().powerOff();                // Display power off
     setPortPin(OBP_POWER_50, false);        // Power off ePaper display
     // Stop system
-    esp_deep_sleep_start();             // Deep Sleep with weakup via touch pin
+    esp_deep_sleep_start();                 // Deep Sleep with weakup via touch pin
 }
 #endif
 #ifdef BOARD_OBP40S3
@@ -166,7 +166,7 @@ void deepSleep(CommonData &common){
     getdisplay().print("Sleep Mode");
     getdisplay().setFont(&Ubuntu_Bold8pt7b);
     getdisplay().setCursor(65, 175);
-    getdisplay().print("For wakeup press wheel and wait 5s");
+    getdisplay().print("to wake up press wheel and wait 5s");
     getdisplay().nextPage();                // Partial update
     getdisplay().powerOff();                // Display power off
     setPortPin(OBP_POWER_EPD, false);       // Power off ePaper display
diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp
index 7dfd29d..fea1fb6 100644
--- a/lib/obp60task/obp60task.cpp
+++ b/lib/obp60task/obp60task.cpp
@@ -78,7 +78,7 @@ void OBP60Init(GwApi *api){
     }
 
     #ifdef BOARD_OBP40S3
-    //String sdcard = config->getConfigItem(config->useSDCard, true)->asString();
+//    String sdcard = config->getConfigItem(config->useSDCard, true)->asString();
     String sdcard = "on";
     if (sdcard == "on") {
         SPIClass SD_SPI = SPIClass(HSPI);
@@ -415,7 +415,8 @@ void OBP60Task(GwApi *api){
     String fastrefresh = api->getConfig()->getConfigItem(api->getConfig()->fastRefresh,true)->asString();
     uint fullrefreshtime = uint(api->getConfig()->getConfigItem(api->getConfig()->fullRefreshTime,true)->asInt());
     #ifdef BOARD_OBP40S3
-    bool syspage_enabled = config->getBool(config->systemPage);
+//    bool syspage_enabled = config->getBool(config->systemPage);
+    bool syspage_enabled = false;
     #endif
 
     #ifdef DISPLAY_GDEY042T81

From ea9a2ff9c49eb8ff98f5f3e3e17d7baeb3e51385 Mon Sep 17 00:00:00 2001
From: norbert-walter <norbert-walter@web.de>
Date: Fri, 24 Jan 2025 22:18:38 +0100
Subject: [PATCH 12/17] Fix for config.json, dependencies to other ode

---
 lib/obp60task/OBP60Extensions.cpp | 4 ++--
 lib/obp60task/obp60task.cpp       | 5 +++--
 2 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/lib/obp60task/OBP60Extensions.cpp b/lib/obp60task/OBP60Extensions.cpp
index 9f7df24..3407434 100644
--- a/lib/obp60task/OBP60Extensions.cpp
+++ b/lib/obp60task/OBP60Extensions.cpp
@@ -146,7 +146,7 @@ void deepSleep(CommonData &common){
     getdisplay().powerOff();                // Display power off
     setPortPin(OBP_POWER_50, false);        // Power off ePaper display
     // Stop system
-    esp_deep_sleep_start();             // Deep Sleep with weakup via touch pin
+    esp_deep_sleep_start();                 // Deep Sleep with weakup via touch pin
 }
 #endif
 #ifdef BOARD_OBP40S3
@@ -166,7 +166,7 @@ void deepSleep(CommonData &common){
     getdisplay().print("Sleep Mode");
     getdisplay().setFont(&Ubuntu_Bold8pt7b);
     getdisplay().setCursor(65, 175);
-    getdisplay().print("For wakeup press wheel and wait 5s");
+    getdisplay().print("to wake up press wheel and wait 5s");
     getdisplay().nextPage();                // Partial update
     getdisplay().powerOff();                // Display power off
     setPortPin(OBP_POWER_EPD, false);       // Power off ePaper display
diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp
index 7dfd29d..fea1fb6 100644
--- a/lib/obp60task/obp60task.cpp
+++ b/lib/obp60task/obp60task.cpp
@@ -78,7 +78,7 @@ void OBP60Init(GwApi *api){
     }
 
     #ifdef BOARD_OBP40S3
-    //String sdcard = config->getConfigItem(config->useSDCard, true)->asString();
+//    String sdcard = config->getConfigItem(config->useSDCard, true)->asString();
     String sdcard = "on";
     if (sdcard == "on") {
         SPIClass SD_SPI = SPIClass(HSPI);
@@ -415,7 +415,8 @@ void OBP60Task(GwApi *api){
     String fastrefresh = api->getConfig()->getConfigItem(api->getConfig()->fastRefresh,true)->asString();
     uint fullrefreshtime = uint(api->getConfig()->getConfigItem(api->getConfig()->fullRefreshTime,true)->asInt());
     #ifdef BOARD_OBP40S3
-    bool syspage_enabled = config->getBool(config->systemPage);
+//    bool syspage_enabled = config->getBool(config->systemPage);
+    bool syspage_enabled = false;
     #endif
 
     #ifdef DISPLAY_GDEY042T81

From 26e551c6162f8ff61aae381637c29da0cd129371 Mon Sep 17 00:00:00 2001
From: norbert-walter <norbert-walter@web.de>
Date: Fri, 24 Jan 2025 22:29:12 +0100
Subject: [PATCH 13/17] Typo

---
 lib/obp60task/OBP60Extensions.cpp | 4 ++--
 lib/obp60task/obp60task.cpp       | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/lib/obp60task/OBP60Extensions.cpp b/lib/obp60task/OBP60Extensions.cpp
index 3407434..b74d76d 100644
--- a/lib/obp60task/OBP60Extensions.cpp
+++ b/lib/obp60task/OBP60Extensions.cpp
@@ -141,7 +141,7 @@ void deepSleep(CommonData &common){
     getdisplay().print("Sleep Mode");
     getdisplay().setFont(&Ubuntu_Bold8pt7b);
     getdisplay().setCursor(65, 175);
-    getdisplay().print("For wakeup press key and wait 5s");
+    getdisplay().print("To wake up press key and wait 5s");
     getdisplay().nextPage();                // Update display contents
     getdisplay().powerOff();                // Display power off
     setPortPin(OBP_POWER_50, false);        // Power off ePaper display
@@ -166,7 +166,7 @@ void deepSleep(CommonData &common){
     getdisplay().print("Sleep Mode");
     getdisplay().setFont(&Ubuntu_Bold8pt7b);
     getdisplay().setCursor(65, 175);
-    getdisplay().print("to wake up press wheel and wait 5s");
+    getdisplay().print("To wake up press wheel and wait 5s");
     getdisplay().nextPage();                // Partial update
     getdisplay().powerOff();                // Display power off
     setPortPin(OBP_POWER_EPD, false);       // Power off ePaper display
diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp
index fea1fb6..51828e4 100644
--- a/lib/obp60task/obp60task.cpp
+++ b/lib/obp60task/obp60task.cpp
@@ -339,7 +339,7 @@ void underVoltageDetection(GwApi *api, CommonData &common){
         getdisplay().print("Undervoltage");
         getdisplay().setFont(&Ubuntu_Bold8pt7b);
         getdisplay().setCursor(65, 175);
-        getdisplay().print("To wake up press reset");
+        getdisplay().print("Charge battery and restart");
         getdisplay().nextPage();                // Partial update
         getdisplay().powerOff();                // Display power off
         setPortPin(OBP_POWER_EPD, false);       // Power off ePaper display
@@ -416,7 +416,7 @@ void OBP60Task(GwApi *api){
     uint fullrefreshtime = uint(api->getConfig()->getConfigItem(api->getConfig()->fullRefreshTime,true)->asInt());
     #ifdef BOARD_OBP40S3
 //    bool syspage_enabled = config->getBool(config->systemPage);
-    bool syspage_enabled = false;
+    bool syspage_enabled = true;
     #endif
 
     #ifdef DISPLAY_GDEY042T81

From bdfaf3c8864004e0e1ee827d855559d765a4baae Mon Sep 17 00:00:00 2001
From: Thomas Hooge <thomas@hoogi.de>
Date: Sat, 25 Jan 2025 09:39:12 +0100
Subject: [PATCH 14/17] Added battery symbols

---
 lib/obp60task/OBP60Extensions.h | 40 +++++++++++++++++++++++++++++++++
 1 file changed, 40 insertions(+)

diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h
index b6f653f..845283f 100644
--- a/lib/obp60task/OBP60Extensions.h
+++ b/lib/obp60task/OBP60Extensions.h
@@ -160,6 +160,46 @@ static std::map<String, unsigned char *> iconmap = {
     {"AP", ap_bits}
 };
 
+// Battery
+#define battery_width 24
+#define battery_height 16
+
+static unsigned char battery_0_bits[] = {
+   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0xff, 0x0f, 0xff, 0xff, 0x1f,
+   0x03, 0x00, 0x18, 0x03, 0x00, 0x78, 0x03, 0x00, 0xf8, 0x03, 0x00, 0xd8,
+   0x03, 0x00, 0xd8, 0x03, 0x00, 0xd8, 0x03, 0x00, 0xf8, 0x03, 0x00, 0x78,
+   0x03, 0x00, 0x18, 0xff, 0xff, 0x1f, 0xfe, 0xff, 0x0f, 0x00, 0x00, 0x00 };
+
+static unsigned char battery_25_bits[] = {
+   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0xff, 0x0f, 0xff, 0xff, 0x1f,
+   0x03, 0x00, 0x18, 0x3b, 0x00, 0x78, 0x3b, 0x00, 0xf8, 0x3b, 0x00, 0xd8,
+   0x3b, 0x00, 0xd8, 0x3b, 0x00, 0xd8, 0x3b, 0x00, 0xf8, 0x3b, 0x00, 0x78,
+   0x03, 0x00, 0x18, 0xff, 0xff, 0x1f, 0xfe, 0xff, 0x0f, 0x00, 0x00, 0x00 };
+
+static unsigned char battery_50_bits[] = {
+   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0xff, 0x0f, 0xff, 0xff, 0x1f,
+   0x03, 0x00, 0x18, 0xbb, 0x03, 0x78, 0xbb, 0x03, 0xf8, 0xbb, 0x03, 0xd8,
+   0xbb, 0x03, 0xd8, 0xbb, 0x03, 0xd8, 0xbb, 0x03, 0xf8, 0xbb, 0x03, 0x78,
+   0x03, 0x00, 0x18, 0xff, 0xff, 0x1f, 0xfe, 0xff, 0x0f, 0x00, 0x00, 0x00 };
+
+static unsigned char battery_75_bits[] = {
+   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0xff, 0x0f, 0xff, 0xff, 0x1f,
+   0x03, 0x00, 0x18, 0xbb, 0x3b, 0x78, 0xbb, 0x3b, 0xf8, 0xbb, 0x3b, 0xd8,
+   0xbb, 0x3b, 0xd8, 0xbb, 0x3b, 0xd8, 0xbb, 0x3b, 0xf8, 0xbb, 0x3b, 0x78,
+   0x03, 0x00, 0x18, 0xff, 0xff, 0x1f, 0xfe, 0xff, 0x0f, 0x00, 0x00, 0x00 };
+
+static unsigned char battery_100_bits[] = {
+   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0xff, 0x0f, 0xff, 0xff, 0x1f,
+   0x03, 0x00, 0x18, 0xbb, 0xbb, 0x7b, 0xbb, 0xbb, 0xfb, 0xbb, 0xbb, 0xdb,
+   0xbb, 0xbb, 0xdb, 0xbb, 0xbb, 0xdb, 0xbb, 0xbb, 0xfb, 0xbb, 0xbb, 0x7b,
+   0x03, 0x00, 0x18, 0xff, 0xff, 0x1f, 0xfe, 0xff, 0x0f, 0x00, 0x00, 0x00 };
+
+static unsigned char battery_loading_bits[] = {
+   0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0xfe, 0xe4, 0x0f, 0xff, 0xec, 0x1f,
+   0x03, 0x08, 0x18, 0x03, 0x18, 0x78, 0x03, 0x30, 0xf8, 0x83, 0x3f, 0xd8,
+   0x03, 0x7f, 0xd8, 0x03, 0x03, 0xd8, 0x03, 0x06, 0xf8, 0x03, 0x04, 0x78,
+   0x03, 0x0c, 0x18, 0xff, 0xcb, 0x1f, 0xfe, 0xd3, 0x0f, 0x00, 0x10, 0x00 };
+
 // Other symbols
 #define swipe_width 24
 #define swipe_height 16

From 097111c27002b1f9bfe1032b3dd39ba107b87f9d Mon Sep 17 00:00:00 2001
From: Thomas Hooge <thomas@hoogi.de>
Date: Sat, 25 Jan 2025 10:21:34 +0100
Subject: [PATCH 15/17] Added code for new OBP40 battery symbols

---
 lib/obp60task/OBP60Extensions.cpp | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)

diff --git a/lib/obp60task/OBP60Extensions.cpp b/lib/obp60task/OBP60Extensions.cpp
index 9f7df24..95b3044 100644
--- a/lib/obp60task/OBP60Extensions.cpp
+++ b/lib/obp60task/OBP60Extensions.cpp
@@ -392,6 +392,25 @@ void displayHeader(CommonData &commonData, GwApi::BoatValue *date, GwApi::BoatVa
             getdisplay().drawXBitmap(166, 1, swipe_bits, swipe_width, swipe_height, commonData.fgcolor);
         }
 #endif
+#ifdef LIPO_ACCU_1200
+        if (commonData.data.BatteryChargeStatus == 1) {
+             getdisplay().drawXBitmap(170, 1, battery_loading_bits, battery_width, battery_height, commonData.fgcolor);
+        } else {
+#ifdef VOLTAGE_SENSOR
+            if (commonData.data.batteryLevelLiPo < 10) {
+                getdisplay().drawXBitmap(170, 1, battery_0_bits, battery_width, battery_height, commonData.fgcolor);
+            } else if (commonData.data.batteryLevelLiPo < 25) {
+                getdisplay().drawXBitmap(170, 1, battery_25_bits, battery_width, battery_height, commonData.fgcolor);
+            } else if (commonData.data.batteryLevelLiPo < 50) {
+                getdisplay().drawXBitmap(170, 1, battery_50_bits, battery_width, battery_height, commonData.fgcolor);
+            } else if (commonData.data.batteryLevelLiPo < 75) {
+                getdisplay().drawXBitmap(170, 1, battery_75_bits, battery_width, battery_height, commonData.fgcolor);
+            } else {
+                getdisplay().drawXBitmap(170, 1, battery_100_bits, battery_width, battery_height, commonData.fgcolor);
+            }
+#endif // VOLTAGE_SENSOR
+        }
+#endif // LIPO_ACCU_1200
 
         // Heartbeat as dot
         getdisplay().setTextColor(commonData.fgcolor);

From df81e6e443abc2ce80fb909ff2574c2b43e979ca Mon Sep 17 00:00:00 2001
From: norbert-walter <norbert-walter@web.de>
Date: Sat, 25 Jan 2025 16:19:10 +0100
Subject: [PATCH 16/17] Fix charge status, add to PageVoltage

---
 boards/obp40_s3_n8r8.json       |   4 +-
 extra_script.py                 |  16 +-
 extra_script.py.old             | 518 ++++++++++++++++++++++++++++++++
 lib/obp60task/OBP60Extensions.h |   4 +-
 lib/obp60task/OBPSensorTask.cpp |  37 ++-
 lib/obp60task/PageVoltage.cpp   |  17 ++
 lib/obp60task/config_obp40.json |   4 +-
 lib/obp60task/obp60task.cpp     |  18 +-
 8 files changed, 586 insertions(+), 32 deletions(-)
 create mode 100644 extra_script.py.old

diff --git a/boards/obp40_s3_n8r8.json b/boards/obp40_s3_n8r8.json
index 260050c..2ac8c3e 100644
--- a/boards/obp40_s3_n8r8.json
+++ b/boards/obp40_s3_n8r8.json
@@ -19,8 +19,8 @@
     "flash_mode": "qio",
     "hwids": [
       [
-        "0x303A",
-        "0x1001"
+        "0x1A86",
+        "0x7523"
       ]
     ],
     "mcu": "esp32s3",
diff --git a/extra_script.py b/extra_script.py
index ed62c64..b185a93 100644
--- a/extra_script.py
+++ b/extra_script.py
@@ -10,7 +10,7 @@ from datetime import datetime
 import re
 import pprint
 from platformio.project.config import ProjectConfig
-
+from platformio.project.exception import InvalidProjectConfError
 
 Import("env")
 #print(env.Dump())
@@ -104,7 +104,17 @@ def writeFileIfChanged(fileName,data):
     return True    
 
 def mergeConfig(base,other):
+    try:
+        customconfig = env.GetProjectOption("custom_config")
+    except InvalidProjectConfError:
+        customconfig = None
     for bdir in other:
+        if customconfig and os.path.exists(os.path.join(bdir,customconfig)):
+            cname=os.path.join(bdir,customconfig)
+            print("merge custom config {}".format(cname))
+            with open(cname,'rb') as ah:
+                base += json.load(ah)
+            continue   
         cname=os.path.join(bdir,"config.json")
         if os.path.exists(cname):
             print("merge config %s"%cname)
@@ -274,9 +284,9 @@ class Grove:
     def _ss(self,z=False):
         if z:
             return self.name
-        return self.name if self.name is not 'Z' else ''
+        return self.name if self.name != 'Z' else ''
     def _suffix(self):
-        return '_'+self.name if self.name is not 'Z' else ''
+        return '_'+self.name if self.name != 'Z' else ''
     def replace(self,line):
         if line is None:
             return line
diff --git a/extra_script.py.old b/extra_script.py.old
new file mode 100644
index 0000000..ed62c64
--- /dev/null
+++ b/extra_script.py.old
@@ -0,0 +1,518 @@
+print("running extra...")
+import gzip
+import shutil
+import os
+import sys
+import inspect
+import json
+import glob
+from datetime import datetime
+import re
+import pprint
+from platformio.project.config import ProjectConfig
+
+
+Import("env")
+#print(env.Dump())
+OWN_FILE="extra_script.py"
+GEN_DIR='lib/generated'
+CFG_FILE='web/config.json'
+XDR_FILE='web/xdrconfig.json'
+INDEXJS="index.js"
+INDEXCSS="index.css"
+CFG_INCLUDE='GwConfigDefinitions.h'
+CFG_INCLUDE_IMPL='GwConfigDefImpl.h'
+XDR_INCLUDE='GwXdrTypeMappings.h'
+TASK_INCLUDE='GwUserTasks.h'
+GROVE_CONFIG="GwM5GroveGen.h"
+GROVE_CONFIG_IN="lib/hardware/GwM5Grove.in"
+EMBEDDED_INCLUDE="GwEmbeddedFiles.h"
+
+def getEmbeddedFiles(env):
+    rt=[]
+    efiles=env.GetProjectOption("board_build.embed_files")
+    for f in efiles.split("\n"):
+        if f == '':
+            continue
+        rt.append(f)
+    return rt  
+
+def basePath():
+    #see: https://stackoverflow.com/questions/16771894/python-nameerror-global-name-file-is-not-defined
+    return os.path.dirname(inspect.getfile(lambda: None))
+
+def outPath():
+    return os.path.join(basePath(),GEN_DIR)
+def checkDir():
+    dn=outPath()
+    if not os.path.exists(dn):
+        os.makedirs(dn)
+    if not os.path.isdir(dn):
+        print("unable to create %s"%dn)
+        return False
+    return True    
+
+def isCurrent(infile,outfile):
+    if os.path.exists(outfile):
+        otime=os.path.getmtime(outfile)
+        itime=os.path.getmtime(infile)
+        if (otime >= itime):
+            own=os.path.join(basePath(),OWN_FILE)
+            if os.path.exists(own):
+                owntime=os.path.getmtime(own)
+                if owntime > otime:
+                    return False
+            print("%s is newer then %s, no need to recreate"%(outfile,infile))
+            return True
+    return False        
+def compressFile(inFile,outfile):
+    if isCurrent(inFile,outfile):
+        return
+    print("compressing %s"%inFile)
+    with open(inFile, 'rb') as f_in:
+        with gzip.open(outfile, 'wb') as f_out:
+            shutil.copyfileobj(f_in, f_out)
+
+def generateFile(infile,outfile,callback,inMode='rb',outMode='w'):
+    if isCurrent(infile,outfile):
+        return
+    print("creating %s"%outfile)
+    oh=None
+    with open(infile,inMode) as ch:
+        with open(outfile,outMode) as oh:
+            try:
+                callback(ch,oh,inFile=infile)
+                oh.close()
+            except Exception as e:
+                try:
+                    oh.close()
+                except:
+                    pass
+                os.unlink(outfile)
+                raise
+
+def writeFileIfChanged(fileName,data):
+    if os.path.exists(fileName):
+        with open(fileName,"r") as ih:
+            old=ih.read()
+            ih.close()
+            if old == data:
+                return False
+    print("#generating %s"%fileName)
+    with open(fileName,"w") as oh:
+        oh.write(data)
+    return True    
+
+def mergeConfig(base,other):
+    for bdir in other:
+        cname=os.path.join(bdir,"config.json")
+        if os.path.exists(cname):
+            print("merge config %s"%cname)
+            with open(cname,'rb') as ah:
+                merge=json.load(ah)
+                base=base+merge
+    return base
+
+def replaceTexts(data,replacements):
+    if replacements is None:
+        return data
+    if isinstance(data,str):
+        for k,v in replacements.items():
+            data=data.replace("$"+k,str(v))
+        return data
+    if isinstance(data,list):
+        rt=[]
+        for e in data:
+            rt.append(replaceTexts(e,replacements))
+        return rt
+    if isinstance(data,dict):   
+        rt={} 
+        for k,v in data.items():
+            rt[replaceTexts(k,replacements)]=replaceTexts(v,replacements)
+        return rt
+    return data
+def expandConfig(config):
+    rt=[]
+    for item in config:
+        type=item.get('type')
+        if type != 'array':
+            rt.append(item)
+            continue
+        replacements=item.get('replace')
+        children=item.get('children')
+        name=item.get('name')
+        if name is None:
+            name="#unknown#"
+        if not isinstance(replacements,list):
+            raise Exception("missing replacements at array %s"%name)
+        for replace in replacements:
+            if children is not None:
+                for c in children:
+                    rt.append(replaceTexts(c,replace))
+    return rt
+
+def generateMergedConfig(inFile,outFile,addDirs=[]):
+    if not os.path.exists(inFile):
+        raise Exception("unable to read cfg file %s"%inFile)
+    data=""
+    with open(inFile,'rb') as ch:    
+        config=json.load(ch)
+        config=mergeConfig(config,addDirs)
+        config=expandConfig(config)
+        data=json.dumps(config,indent=2)
+        writeFileIfChanged(outFile,data)
+
+def generateCfg(inFile,outFile,impl):
+    if not os.path.exists(inFile):
+        raise Exception("unable to read cfg file %s"%inFile)
+    data=""
+    with open(inFile,'rb') as ch:    
+        config=json.load(ch)     
+        data+="//generated from %s\n"%inFile
+        l=len(config)
+        idx=0
+        if not impl:
+            data+='#include "GwConfigItem.h"\n'
+            data+='class GwConfigDefinitions{\n'
+            data+='  public:\n'
+            data+='  int getNumConfig() const{return %d;}\n'%(l)
+            for item in config:
+                n=item.get('name')
+                if n is None:
+                    continue
+                if len(n) > 15:
+                    raise Exception("%s: config names must be max 15 caracters"%n)
+                data+='  static constexpr const char* %s="%s";\n'%(n,n)
+            data+="};\n"
+        else:
+            data+='void GwConfigHandler::populateConfigs(GwConfigInterface **config){\n'
+            for item in config:
+                name=item.get('name')
+                if name is None:
+                    continue
+                data+='  configs[%d]='%(idx)
+                idx+=1
+                secret="false";
+                if item.get('type') == 'password':
+                    secret="true"
+                data+="     new GwConfigInterface(%s,\"%s\",%s);\n"%(name,item.get('default'),secret)
+            data+='}\n'  
+    writeFileIfChanged(outFile,data)    
+                    
+def labelFilter(label):
+    return re.sub("[^a-zA-Z0-9]","",re.sub("\([0-9]*\)","",label))    
+def generateXdrMappings(fp,oh,inFile=''):
+    jdoc=json.load(fp)
+    oh.write("static GwXDRTypeMapping* typeMappings[]={\n")
+    first=True
+    for cat in jdoc:
+        item=jdoc[cat]
+        cid=item.get('id')
+        if cid is None:
+            continue
+        tc=item.get('type')
+        if tc is not None:
+            if first:
+                first=False
+            else:
+                oh.write(",\n")
+            oh.write("   new GwXDRTypeMapping(%d,0,%d) /*%s*/"%(cid,tc,cat))
+        fields=item.get('fields')
+        if fields is None:
+            continue
+        idx=0
+        for fe in fields:
+            if not isinstance(fe,dict):
+                continue
+            tc=fe.get('t')
+            id=fe.get('v')
+            if id is None:
+                id=idx
+            idx+=1
+            l=fe.get('l') or ''
+            if tc is None or id is None:
+                continue
+            if first:
+                first=False
+            else:
+                oh.write(",\n")
+            oh.write("   new GwXDRTypeMapping(%d,%d,%d) /*%s:%s*/"%(cid,id,tc,cat,l))
+    oh.write("\n")
+    oh.write("};\n")
+    for cat in jdoc:
+        item=jdoc[cat]
+        cid=item.get('id')
+        if cid is None:
+            continue
+        selectors=item.get('selector')
+        if selectors is not None:
+            for selector in selectors:
+                label=selector.get('l')
+                value=selector.get('v')
+                if label is not None and value is not None:
+                    label=labelFilter(label)
+                    define=("GWXDRSEL_%s_%s"%(cat,label)).upper()
+                    oh.write("    #define %s %s\n"%(define,value))
+        fields=item.get('fields')
+        if fields is not None:
+            idx=0
+            for field in fields:
+                v=field.get('v')
+                if v is None:
+                    v=idx
+                else:
+                    v=int(v)
+                label=field.get('l')
+                if v is not None and label is not None:
+                    define=("GWXDRFIELD_%s_%s"%(cat,labelFilter(label))).upper();
+                    oh.write("    #define %s %s\n"%(define,str(v)))
+                idx+=1
+
+class Grove:
+    def __init__(self,name) -> None:
+        self.name=name
+    def _ss(self,z=False):
+        if z:
+            return self.name
+        return self.name if self.name is not 'Z' else ''
+    def _suffix(self):
+        return '_'+self.name if self.name is not 'Z' else ''
+    def replace(self,line):
+        if line is None:
+            return line
+        return line.replace('$G$',self._ss()).replace('$Z$',self._ss(True)).replace('$GS$',self._suffix())
+def generateGroveDefs(inh,outh,inFile=''):
+    GROVES=[Grove('Z'),Grove('A'),Grove('B'),Grove('C')]
+    definition=[]
+    started=False
+    def writeConfig():
+        for grove in GROVES:        
+            for cl in definition:
+                outh.write(grove.replace(cl))
+
+    for line in inh:
+        if re.match(" *#GROVE",line):
+            started=True
+            if len(definition) > 0:
+                writeConfig()
+            definition=[]
+            continue
+        if started:
+            definition.append(line)
+    if len(definition) > 0:
+        writeConfig()
+
+
+
+userTaskDirs=[]
+
+def getUserTaskDirs():
+    rt=[]
+    taskdirs=glob.glob(os.path.join( basePath(),'lib','*task*'))
+    for task in taskdirs:
+        rt.append(task)
+    return rt
+
+def checkAndAdd(file,names,ilist):
+    if not file.endswith('.h'):
+        return
+    match=False
+    for cmp in names:
+        #print("##check %s<->%s"%(f.lower(),cmp))
+        if file.lower() == cmp:
+            match=True
+    if not match:
+        return
+    ilist.append(file) 
+def genereateUserTasks(outfile):
+    includes=[]
+    for task in userTaskDirs:
+        #print("##taskdir=%s"%task)
+        base=os.path.basename(task)
+        includeNames=[base.lower()+".h",'gw'+base.lower()+'.h']
+        for f in os.listdir(task):
+            checkAndAdd(f,includeNames,includes)
+    includeData=""
+    for i in includes:
+        print("#task include %s"%i)
+        includeData+="#include <%s>\n"%i
+    writeFileIfChanged(outfile,includeData)            
+
+def generateEmbedded(elist,outFile):
+    content=""
+    for entry in elist:
+        content+="EMBED_GZ_FILE(\"%s\",%s,\"%s\");\n"%entry
+    writeFileIfChanged(outFile,content)    
+
+def getContentType(fn):
+    if (fn.endswith('.gz')):
+        fn=fn[0:-3]
+    if (fn.endswith('html')):
+        return "text/html"
+    if (fn.endswith('json')):
+        return "application/json"
+    if (fn.endswith('js')):
+        return "text/javascript"
+    if (fn.endswith('css')):    
+        return "text/css"
+    return "application/octet-stream"
+
+
+def getLibs():
+    base=os.path.join(basePath(),"lib")
+    rt=[]
+    for sd in os.listdir(base):
+        if sd == '..':
+            continue
+        if sd == '.':
+            continue
+        fn=os.path.join(base,sd)
+        if os.path.isdir(fn):
+            rt.append(sd)
+    EXTRAS=['generated']
+    for e in EXTRAS:
+        if not e in rt:
+            rt.append(e)
+    return rt
+
+
+
+def joinFiles(target,pattern,dirlist):
+    flist=[]
+    for dir in dirlist:
+            fn=os.path.join(dir,pattern)
+            if os.path.exists(fn):
+                flist.append(fn)
+    current=False
+    if os.path.exists(target):
+        current=True
+        for f in flist:
+            if not isCurrent(f,target):
+                current=False
+                break
+    if current:
+        print("%s is up to date"%target)
+        return
+    print("creating %s"%target)
+    with gzip.open(target,"wb") as oh:
+        for fn in flist:
+            print("adding %s to %s"%(fn,target))
+            with open(fn,"rb") as rh:
+                shutil.copyfileobj(rh,oh)
+    
+
+OWNLIBS=getLibs()+["FS","WiFi"]
+GLOBAL_INCLUDES=[]
+
+def handleDeps(env):
+    #overwrite the GetProjectConfig
+    #to inject all our libs
+    oldGetProjectConfig=env.GetProjectConfig    
+    def GetProjectConfigX(env):        
+        rt=oldGetProjectConfig()
+        cenv="env:"+env['PIOENV']
+        libs=[]
+        for section,options in rt.as_tuple():
+            if section == cenv:
+                for key,values in options:
+                    if key == 'lib_deps':
+                        libs=values
+    
+        mustUpdate=False
+        for lib in OWNLIBS:
+            if not lib in libs:
+                libs.append(lib)
+                mustUpdate=True
+        if mustUpdate:
+            update=[(cenv,[('lib_deps',libs)])]
+            rt.update(update)
+        return rt
+    env.AddMethod(GetProjectConfigX,"GetProjectConfig")
+    #store the list of all includes after we resolved
+    #the dependencies for our main project
+    #we will use them for all compilations afterwards
+    oldLibBuilder=env.ConfigureProjectLibBuilder
+    def ConfigureProjectLibBuilderX(env):
+        global GLOBAL_INCLUDES
+        project=oldLibBuilder()
+        #print("##ConfigureProjectLibBuilderX")
+        #pprint.pprint(project)
+        if project.depbuilders:
+            #print("##depbuilders %s"%",".join(map(lambda x: x.path,project.depbuilders)))
+            for db in project.depbuilders:
+                idirs=db.get_include_dirs()
+                for id in idirs:
+                    if not id in GLOBAL_INCLUDES:
+                        GLOBAL_INCLUDES.append(id)
+        return project
+    env.AddMethod(ConfigureProjectLibBuilderX,"ConfigureProjectLibBuilder")
+    def injectIncludes(env,node):
+        return env.Object(
+            node,
+            CPPPATH=env["CPPPATH"]+GLOBAL_INCLUDES
+        )
+    env.AddBuildMiddleware(injectIncludes)
+
+
+def prebuild(env):
+    global userTaskDirs
+    print("#prebuild running")
+    if not checkDir():
+        sys.exit(1)
+    ldf_mode=env.GetProjectOption("lib_ldf_mode")
+    if ldf_mode == 'off':
+        print("##ldf off - own dependency handling")
+        handleDeps(env)
+    userTaskDirs=getUserTaskDirs()
+    mergedConfig=os.path.join(outPath(),os.path.basename(CFG_FILE))
+    generateMergedConfig(os.path.join(basePath(),CFG_FILE),mergedConfig,userTaskDirs)
+    compressFile(mergedConfig,mergedConfig+".gz")
+    generateCfg(mergedConfig,os.path.join(outPath(),CFG_INCLUDE),False)
+    generateCfg(mergedConfig,os.path.join(outPath(),CFG_INCLUDE_IMPL),True)
+    joinFiles(os.path.join(outPath(),INDEXJS+".gz"),INDEXJS,["web"]+userTaskDirs)
+    joinFiles(os.path.join(outPath(),INDEXCSS+".gz"),INDEXCSS,["web"]+userTaskDirs)
+    embedded=getEmbeddedFiles(env)
+    filedefs=[]
+    for ef in embedded:
+        print("#checking embedded file %s"%ef)
+        (dn,fn)=os.path.split(ef)
+        pureName=fn
+        if pureName.endswith('.gz'):
+            pureName=pureName[0:-3]
+        ct=getContentType(pureName)
+        usname=ef.replace('/','_').replace('.','_')
+        filedefs.append((pureName,usname,ct))
+        inFile=os.path.join(basePath(),"web",pureName)
+        if os.path.exists(inFile):
+            compressFile(inFile,ef)
+        else:
+            print("#WARNING: infile %s for %s not found"%(inFile,ef))
+    generateEmbedded(filedefs,os.path.join(outPath(),EMBEDDED_INCLUDE))
+    genereateUserTasks(os.path.join(outPath(), TASK_INCLUDE))
+    generateFile(os.path.join(basePath(),XDR_FILE),os.path.join(outPath(),XDR_INCLUDE),generateXdrMappings)
+    generateFile(os.path.join(basePath(),GROVE_CONFIG_IN),os.path.join(outPath(),GROVE_CONFIG),generateGroveDefs,inMode='r')
+    version="dev"+datetime.now().strftime("%Y%m%d")
+    env.Append(CPPDEFINES=[('GWDEVVERSION',version)])
+
+def cleangenerated(source, target, env):
+    od=outPath()
+    if os.path.isdir(od):
+        print("#cleaning up %s"%od)
+        for f in os.listdir(od):
+            if f == "." or f == "..":
+                continue
+            fn=os.path.join(od,f)
+            os.unlink(f)
+
+
+print("#prescript...")
+prebuild(env)
+board="PLATFORM_BOARD_%s"%env["BOARD"].replace("-","_").upper()
+print("Board=#%s#"%board)
+print("BuildFlags=%s"%(" ".join(env["BUILD_FLAGS"])))
+env.Append(
+    LINKFLAGS=[ "-u", "custom_app_desc" ],
+    CPPDEFINES=[(board,"1")]
+)
+#script does not run on clean yet - maybe in the future
+env.AddPostAction("clean",cleangenerated)
diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h
index b6f653f..5e0d953 100644
--- a/lib/obp60task/OBP60Extensions.h
+++ b/lib/obp60task/OBP60Extensions.h
@@ -100,8 +100,8 @@ void displayFooter(CommonData &commonData);
 SunData calcSunsetSunrise(GwApi *api, double time, double date, double latitude, double longitude, double timezone); // Calulate sunset and sunrise
 
 void batteryGraphic(uint x, uint y, float percent, int pcolor, int bcolor); // Battery graphic with fill level
-void solarGraphic(uint x, uint y, int pcolor, int bcolor);                  // Solar graphic with fill level
-void generatorGraphic(uint x, uint y, int pcolor, int bcolor);              // Generator graphic with fill level
+void solarGraphic(uint x, uint y, int pcolor, int bcolor);                  // Solar graphic
+void generatorGraphic(uint x, uint y, int pcolor, int bcolor);              // Generator graphic
 void startLedTask(GwApi *api);
 
 void doImageRequest(GwApi *api, int *pageno, const PageStruct pages[MAX_PAGE_NUMBER], AsyncWebServerRequest *request);
diff --git a/lib/obp60task/OBPSensorTask.cpp b/lib/obp60task/OBPSensorTask.cpp
index cd488ab..9fb6d90 100644
--- a/lib/obp60task/OBPSensorTask.cpp
+++ b/lib/obp60task/OBPSensorTask.cpp
@@ -89,11 +89,11 @@ void sensorTask(void *param){
     double vslope = (api->getConfig()->getConfigItem(api->getConfig()->vSlope,true)->asString()).toFloat();
     if(String(powsensor1) == "off"){
         #ifdef VOLTAGE_SENSOR
-        sensors.batteryVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.53) * 2;   // Vin = 1/2 for OBP40
+        float rawVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.53) * 2;   // Vin = 1/2 for OBP40
         #else
-        sensors.batteryVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.17) * 20;   // Vin = 1/20 for OBP60    
+        float rawVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.17) * 20;   // Vin = 1/20 for OBP60    
         #endif
-        sensors.batteryVoltage = sensors.batteryVoltage * vslope + voffset; // Calibration
+        sensors.batteryVoltage = rawVoltage * vslope + voffset; // Calibration
         #ifdef LIPO_ACCU_1200
         sensors.BatteryChargeStatus = 0;    // Set to discharging
         sensors.batteryLevelLiPo = 0;       // Level 0...100%
@@ -468,20 +468,14 @@ void sensorTask(void *param){
         if(millis() > starttime5 + 1000 && String(powsensor1) == "off"){
             starttime5 = millis();
             #ifdef VOLTAGE_SENSOR
-            sensors.batteryVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.53) * 2;   // Vin = 1/2 for OBP40
+            float rawVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.53) * 2;   // Vin = 1/2 for OBP40
             #else
-            sensors.batteryVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.17) * 20;   // Vin = 1/20 for OBP60    
+            float rawVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.17) * 20;   // Vin = 1/20 for OBP60    
             #endif
-            sensors.batteryVoltage = sensors.batteryVoltage * vslope + voffset; // Calibration
+            sensors.batteryVoltage = rawVoltage * vslope + voffset; // Calibration
             #ifdef LIPO_ACCU_1200
-            if(sensors.batteryVoltage > 4.1){
-                sensors.BatteryChargeStatus = 1;    // Charging active
-            }
-            else{
-                sensors.BatteryChargeStatus = 0;    // Discharging
-            }
             // Polynomfit for LiPo capacity calculation for 3,7V LiPo accus, 0...100%
-            sensors.batteryLevelLiPo = sensors.batteryVoltage * sensors.batteryVoltage * 174.9513 + sensors.batteryVoltage * 1147,7686 + 1868.5120;
+            sensors.batteryLevelLiPo = sensors.batteryVoltage * sensors.batteryVoltage * 174.9513 + sensors.batteryVoltage * 1147.7686 + 1868.5120;
             // Limiter
             if(sensors.batteryLevelLiPo > 100){
                 sensors.batteryLevelLiPo = 100;
@@ -496,11 +490,28 @@ void sensorTask(void *param){
             sensors.batteryVoltage10 = batV.getAvg(10) / 100.0;
             sensors.batteryVoltage60 = batV.getAvg(60) / 100.0;
             sensors.batteryVoltage300 = batV.getAvg(300) / 100.0;
+            // Charging detection
+            float deltaV = sensors.batteryVoltage - sensors.batteryVoltage10;
+            if(deltaV > 0.03){
+                sensors.BatteryChargeStatus = 1;    // Charging active
+            }
+            if(deltaV < -0.03){
+                sensors.BatteryChargeStatus = 0;    // Discharging
+            }
+            #ifdef BOARD_OBP40S3
+            // Send to NMEA200 bus as instance 10
+            if(!isnan(sensors.batteryVoltage)){
+                SetN2kDCBatStatus(N2kMsg, 10, sensors.batteryVoltage, N2kDoubleNA, N2kDoubleNA, 0);
+                api->sendN2kMessage(N2kMsg);
+            }
+            #endif
+            #ifdef BOARD_OBP60S3
             // Send to NMEA200 bus
             if(!isnan(sensors.batteryVoltage)){
                 SetN2kDCBatStatus(N2kMsg, 0, sensors.batteryVoltage, N2kDoubleNA, N2kDoubleNA, 1);
                 api->sendN2kMessage(N2kMsg);
             }
+            #endif
         }
 
         // Send data from environment sensor all 2s
diff --git a/lib/obp60task/PageVoltage.cpp b/lib/obp60task/PageVoltage.cpp
index d05c903..fe1070b 100644
--- a/lib/obp60task/PageVoltage.cpp
+++ b/lib/obp60task/PageVoltage.cpp
@@ -205,6 +205,18 @@ public:
             getdisplay().setCursor(20, 100);
             getdisplay().print(name1);                           // Value name
 
+            #ifdef BOARD_OBP40S3
+            // Show charge status
+            getdisplay().setFont(&Ubuntu_Bold8pt7b);
+            getdisplay().setCursor(185, 100);
+            if(commonData->data.BatteryChargeStatus == true){
+                getdisplay().print("Charge");
+            }
+            else{
+                getdisplay().print("Discharge");
+            }
+            #endif
+
             // Show unit
             getdisplay().setFont(&Ubuntu_Bold20pt7b);
             getdisplay().setCursor(270, 100);
@@ -213,7 +225,12 @@ public:
             // Show battery type
             getdisplay().setFont(&Ubuntu_Bold8pt7b);
             getdisplay().setCursor(295, 100);
+            #ifdef BOARD_OBP60S3
             getdisplay().print(batType);
+            #endif
+            #ifdef BOARD_OBP40S3
+            getdisplay().print("LiPo");
+            #endif
 
             // Show average settings
             printAvg(average, 320, 84, true);
diff --git a/lib/obp60task/config_obp40.json b/lib/obp60task/config_obp40.json
index 3a4b6bf..888c746 100644
--- a/lib/obp60task/config_obp40.json
+++ b/lib/obp60task/config_obp40.json
@@ -609,10 +609,10 @@
         "label": "Undervoltage",
         "type": "boolean",
         "default": "false",
-        "description": "Switch off device if voltage drops below 9V [on|off]",
+        "description": "Switch off device if LiPo voltage drops below 3.65V [on|off]",
         "category": "OBP40 Hardware",
         "capabilities": {
-            "obp40": "false"
+            "obp40":"true"
         }
     },
     {
diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp
index 51828e4..cad4867 100644
--- a/lib/obp60task/obp60task.cpp
+++ b/lib/obp60task/obp60task.cpp
@@ -309,30 +309,28 @@ void registerAllPages(PageList &list){
 
 // Undervoltage detection for shutdown display
 void underVoltageDetection(GwApi *api, CommonData &common){
-    float actVoltage = 0;
-    float minVoltage = 0;
     // Read settings
     float vslope = uint(api->getConfig()->getConfigItem(api->getConfig()->vSlope,true)->asFloat());
     float voffset = uint(api->getConfig()->getConfigItem(api->getConfig()->vOffset,true)->asFloat());
     // Read supply voltage
     #if defined VOLTAGE_SENSOR && defined LIPO_ACCU_1200
-    actVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.53) * 2;   // Vin = 1/2 for OBP40
-    minVoltage = 3.65;  // Absolut minimum volatge for 3,7V LiPo accu
+    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
     #else
-    actVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.17) * 20;   // Vin = 1/20 for OBP60
-    minVoltage = MIN_VOLTAGE;
+    float actVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.17) * 20;   // Vin = 1/20 for OBP60
+    float minVoltage = MIN_VOLTAGE;
     #endif
-    actVoltage = actVoltage * vslope + voffset;
-    if(actVoltage < minVoltage){
+    float corrVoltage = actVoltage * vslope + voffset;  // Calibration
+    if(corrVoltage < minVoltage){
         #if defined VOLTAGE_SENSOR && defined LIPO_ACCU_1200
         // Switch off all power lines
         setPortPin(OBP_BACKLIGHT_LED, false);   // Backlight Off
         setFlashLED(false);                     // Flash LED Off            
         buzzer(TONE4, 20);                      // Buzzer tone 4kHz 20ms
         // Shutdown EInk display
-        getdisplay().setFullWindow();               // Set full Refresh
+        getdisplay().setFullWindow();           // Set full Refresh
         //getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update
-        getdisplay().fillScreen(common.bgcolor);    // Clear screen
+        getdisplay().fillScreen(common.bgcolor);// Clear screen
         getdisplay().setTextColor(common.fgcolor);
         getdisplay().setFont(&Ubuntu_Bold20pt7b);
         getdisplay().setCursor(65, 150);

From 9dc857056b1d1dd1762e3e92178aba14144346a5 Mon Sep 17 00:00:00 2001
From: norbert-walter <norbert-walter@web.de>
Date: Sat, 25 Jan 2025 18:04:58 +0100
Subject: [PATCH 17/17] Fix LiPo battery level and sensor pad

---
 lib/obp60task/OBP60Keypad.h     |  4 ++--
 lib/obp60task/OBPSensorTask.cpp | 22 ++++++++++------------
 lib/obp60task/PageVoltage.cpp   |  4 ++--
 3 files changed, 14 insertions(+), 16 deletions(-)

diff --git a/lib/obp60task/OBP60Keypad.h b/lib/obp60task/OBP60Keypad.h
index 937f44e..eafe3a2 100644
--- a/lib/obp60task/OBP60Keypad.h
+++ b/lib/obp60task/OBP60Keypad.h
@@ -277,8 +277,8 @@ void initKeys(CommonData &commonData) {
         starttime = millis();   // Start key pressed
         keycodeold = keycode;
       }
-      // If key pressed longer than 200ms
-      if(millis() > starttime + 200 && keycode == keycodeold) {
+      // If key pressed longer than 100ms
+      if(millis() > starttime + 100 && keycode == keycodeold) {
         if (use_syspage and keycode == 3) {
             keystatus = 12;
         } else {
diff --git a/lib/obp60task/OBPSensorTask.cpp b/lib/obp60task/OBPSensorTask.cpp
index 9fb6d90..cbbfa15 100644
--- a/lib/obp60task/OBPSensorTask.cpp
+++ b/lib/obp60task/OBPSensorTask.cpp
@@ -473,9 +473,15 @@ void sensorTask(void *param){
             float rawVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.17) * 20;   // Vin = 1/20 for OBP60    
             #endif
             sensors.batteryVoltage = rawVoltage * vslope + voffset; // Calibration
-            #ifdef LIPO_ACCU_1200
+            // Save new data in average array
+            batV.reading(int(sensors.batteryVoltage * 100));
+            // Calculate the average values for different time lines from integer values
+            sensors.batteryVoltage10 = batV.getAvg(10) / 100.0;
+            sensors.batteryVoltage60 = batV.getAvg(60) / 100.0;
+            sensors.batteryVoltage300 = batV.getAvg(300) / 100.0;
+            #if defined LIPO_ACCU_1200 && defined VOLTAGE_SENSOR
             // Polynomfit for LiPo capacity calculation for 3,7V LiPo accus, 0...100%
-            sensors.batteryLevelLiPo = sensors.batteryVoltage * sensors.batteryVoltage * 174.9513 + sensors.batteryVoltage * 1147.7686 + 1868.5120;
+            sensors.batteryLevelLiPo = sensors.batteryVoltage60 * 203.8312 -738.1635;
             // Limiter
             if(sensors.batteryLevelLiPo > 100){
                 sensors.batteryLevelLiPo = 100;
@@ -483,22 +489,14 @@ void sensorTask(void *param){
             if(sensors.batteryLevelLiPo < 0){
                 sensors.batteryLevelLiPo = 0;
             }
-            #endif
-            // Save new data in average array
-            batV.reading(int(sensors.batteryVoltage * 100));
-            // Calculate the average values for different time lines from integer values
-            sensors.batteryVoltage10 = batV.getAvg(10) / 100.0;
-            sensors.batteryVoltage60 = batV.getAvg(60) / 100.0;
-            sensors.batteryVoltage300 = batV.getAvg(300) / 100.0;
             // Charging detection
             float deltaV = sensors.batteryVoltage - sensors.batteryVoltage10;
-            if(deltaV > 0.03){
+            if(deltaV > 0.045){
                 sensors.BatteryChargeStatus = 1;    // Charging active
             }
-            if(deltaV < -0.03){
+            if(deltaV < -0.04){
                 sensors.BatteryChargeStatus = 0;    // Discharging
             }
-            #ifdef BOARD_OBP40S3
             // Send to NMEA200 bus as instance 10
             if(!isnan(sensors.batteryVoltage)){
                 SetN2kDCBatStatus(N2kMsg, 10, sensors.batteryVoltage, N2kDoubleNA, N2kDoubleNA, 0);
diff --git a/lib/obp60task/PageVoltage.cpp b/lib/obp60task/PageVoltage.cpp
index fe1070b..c98bc96 100644
--- a/lib/obp60task/PageVoltage.cpp
+++ b/lib/obp60task/PageVoltage.cpp
@@ -205,7 +205,7 @@ public:
             getdisplay().setCursor(20, 100);
             getdisplay().print(name1);                           // Value name
 
-            #ifdef BOARD_OBP40S3
+            #if defined BOARD_OBP40S3 && defined LIPO_ACCU_1200 && defined VOLTAGE_SENSOR
             // Show charge status
             getdisplay().setFont(&Ubuntu_Bold8pt7b);
             getdisplay().setCursor(185, 100);
@@ -228,7 +228,7 @@ public:
             #ifdef BOARD_OBP60S3
             getdisplay().print(batType);
             #endif
-            #ifdef BOARD_OBP40S3
+            #if defined BOARD_OBP40S3 && defined LIPO_ACCU_1200 && defined VOLTAGE_SENSOR
             getdisplay().print("LiPo");
             #endif