From 54d6d51519a65c8cab6af187a8286d85b5ded2a1 Mon Sep 17 00:00:00 2001
From: quantenschaum <woxpox@posteo.de>
Date: Wed, 21 Aug 2024 14:57:36 +0200
Subject: [PATCH 01/58] tried to fix wind conversion, disabled wind calculation

---
 lib/boatData/GwBoatData.h               | 53 +++++++++++++------------
 lib/nmea0183ton2k/NMEA0183DataToN2K.cpp | 37 ++++++++---------
 lib/nmea2kto0183/N2kDataToNMEA0183.cpp  | 41 +++++++++++--------
 3 files changed, 69 insertions(+), 62 deletions(-)

diff --git a/lib/boatData/GwBoatData.h b/lib/boatData/GwBoatData.h
index 6d3ab19..fd57a87 100644
--- a/lib/boatData/GwBoatData.h
+++ b/lib/boatData/GwBoatData.h
@@ -175,41 +175,42 @@ class GwBoatData{
         GwBoatItemBase::GwBoatItemMap values;
     public:
 
-    GWBOATDATA(double,COG,4000,formatCourse)
-    GWBOATDATA(double,TWD,4000,formatCourse)
-    GWBOATDATA(double,SOG,4000,formatKnots)
-    GWBOATDATA(double,STW,4000,formatKnots)
-    GWBOATDATA(double,TWS,4000,formatKnots)
-    GWBOATDATA(double,AWS,4000,formatKnots)
-    GWBOATDATA(double,MaxTws,0,formatKnots)
+    GWBOATDATA(double,COG,4000,formatCourse) // course over ground
+    GWBOATDATA(double,SOG,4000,formatKnots) // speed over ground
+    GWBOATDATA(double,HDG,4000,formatCourse) // true heading
+    GWBOATDATA(double,MHDG,4000,formatCourse) // magnetic heading
+    GWBOATDATA(double,STW,4000,formatKnots) // water speed
+    GWBOATDATA(double,VAR,4000,formatCourse) // variation
+    GWBOATDATA(double,DEV,4000,formatCourse) // deviation
+    GWBOATDATA(double,AWA,4000,formatWind) // apparent wind ANGLE
+    GWBOATDATA(double,AWS,4000,formatKnots) // apparent wind speed
     GWBOATDATA(double,MaxAws,0,formatKnots)
-    GWBOATDATA(double,AWA,4000,formatWind)
-    GWBOATDATA(double,HDG,4000,formatCourse) //true heading
-    GWBOATDATA(double,MHDG,4000,formatCourse) //magnetic heading
-    GWBOATDATA(double,ROT,4000,formatRot)
-    GWBOATDATA(double,VAR,4000,formatCourse) //Variation
-    GWBOATDATA(double,DEV,4000,formatCourse) //Deviation
+    GWBOATDATA(double,TWD,4000,formatCourse) // true wind DIRECTION
+    GWBOATDATA(double,TWA,4000,formatCourse) // true wind ANGLE
+    GWBOATDATA(double,TWS,4000,formatKnots) // true wind speed
+    GWBOATDATA(double,MaxTws,0,formatKnots)
+    GWBOATDATA(double,ROT,4000,formatRot) // rate of turn
+    GWBOATDATA(double,RPOS,4000,formatWind) // rudder position
+    GWBOATDATA(double,PRPOS,4000,formatWind) // secondary rudder position
+    GWBOATDATA(double,LAT,4000,formatLatitude) 
+    GWBOATDATA(double,LON,4000,formatLongitude)
+    GWBOATDATA(double,ALT,4000,formatFixed0) //altitude
     GWBOATDATA(double,HDOP,4000,formatDop)
     GWBOATDATA(double,PDOP,4000,formatDop)
     GWBOATDATA(double,VDOP,4000,formatDop)
-    GWBOATDATA(double,RPOS,4000,formatWind) //RudderPosition
-    GWBOATDATA(double,PRPOS,4000,formatWind) //second rudder pos
-    GWBOATDATA(double,LAT,4000,formatLatitude)
-    GWBOATDATA(double,LON,4000,formatLongitude)
-    GWBOATDATA(double,ALT,4000,formatFixed0) //altitude
     GWBOATDATA(double,DBS,4000,formatDepth) //waterDepth (below surface)
     GWBOATDATA(double,DBT,4000,formatDepth) //DepthTransducer
-    GWBOATDATA(double,GPST,4000,formatTime) //GpsTime
+    GWBOATDATA(double,GPST,4000,formatTime) // GPS time (seconds of day)
+    GWBOATDATA(uint32_t,GPSD,4000,formatDate) // GPS date (days since 1979-01-01)
+    GWBOATDATA(int16_t,TZ,8000,formatFixed0)
     GWBOATDATA(double,WTemp,4000,kelvinToC)
-    GWBOATDATA(double,XTE,4000,formatXte)
-    GWBOATDATA(double,DTW,4000,mtr2nm) //distance wp
-    GWBOATDATA(double,BTW,4000,formatCourse) //bearing wp
-    GWBOATDATA(double,WPLat,4000,formatLatitude)
-    GWBOATDATA(double,WPLon,4000,formatLongitude)
     GWBOATDATA(uint32_t,Log,16000,mtr2nm)
     GWBOATDATA(uint32_t,TripLog,16000,mtr2nm)
-    GWBOATDATA(uint32_t,GPSD,4000,formatDate) //Date
-    GWBOATDATA(int16_t,TZ,8000,formatFixed0)
+    GWBOATDATA(double,DTW,4000,mtr2nm) // distance to waypoint
+    GWBOATDATA(double,BTW,4000,formatCourse) // bearing to waypoint
+    GWBOATDATA(double,XTE,4000,formatXte) // cross track error
+    GWBOATDATA(double,WPLat,4000,formatLatitude) // waypoint latitude
+    GWBOATDATA(double,WPLon,4000,formatLongitude) // waypoint longitude
     GWSPECBOATDATA(GwBoatDataSatList,SatInfo,GwSatInfoList::lifeTime,formatFixed0);
     public:
         GwBoatData(GwLog *logger);
diff --git a/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp b/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp
index bfed8fd..8477ab8 100644
--- a/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp
+++ b/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp
@@ -399,20 +399,20 @@ private:
             return;
         }
         tN2kMsg n2kMsg;
-        tN2kWindReference n2kRef;
+        tN2kWindReference n2kRef; 
         bool shouldSend=false;
         WindAngle=formatDegToRad(WindAngle);
         switch(Reference){
             case NMEA0183Wind_Apparent:
                 n2kRef=N2kWind_Apparent;
                 shouldSend=updateDouble(boatData->AWA,WindAngle,msg.sourceId) && 
-                    updateDouble(boatData->AWS,WindSpeed,msg.sourceId);
+                           updateDouble(boatData->AWS,WindSpeed,msg.sourceId);
                 if (WindSpeed != NMEA0183DoubleNA) boatData->MaxAws->updateMax(WindSpeed);    
                 break;
             case NMEA0183Wind_True:
-                n2kRef=N2kWind_True_North;
-                shouldSend=updateDouble(boatData->TWD,WindAngle,msg.sourceId) && 
-                    updateDouble(boatData->TWS,WindSpeed,msg.sourceId);
+                n2kRef=N2kWind_True_water;
+                shouldSend=updateDouble(boatData->TWA,WindAngle,msg.sourceId) && 
+                           updateDouble(boatData->TWS,WindSpeed,msg.sourceId);
                 if (WindSpeed != NMEA0183DoubleNA) boatData->MaxTws->updateMax(WindSpeed);    
                 break;      
             default:
@@ -467,8 +467,7 @@ private:
 
     void convertMWD(const SNMEA0183Msg &msg)
     {
-        double WindAngle = NMEA0183DoubleNA, WindAngleMagnetic=NMEA0183DoubleNA,
-            WindSpeed = NMEA0183DoubleNA;
+        double WindDirection = NMEA0183DoubleNA, WindDirectionMagnetic=NMEA0183DoubleNA, WindSpeed = NMEA0183DoubleNA;
         if (msg.FieldCount() < 8 )
         {
             LOG_DEBUG(GwLog::DEBUG, "failed to parse MWD %s", msg.line);
@@ -476,11 +475,11 @@ private:
         }
         if (msg.FieldLen(0) > 0 && msg.Field(1)[0] == 'T')
         {
-            WindAngle = formatDegToRad(atof(msg.Field(0)));
+            WindDirection = formatDegToRad(atof(msg.Field(0)));
         }
         if (msg.FieldLen(2) > 0 && msg.Field(3)[0] == 'M')
         {
-            WindAngleMagnetic = formatDegToRad(atof(msg.Field(2)));
+            WindDirectionMagnetic = formatDegToRad(atof(msg.Field(2)));
         }
         if (msg.FieldLen(4) > 0 && msg.Field(5)[0] == 'N')
         {
@@ -497,19 +496,17 @@ private:
         }
         tN2kMsg n2kMsg;
         bool shouldSend = false;
-        if (WindAngle != NMEA0183DoubleNA){
-            shouldSend = updateDouble(boatData->TWD, WindAngle, msg.sourceId) &&
+        if (WindDirection != NMEA0183DoubleNA){
+            shouldSend = updateDouble(boatData->TWD, WindDirection, msg.sourceId) &&
                          updateDouble(boatData->TWS, WindSpeed, msg.sourceId);
             if (WindSpeed != NMEA0183DoubleNA) boatData->MaxTws->updateMax(WindSpeed);             
-        }
-        if (shouldSend)
-        {
-            SetN2kWindSpeed(n2kMsg, 1, WindSpeed, WindAngle, N2kWind_True_North);
-            send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((int)N2kWind_True_North));
-        }
-        if (WindAngleMagnetic != NMEA0183DoubleNA && shouldSend){
-            SetN2kWindSpeed(n2kMsg, 1, WindSpeed, WindAngleMagnetic, N2kWind_Magnetic);
-            send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((int)N2kWind_Magnetic));
+            if(shouldSend && boatData->HDG->isValid()) {
+                double twa = WindDirection-boatData->HDG->getData();
+                if(twa<0) { twa+=2*M_PI; }
+                updateDouble(boatData->TWA, twa, msg.sourceId);
+                SetN2kWindSpeed(n2kMsg, 1, WindSpeed, twa, N2kWind_True_water);
+                send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((int)N2kWind_True_water));
+            }
         }
     }
 
diff --git a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp
index 26c6811..b08ac7d 100644
--- a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp
+++ b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp
@@ -468,37 +468,46 @@ private:
     {
         unsigned char SID;
         tN2kWindReference WindReference;
-        tNMEA0183WindReference NMEA0183Reference = NMEA0183Wind_True;
-
-        double x, y;
         double WindAngle=N2kDoubleNA, WindSpeed=N2kDoubleNA;
 
-        if (ParseN2kWindSpeed(N2kMsg, SID, WindSpeed, WindAngle, WindReference))
-        {
+        if (ParseN2kWindSpeed(N2kMsg, SID, WindSpeed, WindAngle, WindReference)) {
             tNMEA0183Msg NMEA0183Msg;
+            tNMEA0183WindReference NMEA0183Reference;
+            bool shouldSend = false;
 
-            if (WindReference == N2kWind_Apparent)
-            {
+            // MWV sentence contains apparent/true ANGLE and SPEED
+            // https://gpsd.gitlab.io/gpsd/NMEA.html#_mwv_wind_speed_and_angle
+            // https://docs.vaisala.com/r/M211109EN-L/en-US/GUID-7402DEF8-5E82-446F-B63E-998F49F3D743/GUID-C77934C7-2A72-466E-BC52-CE6B8CC7ACB6
+
+            if (WindReference == N2kWind_Apparent) {
                 NMEA0183Reference = NMEA0183Wind_Apparent;
                 updateDouble(boatData->AWA, WindAngle);
                 updateDouble(boatData->AWS, WindSpeed);
                 setMax(boatData->MaxAws, boatData->AWS);
-            }
-            if (WindReference == N2kWind_True_North)
-            {
+                shouldSend = true;
+            } 
+            if (WindReference == N2kWind_True_water) {
                 NMEA0183Reference = NMEA0183Wind_True;
-                updateDouble(boatData->TWD, WindAngle);
+                updateDouble(boatData->TWA, WindAngle);
                 updateDouble(boatData->TWS, WindSpeed);
+                setMax(boatData->MaxTws, boatData->TWS);
+                shouldSend = true;
+                if (boatData->HDG->isValid()) {
+                    double twd = WindAngle+boatData->HDG->getData();
+                    if (twd>2*M_PI) { twd-=2*M_PI; }
+                    updateDouble(boatData->TWD, twd);
+                }
             }
 
-            if (NMEA0183SetMWV(NMEA0183Msg, formatCourse(WindAngle), NMEA0183Reference, WindSpeed,talkerId))
+            if (shouldSend && NMEA0183SetMWV(NMEA0183Msg, formatCourse(WindAngle), NMEA0183Reference, WindSpeed, talkerId)) {
                 SendMessage(NMEA0183Msg);
+            }
 
-            if (WindReference == N2kWind_Apparent && boatData->SOG->isValid())
+            /* if (WindReference == N2kWind_Apparent && boatData->SOG->isValid())
             { // Lets calculate and send TWS/TWA if SOG is available
 
-                x = WindSpeed * cos(WindAngle);
-                y = WindSpeed * sin(WindAngle);
+                double x = WindSpeed * cos(WindAngle);
+                double y = WindSpeed * sin(WindAngle);
 
                 updateDouble(boatData->TWD, atan2(y, -boatData->SOG->getData() + x));
                 updateDouble(boatData->TWS, sqrt((y * y) + ((-boatData->SOG->getData() + x) * (-boatData->SOG->getData() + x))));
@@ -534,7 +543,7 @@ private:
                     return;
 
                 SendMessage(NMEA0183Msg);
-            }
+            } */
         }
     }
     //*****************************************************************************

From 1fa10497594d4155fc4b345b6443f870c248bf68 Mon Sep 17 00:00:00 2001
From: quantenschaum <woxpox@posteo.de>
Date: Thu, 22 Aug 2024 15:16:23 +0200
Subject: [PATCH 02/58] renamed HDG->HDT, MHDG->HDM

---
 lib/boatData/GwBoatData.h               |  4 +--
 lib/nmea0183ton2k/NMEA0183DataToN2K.cpp | 35 +++++++++++++------------
 lib/nmea2kto0183/N2kDataToNMEA0183.cpp  | 16 +++++------
 3 files changed, 28 insertions(+), 27 deletions(-)

diff --git a/lib/boatData/GwBoatData.h b/lib/boatData/GwBoatData.h
index fd57a87..b4df0ec 100644
--- a/lib/boatData/GwBoatData.h
+++ b/lib/boatData/GwBoatData.h
@@ -177,8 +177,8 @@ class GwBoatData{
 
     GWBOATDATA(double,COG,4000,formatCourse) // course over ground
     GWBOATDATA(double,SOG,4000,formatKnots) // speed over ground
-    GWBOATDATA(double,HDG,4000,formatCourse) // true heading
-    GWBOATDATA(double,MHDG,4000,formatCourse) // magnetic heading
+    GWBOATDATA(double,HDT,4000,formatCourse) // true heading
+    GWBOATDATA(double,HDM,4000,formatCourse) // magnetic heading
     GWBOATDATA(double,STW,4000,formatKnots) // water speed
     GWBOATDATA(double,VAR,4000,formatCourse) // variation
     GWBOATDATA(double,DEV,4000,formatCourse) // deviation
diff --git a/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp b/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp
index 8477ab8..7868eaf 100644
--- a/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp
+++ b/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp
@@ -500,8 +500,8 @@ private:
             shouldSend = updateDouble(boatData->TWD, WindDirection, msg.sourceId) &&
                          updateDouble(boatData->TWS, WindSpeed, msg.sourceId);
             if (WindSpeed != NMEA0183DoubleNA) boatData->MaxTws->updateMax(WindSpeed);             
-            if(shouldSend && boatData->HDG->isValid()) {
-                double twa = WindDirection-boatData->HDG->getData();
+            if(shouldSend && boatData->HDT->isValid()) {
+                double twa = WindDirection-boatData->HDT->getData();
                 if(twa<0) { twa+=2*M_PI; }
                 updateDouble(boatData->TWA, twa, msg.sourceId);
                 SetN2kWindSpeed(n2kMsg, 1, WindSpeed, twa, N2kWind_True_water);
@@ -511,15 +511,15 @@ private:
     }
 
     void convertHDM(const SNMEA0183Msg &msg){
-        double MHDG=NMEA0183DoubleNA;
-        if (!NMEA0183ParseHDM_nc(msg, MHDG))
+        double HDM=NMEA0183DoubleNA;
+        if (!NMEA0183ParseHDM_nc(msg, HDM))
         {
             LOG_DEBUG(GwLog::DEBUG, "failed to parse HDM %s", msg.line);
             return;
         }
-        if (! UD(MHDG)) return;
+        if (! UD(HDM)) return;
         tN2kMsg n2kMsg;
-        SetN2kMagneticHeading(n2kMsg,1,MHDG,
+        SetN2kMagneticHeading(n2kMsg,1,HDM,
             boatData->VAR->getDataWithDefault(N2kDoubleNA),
             boatData->DEV->getDataWithDefault(N2kDoubleNA)
         );
@@ -527,28 +527,29 @@ private:
     }
     
     void convertHDT(const SNMEA0183Msg &msg){
-        double HDG=NMEA0183DoubleNA;
-        if (!NMEA0183ParseHDT_nc(msg, HDG))
+        double HDT=NMEA0183DoubleNA;
+        if (!NMEA0183ParseHDT_nc(msg, HDT))
         {
             LOG_DEBUG(GwLog::DEBUG, "failed to parse HDT %s", msg.line);
             return;
         }
-        if (! UD(HDG)) return;
+        if (! UD(HDT)) return;
         tN2kMsg n2kMsg;
-        SetN2kTrueHeading(n2kMsg,1,HDG);
+        SetN2kTrueHeading(n2kMsg,1,HDT);
         send(n2kMsg,msg.sourceId);    
     }
+    
     void convertHDG(const SNMEA0183Msg &msg){
-        double MHDG=NMEA0183DoubleNA;
-        double VAR=NMEA0183DoubleNA;
+        double HDM=NMEA0183DoubleNA;
         double DEV=NMEA0183DoubleNA;
+        double VAR=NMEA0183DoubleNA;
         if (msg.FieldCount() < 5)
         {
             LOG_DEBUG(GwLog::DEBUG, "failed to parse HDG %s", msg.line);
             return;
         }
         if (msg.FieldLen(0)>0){
-            MHDG=formatDegToRad(atof(msg.Field(0)));
+            HDM=formatDegToRad(atof(msg.Field(0)));
         }
         else{
             return;
@@ -562,11 +563,11 @@ private:
             if (msg.Field(4)[0] == 'W') VAR=-VAR;
         }
 
-        if (! UD(MHDG)) return;
+        if (! UD(HDM)) return;
         UD(VAR);
         UD(DEV);
         tN2kMsg n2kMsg;
-        SetN2kMagneticHeading(n2kMsg,1,MHDG,DEV,VAR);
+        SetN2kMagneticHeading(n2kMsg,1,HDM,DEV,VAR);
         send(n2kMsg,msg.sourceId,"127250M");    
     }
 
@@ -702,11 +703,11 @@ private:
             return;
         }
         tN2kMsg n2kMsg;
-        if (updateDouble(boatData->HDG,TrueHeading,msg.sourceId)){
+        if (updateDouble(boatData->HDT,TrueHeading,msg.sourceId)){
             SetN2kTrueHeading(n2kMsg,1,TrueHeading);
             send(n2kMsg,msg.sourceId); 
         }
-        if(updateDouble(boatData->MHDG,MagneticHeading,msg.sourceId)){
+        if(updateDouble(boatData->HDM,MagneticHeading,msg.sourceId)){
             SetN2kMagneticHeading(n2kMsg,1,MagneticHeading,
                 boatData->DEV->getDataWithDefault(N2kDoubleNA),
                 boatData->VAR->getDataWithDefault(N2kDoubleNA)
diff --git a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp
index b08ac7d..390d7f1 100644
--- a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp
+++ b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp
@@ -185,13 +185,13 @@ private:
             if (N2kIsNA(Variation)){
                 //no variation
                 if (ref == N2khr_magnetic){
-                    updateDouble(boatData->MHDG,Heading);
+                    updateDouble(boatData->HDM,Heading);
                     if (NMEA0183SetHDM(NMEA0183Msg,Heading,talkerId)){
                         SendMessage(NMEA0183Msg);
                     }    
                 }
                 if (ref == N2khr_true){
-                    updateDouble(boatData->HDG,Heading);
+                    updateDouble(boatData->HDT,Heading);
                     if (NMEA0183SetHDT(NMEA0183Msg,Heading,talkerId)){
                         SendMessage(NMEA0183Msg);
                     }
@@ -206,8 +206,8 @@ private:
                 if (ref == N2khr_true){
                     MagneticHeading=Heading-Variation;
                 }
-                updateDouble(boatData->MHDG,MagneticHeading);
-                updateDouble(boatData->HDG,Heading);
+                updateDouble(boatData->HDM,MagneticHeading);
+                updateDouble(boatData->HDT,Heading);
                 if (!N2kIsNA(MagneticHeading)){
                     if (NMEA0183SetHDG(NMEA0183Msg, MagneticHeading,_Deviation, 
                         Variation,talkerId))
@@ -252,8 +252,8 @@ private:
             tNMEA0183Msg NMEA0183Msg;
             updateDouble(boatData->STW, WaterReferenced);
             unsigned long now = millis();
-            double MagneticHeading = (boatData->HDG->isValid(now) && boatData->VAR->isValid(now)) ? boatData->HDG->getData() + boatData->VAR->getData() : NMEA0183DoubleNA;
-            if (NMEA0183SetVHW(NMEA0183Msg, boatData->HDG->getDataWithDefault(NMEA0183DoubleNA), MagneticHeading, WaterReferenced,talkerId))
+            double MagneticHeading = (boatData->HDT->isValid(now) && boatData->VAR->isValid(now)) ? boatData->HDT->getData() + boatData->VAR->getData() : NMEA0183DoubleNA;
+            if (NMEA0183SetVHW(NMEA0183Msg, boatData->HDT->getDataWithDefault(NMEA0183DoubleNA), MagneticHeading, WaterReferenced,talkerId))
             {
                 SendMessage(NMEA0183Msg);
             }
@@ -492,8 +492,8 @@ private:
                 updateDouble(boatData->TWS, WindSpeed);
                 setMax(boatData->MaxTws, boatData->TWS);
                 shouldSend = true;
-                if (boatData->HDG->isValid()) {
-                    double twd = WindAngle+boatData->HDG->getData();
+                if (boatData->HDT->isValid()) {
+                    double twd = WindAngle+boatData->HDT->getData();
                     if (twd>2*M_PI) { twd-=2*M_PI; }
                     updateDouble(boatData->TWD, twd);
                 }

From 26d1fd40eebfc08a77e1264f032131eff7d29d16 Mon Sep 17 00:00:00 2001
From: quantenschaum <woxpox@posteo.de>
Date: Thu, 22 Aug 2024 15:17:16 +0200
Subject: [PATCH 03/58] adjusted formatting

---
 lib/boatData/GwBoatData.h | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/lib/boatData/GwBoatData.h b/lib/boatData/GwBoatData.h
index b4df0ec..f5af8e8 100644
--- a/lib/boatData/GwBoatData.h
+++ b/lib/boatData/GwBoatData.h
@@ -180,13 +180,13 @@ class GwBoatData{
     GWBOATDATA(double,HDT,4000,formatCourse) // true heading
     GWBOATDATA(double,HDM,4000,formatCourse) // magnetic heading
     GWBOATDATA(double,STW,4000,formatKnots) // water speed
-    GWBOATDATA(double,VAR,4000,formatCourse) // variation
-    GWBOATDATA(double,DEV,4000,formatCourse) // deviation
+    GWBOATDATA(double,VAR,4000,formatWind) // variation
+    GWBOATDATA(double,DEV,4000,formatWind) // deviation
     GWBOATDATA(double,AWA,4000,formatWind) // apparent wind ANGLE
     GWBOATDATA(double,AWS,4000,formatKnots) // apparent wind speed
     GWBOATDATA(double,MaxAws,0,formatKnots)
     GWBOATDATA(double,TWD,4000,formatCourse) // true wind DIRECTION
-    GWBOATDATA(double,TWA,4000,formatCourse) // true wind ANGLE
+    GWBOATDATA(double,TWA,4000,formatWind) // true wind ANGLE
     GWBOATDATA(double,TWS,4000,formatKnots) // true wind speed
     GWBOATDATA(double,MaxTws,0,formatKnots)
     GWBOATDATA(double,ROT,4000,formatRot) // rate of turn

From 0696b2acb1d0b84f73cb3dbc6cad1adc28120a90 Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Fri, 6 Sep 2024 11:32:41 +0200
Subject: [PATCH 04/58] update flastool to 3.3.3 - esp32s3 support

---
 tools/flashtool.pyz        |  Bin 412602 -> 467869 bytes
 tools/flashtool/esptool.py | 2111 +++++++++++++++++++++++++++---------
 2 files changed, 1590 insertions(+), 521 deletions(-)

diff --git a/tools/flashtool.pyz b/tools/flashtool.pyz
index e8ceb142b189fa276e2c6105ddd64bdd775cb6bd..a3f96072c08ddc78470948229227a964052639e1 100644
GIT binary patch
delta 87969
zcmd4436vz)RUk@aFOsUI*3!FL>dvl4>MCVdWklrOa!bmL-1mJ`x66@{k(rSh8Ic(g
zxk_@{24fq8EtBA|!5-VpXC1&c4X$I$A21&S<N09m`2(2wUI8<nL1X*if%(Rme_upy
zRoz;Au>GlXx+^o{E%&~6_jli|kEv(g`v(tv=qC>tzV#n&yz{^xpFjG^pL$Q>_j2oZ
zx{v?B54extkS;!Zbjm$<@6<rz$1W=p&9FPHKv#ISdNnKcSe5va;lz|8{nIy+&gm0>
zcX0ar^ziimAam2_TL-643=dDAc;g+@XV(u-A0uv?Zd;B|AG2&t&l0Ai>2xA}#khW1
zR-_GfbNWZt^&3BA`JV^w{^pYpmwx!)oePv6KllUiZ~7+3=7mhsTio(<JWGk4k;K&-
zN|qfeSBxZ1Shq-=B&YxAs9s*XXw(`;IffNvb}Lk61%<0|taQZ~NJh8FZG20Vws=}$
zrRmj2cMTGI<L@55>+k`~w&lh*-M@06QAmffz8z0AK}B}l*{m;?%w~*NjTYmcpNmGJ
z*RHUVQLGsiuFc9uMKmhX$T*NVg%j#V+E}F(+F0R5rnR<~$t9DCbk^tHp@N|#8t+N<
z3(p!H>I3gEoV|S6c!`xe7P3KBScN7o?fp(JzBA;d>o<PR_siHFm#!FnnWTlxOkW>3
zF&zXRo!%c<J2~$^dl@7>zO0*=-Wxb}LVrch`{}QyU-uus69%2Nr^p+>6lfeExtehY
z;4#udRr|2L(-9@*%G#a<fG?<#{!YF7&w&E&jo2qzBYa4Zu&t!c9`USSo<Fj{BO5`k
zKC;3iBDT`ThPtgju)_oE>H`NnaIQXZqA~2VCljL)CU?Oj%bsD}@Wj4n91rS7?iq=K
zM*#E6;3U4kA4%NQkDTF(`pxmT+>F2FX8bMt@we>9-?ATn%YOVVd+=XJ_*-tm-?D=L
zjju=6vHOf0Xm>R>Fft9UBX6qT_}~HCA*I2}th)7~1Mca2Vt+Y(XZBZaNbw8Uag7*O
znj6!J*z|?$qth=Y9zI@WYetQLWy_8z?Fj4unCrb+|GCO&My8e*>F**rnG<A%78rIz
zygY9WgKru|$#{#Gl;y_1B&G+ap4^We_S0>SAKmz)+(qoDKN)cEWTJ_}jwg}GPQR2V
zj^{GI%ud=*ktE>&j`P#J%-*=GkihQVhx?5ui#_a~{UF}>OzEA6Di@6uERQkd1rBe}
zijkIBqaYf2QK$p=%e@Zb4Yq0oRAC7eQRFpx&&6+M`KQokYHYiev1}k#H(EB=pHV;X
zTI|e?H|#uPcyP{PGRxPtWu;fy;pCkvCu_2H`b)HhU__w`0!9>M<DkJY4Wq<%dmO+7
z8)rm4kQ}P8#(Af;>7RV))Qyvsj~tr#IZ0M78|$pHBeP5o<kV<~6KbN75!-FFBAjfL
zL0X~_s;t5?@CgPDc{LdapAelaguuoUby46)2!9<lYGto48waA)l8tPi6>c+f0vb^L
zJb&oKWOI8lQV1ky1Lm&L94|u;7H|SI1q1*Bt48zuR)uC-2r!AIt4O>e$l7$BV`g%A
zWjN)M46iyJ*c7y1M!CZ>paB>}vPQaEl~`Fmh(@0N()G;?%Z^zD#DE474ye+Cu_v=e
zQDCR>#+mb+wkrE4HP(PeS(<66ANaikC)Brp^1z#>m$^HSAc_Wpi=*CVF8yi{sQQ9x
zf^=JEC64B2pA}kC<N>ENHrAyYgGlwt`sQ9)yV`djO(XR?9dbW?FCYaouC$!~(?@^s
z4%m^XNLPUyFMjNysrO?SC!zJru!i!JR|xy{O{4Lm(ONOqc)AV?PiUXy#ttKmI?4_s
z0I@h8fu}8YwA1NH9Z_bX8FWB|u)|dsJBY}3_6&eP!@I7du}QNPXp&ZXGLVM?5OTK*
zeHxG#l<8}m4;}~oWkD*6gq@l5{`3pKXT)b;W(sWimRf14zeEqNUr{goHFo@g1qfG-
z3@>koV(x%X)3-Yz@057t<n)JHuT?`|#ZF9rCvy7%*raK^2~z=}-d?*s(r63O7}H>Z
z*fNMmSff`Q2t2Hzu`O2F>Ah!9s9XP+;kN1j%Rlta7Yql{NC(k&tFNa)1ODE>yXSUx
z$gmy7xS@V-%W!IP85Th5G0HY8NuqQagpfk>KJ<ChNCRW+-RX<QoUq_4s$GSKnr?QI
zcDq?)FO7(wUKw<zySFl=@#)3ri%v&oy)xOHXG##qA77ndMlK*sj2%f-L`LKRT%bi*
zNMykP?}0_5eXP@*u)V##u0Hxz>>_(nXLU3-f=rW%u<I;vTMg_g=xOXN@tDh(%SM3(
zo<s{g=WaBtaYGOl?G?NPP;@DhA=T8Uuw%v=2wP=U53qYzbV~f)G1!EI)7NvJBOp9z
zNdSQ5tJCgVj;WvfUF^v8Z@8`LSErYz2Y%+*)Y}ZI+n>d5pFZ34901Che&SEhO&|R5
z{pRnG;52G_;yqEIsN6gM20JwUOlt%EJ_eMqt&z{pj2JgIr&#y+j9k{HPdt6+6mJhT
zh<~yDn%h7!Xw>tLcVhReuek;U_5Ad<>FvCE`Y7+1_IYCZAK){3cDHa!T|123eF(K*
zQ&V5T?!Vy_`X^8GB52!<&23<`xg^<G2a$f`w|+Q*WiA@CASu_G-nwyvL4vf`*@S)X
zgL!Oh00F{ofVKcp2XYpKnb=e6BHD4J6M6#2&`8&yZHduLBBOEq^sxK<T^e<)E6Qjc
z5SU*7k^85A@y4U(Cuv6E`m83}E*n?AY)fp12ftxM>vCBGX!H8?;ExmxTKA8v_vFe>
zQ=#jin~-$8s%h)XMlkUudZ)_r^k_<d?f5kEri<$Jzc^sK@u@d`;=lx9hcrj04(2v$
zgLD|e!zXuuAZ2ZNfg%N(M>y^gSg4aeFvu(<M~%>0Q@ndk2@O+G-e!e9CyBx~=+6zL
z#j}aTB)a2^=XX$h5UcC!XcHu^v$1KsdKIk8bt4NFhi-T5nOX<+s?mX_T!BMtj&Ix0
z;}yj9VeoC-hGZ3bj~EGd?6ig|@&iD2^>GbS_&}^pFZ}4q>5sqV?&-gOD>r@nJ%^_6
z|Iyp0Yd?N%`n9(nIixS<>+d->egE4EuXNqbZ-3wrV7R_EHUH!Z3Td_lMio(1c6hN4
zi;0W|fNN%JEm~A(GIR&Y(AnGRwRil3`p8$XJ1?%Pv?~b17;^4(&k0c;sKx0y_SE#~
zxjTHi($r)P*e;!(vQz2RYKZ<(M_;?6j9?ED0U7ldh`>2nM3$=&0Sbm&ed_y(i<5oy
zwujO-*A}27)du=uX%2zZSPY~sxUMaL)_Z7DG=+xchAys=5Y^3QWF>7b#sx>k2+~UT
z6|d@arz6J+1^w~HvNdXqET0`+jqYiWo^Q-X+S@~OK#@KMi-VS!#zy55g73-(K72Bb
zZ@K8LntRKC-L$!}{m2{?L{anY+I$>5&*ik}^R_pUbw{In=K*R(Q>hCfcKe(prXS@m
zoYua~b{@6cu1jLCvq7l8<24+=q-)nXWvsj>HxV-<>95&Qo!6h4zW*ow2xMXZ{9+Yh
z=J`|$>btTwHT~q7$x_j7tQUZ9>nnS+Ak>Ygp4q&hNk}BFHMW|IL{Logb<j3+dwm<$
zrcEmw3w*Y#V5e3=ruCHCmUDe`b7ff$!;S%T>a))q&QD+SuDkZYv&W_YAA(^(R@Hhu
zn9+04E(854$X3^a3E*(ZBY}Yp=<uT}8o)Cu&lZgxT4?|QvF$nN2=49mB`^}J^rokO
z;@A!LC*O-5T~U%C^`_W6i+9eoCZgvX#dm%jJF$lykl{WCu{J&ZF1zuvkpqE>{2Y0k
z88JM&u`v_XNQQ21Y6|%^@7kXJ`*)o`vuBvAD<Vx(?$fut`%{OXdghApsb|#GC$ZC@
z6>qzE>6+jN8dQKrAiyLyfMAh6k`=Zi8~fl8DpyqZyA9{F#!C%F>Bv{i=Bmhmc%DoA
zZBSiiuxFYei)8c6Co{8x8L^?PnD>pm-B8;6rP<!zxc<|>fz1qqb(_Y)lx1mt-eiw1
zZP1wkL6OtHYCept!s!$5dDn^6g+=NRL<B#Ye(_!BGy?#lWH)Yq@84k47g~2t#S@38
zU%PQ|ZD}~D??_{p)XN#{&Ku|7_l<k+Uc#Zl1h&h0mky#G8PUtvH-UKzvw8aI-`Kcu
z?Zato`srV%+922q;17a`MByqVGr=d28;IjkObZAZIcG@xBT0mQker|ug$0Q#@9=C5
zY&4M6G&sc@w+x_d8HvlrE$!d*13$4j{pyWF)A*<FKnC^UbrdsW)x_5hJUD&s4<4L;
z=+vR<<&UgQpSp4Xbmt>?-nRa#5d5r9KX}8cz5D8oA5`(O;kE~^qR1PVEP9AZ2TcEe
zb8Y(7cdbv4c5j>h%dehBmij$_#2$dBU;nGK)7L47r`^bz<Le#`aSad+SGIb2ntJb9
z>$<iAhysz129&xO#E7t72!++5r_b`Iuub*DXACCw@t?yEVQbUEZzM@rKA)iZR=|R<
zXtlw2H&6fj@7;-KUWy2By}RFd^t-r4*t+_}*A84>*`3SZr=2-BUo$W(bmIKp?9q)E
zKJq@p^y?ojm_TA!NSRzQ3VBx80Fy(r>IibhsGC0ivCW4yV{O()GvI(+J+mL6TFu=C
za{QIc4^GxKPka3eu$BBIetj?5qL-`A_pBMQzuBrcK5Ii>aqVw4b3dQ~(w;huypEU#
zB!b|kPIQOc>9djyCjXv}`}z^9pg*JAN>Av^wi5rl@CT>vL$Rj0BZN%r%`4OY`q9bv
zCI~T`2lh?J4cTos2Ov=V;VcS3EF>Ayj*>8sU)*^3xyP{S<>xLOtU+XLntJZa9saa0
zmGi}^()8Kqs5{W(9xE`T>7PGmU3}xFM?dafw!D%3_?vJ4?hEYcwGTgV!~MzkVN>s?
zE>9o(gU1elOqu@m??06P{z&TS+y5Z6kj|g~<e}+fANhVs==1B8*w%cy@3kOE{bAuG
za4iZHUPZj4e*_&TT!mm!6-A2Bmm5F-xswLF+v`mujdRtwK^R~4Dx(Eonq8S>XeXCR
z?|5^uSZSpV>6-rX7e0Fyb=^t(0=mMwYOK)UE1DpDk6Ukyzj)xLAocXkUkaVjJdV{c
ze1q<@H$MNRKg6a_{Ppd3Xz~Ss!L@+ckp-~E$5bL>IIdY84}9g0qZ`XNpqMpd>`qXR
zfHFdDU+ayH#S3V>iO`&*rr{9v#dk8nkUy(UdPsu-l<Vf*fINXPysSqURqT%p58U|J
zUk9+;W^hiQ`0_*3w|x1$n*J1a-(BEE%oleb1m7cgr`WlZ(>wqA=rON5>)t_VEP<W=
z;+NllQQsQli=YeZOxkY*{?=?bf^3V~;!W4Sa`6mWmxX|VmkY$u+d~>bOxs`ii({yh
zwx#pl#>b|q|90{2Ic?~)2R8LrPTolTw>KPkti$PALatRi8%Uz+EfD)7?~qgEmDn*A
z^X{a*8NWA)NMdMnPmLda#J7C9((CG?|BUhxKr56_T31N_s&n(kCtmo{J@=#aL1#aE
zBVGus>8qc6;KEWOYhjgDWuSA3`oI5A?Cf28;<W1Xzl)t#Kleq$TAI?&U69R%{4{XK
zSXsCP?llboN{Ej@SRVx&md&>3Cos@tq<N0$^Z`Oy5*#Qf{AgQs{{wbH{iCSi$kiEa
z5KKfzEjOLkXi=U;n@c&NC4f_PX;W`M?T}~GZ+#BCeFFkeU?0!zW!TCQ93Y@*_v`>h
zcU^iFL~5t^ALziYAfIVX<rstQV276F8z1=g0|zSps5=wfS*RUo_SAxyE;;cwoY&&R
z+R}QsWSu?3SDuk}y&bXoY}St4{SbzoJ$7PWpi2iwed1~C2Y0`69DCBB1u5J~&kmK1
z?f{ecb`W6#Ji6rC$&@lwBEI8sXMCFEa!?wadwS$G+9Pg$p&hCdy7hqMNdezU`2FZ9
zK6K*RbM-Z+u-}}lAb<{aHSBGxsG@^i?WukBsdl)lJ$B&Q>@!Q{J6&D!J6~QEmUm<K
zRj0A*ca<+1)0%++p$v9loLN`MeF8GpWWE;kZqrTVP(iS$%?a>n)=t&{M=Z8r9uy|n
zfgm3ztIxj%yMKy*^w0#o)JD*atyyP?i9KQ@a2&Q~!GGH5`&#K+u!K|OtMmEiaBldh
z#O$DLbK95$MTL&S5JCu%2LO{>zz0?&gr(J!GWLjip2luNpyDqEst&{)(<PXyTLO82
zx~rd;u_Ie-4dS>PwoT(>j~Pkprtx8*7R$cQXe~7y_0Hjku{-b6RzMSS`UsXy^~r~^
z_4E6m&ql`ekqPym9>Prb?ICEy0-NgFAI2V<Xr1@G%%FZZKhA6Wh{PoXCn}u4?(}IM
zBF<noz*Zu;O(L}lA+M(wPc(Z1)a}zC+qEV-PoP226NrRNAUL`z8_*j_SXf~K^aK(X
zR%8Hr0*MIZlBv)WNGh-_pFE>Wzg0*t8&?dQnZ+y}04UUZW$cvt_+jj<`Y9iFRDJ#u
zc0Q-mA^-{CX^ZiqyN)JDKl+C6?}1ohL9FV%8`#OCD?>w0&hDcd*qg9ZXatlOp+!Ts
zym-NzCb#kyb$1i{<AeQ&s@rP!8SL1JMc>QI`8L&eJdGVwj|8yGyMOZt_G9;)yh$i(
z;Vt#+$FT=?-}ET<jNt^D&gX})wVowJXi`!AVi`Mj+G^EydgU#-e0#t~t*IYuV<*+~
z<%5TI&p(FUaa-YE!FR8m?M}bg2B^4%Suv~nX&F1Oo*}R!9xd@nM+pSSh81K8WIFAD
zCarF2TsLD4+`o_O7MrDU-MI$0Gw%mfU|BFggF{rW9wD$Zg<H(E(oLU^oX@AL5VZDi
z;<Y-qzT~qPi^17j48Gb?AAKf4aK{3^8F6m_oXDQvghhaE`*7VOVYbx2kYP{J>X~ja
z>3kLR2??hzS#XQ`4jMbIdL-=fvj1Z*+n)r9fMnkO<?qN6-9{_F0?p$uKiV9ceVeI8
zT1agpk6ko+qLH1vFP#a2u8)P{iL{!Ou;Z&#^N2Q+PU^R&n#DpReoLz*KXx7?)dx1Q
zGm{MI4_aqwVrUo94}qfuDxII<eLPLx)BvSs%^U1_t&6XP+|do@rA}PYhT5XtGY@&p
z^`-hd0Nj%cVe#FRVJNb)v8mP{!Op#6x%5+$Js1EuOKT7<6<UY52xngvwu%k~{V0sI
zf3?tB?P*|KrpKW{(2us_dmCvPbGlK7TD<hi!U^ciPTmj9=9+qM5IeCKk?T57p>ef+
zm~X?&BACpi-LaQKP-}1t1W)e4P;Uhos_*t=XCFcQGY4S<?EIT?(dV7va`zsXudk_(
zg|PdN-)d*nPlT{@_bs(6LKr0xsvEh3j+4ebu@4mlr1mau%&$`YzQxyB^p+4)G|yEe
z{p22^^1!|%oCaxIaL?Xu*VlHx5W;@(i2C_28tzcvp2NO$5Dwf}wV%iSWcucB=G1lp
z`}^r@-*(sZ{Et2`{g=1grYc43arGBC>~7439EFPp*LC&mX>9$$nRW6a+OylluIzrf
zh`k3ph&ymx{VI>0Q~$Rzb}56FV?|xgG|$!fv$<>WVu}^0W!8J%(njrm{3`Y?gL+*A
zqeOfH>l|j-8ewJBzj*>nV;0rv#-{4gS7Udm|F;{<-(|;f+)0o+m8-EFc2O;RFtbXZ
z0`dI9b!5|sSZ=rbYAkH{eroH5K3(oH($<eHwZ-J+<@;P)IF<?wG790?5}!p6W`-9%
zScw~4G#0>brr`umsOpD}nlM~C1sn@5!hoL0MmWzBAx8wn4*0+#I0ce%o+-A_s23qq
z5rRs3^nNCX(a~<7*d~_QcNdM(9@Bz!PB`+1|B&x(>_g52WU2I#y8#_q9Qa3&Z#^Gr
zqD87TuSqlNA;p8Pv&$jps(b+nqP2|$#+n{=Xi<GVbz-41Dtv3c=(=B}XLJzSRLfSb
z)n;hBX3ZA!&GHCd#H+cugk;4`mgvEb{sJw5`h_j*<aF}8hwhzk+oIF-tFe=I-Lpk_
z&5&KZUT!$D{q~KDoT_~mnr#8~cWj}7k))X<FKVGJtv@iOfMa#@?^fh{vxT-Wn7%U;
z?dB~+`?f;lLQjqPusc?*n9$2wF>_mVpU1gpGDHaKqdx4RC8JU=&!`LgXz#x1R)|Ik
zXwSY2B!l4)1w7$g5XHijol`udooJ*mr&^747Ojv?coDNL9fp@TdFO6AGI^iIw!i0=
z6@fgyhpL<F#)piWl-ovkE_UMXm=7F34HIxQZ|MV`-i1hmaGUyo1-pH>l)}shU%6&n
z|7Y+c-`BvNQs2m9_o;6v!=dR#1>5?!i0^t6%e{!By9=rsH{5%ZO4j^agVnnG9v=JY
zL;toE;Va;;TPQ-!CthgTTY0VDf0<ajg{1hd$;7?F<X^?b8MUWi{_i<yzhWmz3lSdK
zE%mUUIegnJ|3$t0DE9E}K~lSEL8#9+uu}#CNA%ssRR7c1gLg1AX=fnk{<^wV0<*8r
zV;9a6Zm<V60r-+E;5jgvIWR97PqsR5j@FY_{n<*K?h<i}Xgxvd?PjsMS@?PpFNzz#
z<3Q~z3m9SezalEMVrbsXUn{YRmuJ}(#lJ@%?WN+CAzy(eno9vq6Q!}B`Yu1_ngo%1
zgls7EvrrIVrz}jp<(w;8Qg8o?Vwbw#znEiYtF1pIqiC=C+DEY4zB5X{<4#Z1XLjP`
zHeuVgs@HlDL_#rggN0O|s$oB32$JgS2{^{NRL5$V`hO0vEEe4Tcocj1fEsuT`_)s+
z#5Fxnb<Q$BR>U4eoj=up--Px1qp;oIvBF+kTVo*1A?ud~C{q3AQ`jcNlm4WEoiq@n
z3OxQOSa!eK#2!Cn1%AfWKW|~@A0Ulzj-lK7@PPoE8Ql#+8SDH=bw7=*pRoIFgax<z
zA>KW7Fjj)b?X~ObgvZubHR4K47iRex9^07A-QSlwy0hQn=+1Jhqr2I&m9YIK!uJ1<
zFhGYkFB3f`_8WsQ6$d2Yj6-u49Ei3uuxFp!z|LTl&WY5&DNfv4$bwa4aj$i};;Lzu
z@ycT8q4bwp48pE2#=hw5QrhU&_R5$qA84J&%R$7U^wAxV3RkHa;cNNSx4gQDj`OrI
zBRKHrn0g_qxSIOyOW2ud7p~b%&K)|s_Y8abJN(Jxnv@J>cF-Lo5SsdN$(NtP9y_FI
zUQO|;k3WqSzxPI?uDVu2BrmUKNY|VoUut7l)Xd8dxzJoMxvwijz5>lPXB$m(z5DxU
zcpbkL6YRd@8O(nBeHow>{eGdYMKTukL(gD8sJ`~C2i4v8zYcR4Al&xs8{q)=Yj40#
z@4n&9*e@827ILO+_V=FaAT-3nhI`#x5B?My>Sr<ai8o_Q+YUxOxoCt&KQmKlH;<}b
z_#td_)|OQF4R~1ZrhZ_8otl6C>Km{#$0&0DNR;})XRwD1pl$Yz0n!U~hhNb1)b8e6
zvHxVafNov%DjU-JlUo~`*R|_Gm$j(YHrVs%!piPFZ^Je)a1xVm!|um2>Z{+5eN27+
zZP*d@`L_e2v%CCi;%(U31)@7|$1We%->C7oVGk^@f)`ub69gW7IlFuMS?sPOr{^|`
z#-klb5mLk*nH#ga#-G7HvxXVfZ~h#%t-k)ZAk_J~U%=LO13!;h?@{^pV7H%y>wG(K
z2W+PUF*U7^=1Z#|K8l?<6M~yk^PA6?_23+2>i77n`u;B+c;vh>z$!A_q2-}M1j-AC
z$P3BAZC!R-yF#SC>iyX1d#MIwi=+IQSS$y380xHg{`jF&SK&<isb?<0jhh<X<F(X(
zh%DNz5zqEKNniKrA>Dl^ns9r4>7{$WIeq`PkMI82N3c6i?c#ro{oH}0!QeVNk6QPv
z!#SpP_ltjm{S~HmzKI>bxPuZYKwK>&XxbXJ=^@Il(BAI;_Mc+-gVXI(x34XiPEB1i
z98;hDVZ&kdA721J|Ksn)ZdafD6n4i2xXOocL>Y8-NELrbHy@WhS#>*NIE8ZXUWFb)
zj?m1)Txnw5GH$%gDARZFC)HPf6}$bO%KEiot%fku$J_nF*Ra1hqz=D{-FX*`4Mi{b
zs{Eu44kr-CjycA&=l1IhA%7F*%ifOb>aYC+_G-+zd)Gf=g9C8s;5f<vRiF7LcH1Fs
z#QCF3t4p^WTc6Enf{GXTb+ifU`@V_2>+Xg5hAxo8QT^AyjcwniT_%Mu%%`!7XSB0D
zeMvPy(d`56gWCNDcK;CoQQuX<t`ZMG=;cd)hOHmg66zjP@n41OlpB8tN%&_!gZ<QQ
z{4cS)u)DSBEo{z)ysceVD*qSkjr$+&e(t|u$ph+re}(<k_C>G{+ad&?owM7^=&}PO
zcA>H!D7PLiV!}lj*yVZ~Zq2J-`~t{1?z7mDQv%(F)Q+o0AXs!3w=IdN&n3u-Jgx5h
zJ$B?iwC`Fc4Ui|DSFTU#A3LsAzl>da=YIt;^3R7M-Q|D$CHxosTNw1UFMxCUroY8L
ztG@3AY_R*|{{#DkVfWAe9{W@56yz4pwv(`3N38ZF<fszYr`>bM)O-FHcJJEqt-Afc
zu+d8El}m0Krf?M(xKaJxf55^Atyc(}de1*%KYC~lZG-x?Z($Ga7XJzR42CueS6}-r
z?Ao2cfnbMg+mum1{B7((5MrN(U%RKjjorYGK)<=9*E%7$;&tD`iZek?taP7C{;z*)
zA=1EK|Mo)gk$VO>K9lr&22lU*Ti69O#ohmm8IceKI0>*W(Trv7hQw06-&(RkY99#3
zl?XAcEqbC~K!%ki>=q}aL0U+M-Few)x7jRq*teQ70||<=n`*QX@_M+cCeF0Z%UfgG
z;7d1CH=2+Tb4NpgI1&Xg*L*!GZN(89unMQt|MV|dP|L;3_^6$1De2n@#Y1r8C+#jl
zM@R<HFJms=`r-A;+nSDgGMxzQ%-f(myN&sm9rVf*%4RMb7mpS*%$^-Z(=G<y{LT9e
z?^!zy1m}zTQBj`lJ)`gb$2G(Hop&#9w(ZC~9A-eq^v|jI8N<2L=sFQ(#lpdk2<xly
zl+N2<-!>eZWb_-H8mGaHT9$5uQ7WQ}4p)px5w7*Z%`jjg?V6_`a;Q#&q@{WGI_SFc
zHrz=@3Tl3>PMci6wt-H{XC1ZCAu*H%_piFWyxC+ls-<&=Bibz>08v6Jssq(0;C4G)
zMHXbZZMp<CmDJzBjdn#VoiS@MhZp(x`V1(IRlB{pxwtcgWKjn$ZEM9Kv~l1Dq5Aw8
z!`XvvxxP8Qw|(vq`mm<%JYcwgw}0C38pFe{Yz21z?0&-=4;(}4f8RQ&^k)sn)hEvx
zdQm+IQBSZ!GHZjGQ9p=UxHbYAiaer!?I=!@X*Ee~qkGgy_QBm-O_`%97~@s-b<Y~k
zKB_GYtUWvc@dw_u)9$oF4kld1$R@mrE1=eSxJnDhh8zv0BtUk+E$Zl&#pznpjDo9_
zb`2BBPk4tqBhIR_kod2kG6KQX=sxT=90O{SXA68Alr}lwcwT+qmkh=yAUzRn+2Sp_
zFAvGl+n1KiH;cdMMVFxdkp}kcA+!4*&l!GW?I0?&u>1N=!=2bs7#<R<jcu0JB;;Ek
zF>L8|eh}_Z*8#4c)=>40eW(9J0*7<{O9@ygfVK^~L>gV%n+E{3icq4<r4U?8L2`qZ
z>OG`PZqW>q_7=@#Wddiew`c_QgqDq9?{3iqDhyppqbn@1DpeE#5qlEU0zmg{Yv{oD
z7G0oh&80+#on;ij;@4Q19~H+yy%vVxEqX#y-bzn&TMrI!d$&Rlsu%%SGgd)y8K`i>
z!WlEv4AIwS@8}bddIS!2jxJGYO5=W1vqSE+U%ZRf1L`1M%Ii0rUZ$l7TE^VeykN|}
z)|xa%+mC)O>Nz(v*BMcSCXTACp1!L$LWP+qh(y7#9;hawNgv%(+>04%21&cC&9wA-
zz-^_WHb1l^7cI3zail|5TA`Vtkwt4Xr!iF70XXL7W)LWYS^XfHVV58y+bGWW{N}a}
z+y2HqsFK38Zf*(b6)<WX2nF%3`oRwvPEP<PNCJV!AptBTkO$h5-Q3r?=*tRXuinsB
z?dE1KtyxwSjqM)X4A6|N{e0&4YmML%QJYQ!LnF%N+8v%n?8dIH#w^Z`)(-JDy5Wtq
zv!YdT=?O>`Y^*;P)Gzjdi}K_Z^7^K_`-ovZqTgj+3it*)<Z2^ycM#e^PSIReX!*7C
z!O=xP<NSsJVn}FvX><35M+}c2+XP0~1{EQ*JDMrEgKnSgfRxyPf3j{~YR@)z>yH_}
zvWD&Krb33T^QYiG3TmK<Zd8kc?kVE>6;Li;G@Rbv+R{ZjVqU#A65Mu07dciuE+l@z
zGKXvA>abzp?+2m29Pt7OcQoQ9Bzo0^Vz@BrfSyz~G(n114*jghz@h{lqAL(^svu=q
z6t3(`cUtd1_00BD&s<(;Ma})$hPc|o4>Ow0D32X(o@+kjo>vc{H=DD%Yx=w}^9Ik>
zX=a4de9<UKHb1l6mOaBPwZih#Es>#zFM&*MHuVgf{m861mHUxdeKkX7v5LE&YZ!j=
zu=)YfaMq^7v`&FFq<O{2=+u@2s`AV$6D)QJ><DcT^#fhQNfbgjvin)l@c&@yZ*>j#
ztDlg;0{FHFZ$95Kcy}X`;V7nFh5r{p2WWPB5p>wwd>(%I6VY&H9x(YG*>GYxAjYWZ
zCHOIWp4-)uY>22SG&#IKr9LMEEPwX2;i&o{*>Dd;ieRGOo@hAC_HD5cdv<K@$F8>2
zp6zeGy0lky@?G^&^(C4Uw$5Me;%dZWS)54+;AY1`?}fy!UYcW-FZTpbHq~bp!@Dyx
zE?V*U_s_BN3dr`><FB*<<FMT`oK+v|8IDce&z-nOTa{(EP4xPlU56jJ8?%_nD-$Ze
z-!ps-dljPX8Tn-vMm5UOLbM3FVnK?xAjI7p1H&)hZIC(jhu&ncB{W+Nm0{3Tqdw{-
zKPo^5WgxCx#0h-(jCS2?8SUYooZXuSISF?sHss|4^}oN#V4Og|;WPW&@3~5SeO8)d
zHeUn^nk8oi%~17;J$?7wuby{A{sc^m<R%n8TL)KrS(@aeh7@|edVl&lDu;4X{piqe
zqzLuLZklE>)JoNm8F+5EhCMS<ZtAvoiqxA8=k9<C4LT92tv4I+=}$lR-~_w|AfELk
z&!S7Ex{!x!Y7petBtOuIW@c)Bl14Dv<AtJ_`AQ3m0U6~?Lxh@9ptE7QZn4h`-a}JA
ze$|M6nT)9D6jbm47&O;mRS2V^rrS-ZHMOy?P$Keyh+o6b9DpHq-~DF8=50skr9Cw0
zW%Uo=ZusCm`<)l{XPz}s$F9Sq`^JO#)cxw#&l-%o(mM>VyE}|7(81j3Fb;m91A6pJ
z|AtQE;2XL-hkn69J^H0TL`U@S4IRXxUvN0DzP<~!<UaJCH8lK%-N*+F@5GLKP=p``
z|D#)J5Q{-2Mt8sXvxfhC^dNMzsK5DPgGK$-pEBHW@gfw~gF<mAlms>zRB_=d91leq
z7fX53sA9ivI59D9EIlBIA%kViK}kPw#E_q}wFSa*3zXaz3TQ5M*!tqdio#vrl2*Q5
zG=>CY--7BLR*@U7pkED^?_33%&;-ObTFXh=m&rhtFop)#Zv{B0iKqvCQJf4FazhFp
zJo9A&rZ(2C%>bizrEH!Js&D4z3T(=#!(IiG0_-^Lf;EUBodV^ndX*6<)VjGe^4d^L
zvrkJ`*PjA20SeU_Nt^ANmEmT_4cy??5KvPeHvFKr3zOwKX$!d0=Ov*>#;kUS?~0x=
z0Fh1osvYbG)b?8$W(AX(k+6yg#cpVkq?f`4HTVv&p@$1{SOH^(&8@5$2swY3unGKY
zu+h+5?$4!N?$-@F2UPE`8y-@h{ea<6ZVNQ(7CI@|()}ErR@b$IgxT@s;xJ(reM3hs
zNN&kjpS*+)5fJNIpaikT!cd*~()GQh0Gf465w-DeTDvTWPd)V*O|RF3(QB}3r>7h$
zG6(vJMg?<_Wi~5@2t#TWOWQpt3JSIMz|pS@pfwe7+nCpG0pdfy9~4Reg;w-3e6v0%
zBncI6X()#zg7W~fTz3OjK${b`R;fUv?M*fL^M;3ZU-(VK&)=<n;gbfpy8aO`_1^PI
z!}jil-#5JK_T6iLZ1}vv1S$=&Jn}H+hqow@g1*c9D$g!2Wvjpayy4D^sBmC~g`Dnr
zmD!tx*zVsyZ`eNAxM(cKC{1jG2L++7UI)T&TK$ZrB!L!iy(sQYq(M)D+QNtA%2h`|
z5Y}dA*k<%V6{@huC>twrTEtcA>+OaIG^bdqLL3ERn;Y;8B^uOfTEzl%)DCwm(NnlN
zIzM<vFX8E;DYz24RXF2RNO>2ka0hWVq&D_ScfWrrZ=pxS*skqsIlEuAuc49Y=<T!t
z5tOLGLbRgEVRx2ugTJ+BkyQE+cII*IpnY#I?P@&G(Eb8^vk`Uh&<a~|y=Llz8YGXQ
zM}%ItS9|a(eHT!f%R}0j=%D{TmItU-#vpDU!=c$iuY3&K42}m@;IQo*gHSIV!n7iv
zOIXzNM(no94Bh1#AY##NUqml6irY^sp!QJ3Vb+-T0=?-MalxFM$L<lqYGprA5dpac
zrjN$naB90}gKD&V8!B!CVJ9u5`Vg!QJexhzP`CxzYSq!67~VF4pwZHn)|f<tTW1wi
zVGas)E{egSdIAVokOu%y2h{%d5@)oxs2s!&+L;~g08HD?<-sOu(XQ!4jMy8@_DOX|
z-djsz&j^=|&OMZk$Wb3*KPhUlT@XH73qGG$utPv=wI|XLeO-kdfukNijNJ!yv7jWa
zmSJ5+-EXl1=$rrr8s~#vFs6F~io&8LBW<bRa7ZmAOAYpDC6f=$1L#h%Z=77mhaSlD
zfJOH~v_~>}liod>Ics(ilD<}(<zA%SeHz1R!wta-Ld~Y#x1Ggt$kN#RgGxuDvZPA!
z#l|{|58WOOv^2!Vk-45k`2ky~Mj#qA3Nb@+?`EKq<fuY1vK(j8{XN*gW@+KAo28~k
zeo%H!E3k2Hx#n|%t8v3w^)o2-i~g!ZV%bJ>5mp;64;C)KsxKf|K!?sgb$XuF`-SL_
z`j>8l9o1kysaBsdJg62vh1~@;es!J&nFxjmRHv+IKPU$e<?i8A{O`2VHR|hr8_wFJ
z@5gQr&!Uu|7Ew4;FH}2I$vU^r=hkbSnDwGzi-0E7`G)fX4OR7tcN@;7FQGCX8Z#r>
zLPKciE&-QOkXbtqMh&3O$LzLKgO;jL<qz8HHH|gJZVV-4m1f|uf1P+hE9*N!d<Vxz
zPyrW0eW;u}Qt4p+LVR$Q?w1p4PVn}^Glj~EH!m@4G}<2&tKG*tY!%6m<qjR?M^J?a
zc2}p3<u=@hc1hE&3u&npES>|NxFz5a7)I9bez~<@#oq!4_3l44oZYqjXTxDk{l@38
zdmfpS3jl`9j`?u{q>nVfZv?wkBO_#SY2}5~KmLs2h@Qf^FVdmW4j6Y(3j~ZjttN=(
zr-A=<X;CG5M*DY9=E3@TQ5n7Z=IktEWl|{KDxpIEQ2Sp1vjMquK%)%eQ#jO_izhJL
zz!4L`a0C=k>CFW<09t4TS1l~Co}d&Y{KXT509-UhA{**|u0ug^u)omTm6r(d{q^8s
zObz#0M{s$yubNf*7}De#*We4Q?e3lLF}Bm!Ja_+Y=Fbejc6L%znuNEKF<T|2L8U4!
zE?O>j%`uT2kfT&cDN;TrQmsZwXC0Dmhn>N=Tx6OStGQoEI&ERzF`_+oN6sAe^R{xK
zg~x1N%P<I$Xq!3VH=EhMhY;gAKHhF;n=Dl;Nr{oIQg-IsBc?d&44FwFNR04m)<Qbz
zQj!!LITjZxO;^}t;|JH)5+;8##o}Dj(H2R?gtt33h6q`TxW8D5#i7PQpzp+!<uRP`
zl1#}SXUsLl776F1UeFTfaG7WG0o+oL*CM`BKkjj{F2Pi&+gYXNDtIf-1U0d@^6{SC
zTCvLkv6oY-g=Uc$*zz<v!ZY?xv(_F~vhiFvu1GffFk@C~=7JUP358@qDdyYNYir)J
z&E;T7c3=)kgM74UtrG2u6d`l9rZqy;xE|%G7L#IAto8AVE#S3U<fPb*k|lqEHsvOS
zt(M>{E!^h_R<gmmnQjqdM_)--Tz#luq&OX}YOii#n-+^XR8g9wFHo{oye?KSwPmv2
zteTsttXx*&aZh5{!^gCz>QWTDJ7n&;Qf6nmYZk7p5djn9RVqZ<;=tpq#p<&NVxnWW
zH)VDbaIhoV)yvR|)E?D}Hox7@NQIV-EV|&JksS1)bgYB0SWVTct1VbFO(*UuxrW}6
zts!<|ArIpndDHIExN8v_QmvP0TTJyh>#X)2$zB{XE}ChZr{?K|+wKTcRbq;#huaDc
zPZW08!6k`_7%}^;ooj2(VvipsO!=7PDpI_qXqGccn?yEBq%)Wmyh6FsY!0PyHrFXJ
z6l;Z)Z+z_P^z(_fDIAl?d_^i3>=xd|Ft$Q>T;Sw*gLa1!1DY+ns1nuorMs4K5f9|k
zodC|%l(s9~&)Nt`U2rsmQB$T97)YjZfsdrzG0};a{BCAYEsVm$M5gR0_<X@@Ym|c`
zx=!af<tvV)0LeSOVoYFs#c0k<r&5$BEm($bc}VA@qIux$jjLR@W$~xeMZzMxZQcQ2
z>&J^SWmd*XOUM@r`{Z_IQi{9tad+EEIJ^C1)LLsvWH1z`ivDh$_FEW-Lre~`!7fvY
zg)?*lZx<jOW==#xqZp_3oxXvSv?ke-Icwp1;cIJQF=6s#!lMe6s#>{ZBN?|eiuSm>
zf@eh+lXg^uK|2?7R86eZuy9E(Jn{EO(zr}CX&kD1MDd`5iTX!9qU9ikfsl-vOmRBE
z6qDJwAf#9}ZVJ14tzLVS&GCtXNhHXA%hFE=vw7A%8b!&x%Qbe3{c5ygXG<`mC0?_-
zsVs{-8{(kVkd<p|QA^7lPgWZZUzZdulR%Oyc)VcDc1%h<G0wX9ZYC5D+p@W0${b4x
z!q{&Yirt8(As2%|)*kF+N|sDxFbZ^=c-q>K^JL$dW}Q-3;uE4f0=Y)PMAKv)<xS?S
zyU{7~yf2!JWP<5<VO(TGd`|E<Vot`KD4B<2=cMW#5}uk<%GGPm3Oh1~M&@g4`BbGU
zn0PNZIr&6*Kt$Vuo%JQ#HlgaR*W2|m9>nEBA}zZ_F<;0QL&~Tj`D{wWUW|~@5+#8@
zK!wevIzNg=*@RMV@f8ziEBPwPSfmjR<YTQ!$eb8@GZU}Bh?lbV01=9KSa(^*Qv-K7
zXRF!BOi+r-V$@x0QYEiq9@~<lMNlZN5SQ3%YYZC^&8bj{YsKyTK`bDUlG&d2#fu|S
z$%GS<rI|{XQX`KDi9(rHdl2kr`H(YIa|XR(#@jFj1+(mQu#=H5FQ)|;D|-@Ll;@oO
z)ToqjmwVxIr9dT`tx=sS$E2(f@>a;UOqg52T)951fRYYc*km<7RNRcy-}N(TQ<Vso
zO|i1mOqPZU>~tu~5=<*lq*AG}RrKU@PN!Lnve^PP$kd&kba&JcC#k5pE9G&EYgoa#
zz&K|cHL7VPFs>CssZyid2uI6gDpBqPh9qhAWO1iSEL-g!KR(FtBQawwWEf{Y#s>-=
zo6YKC2q~I(B?17VoEm0iXQvm>hpB<fKZxa$w8zPX6uHRMJMBuO;<&b!n)v<Jf*AD-
z{1JJSE_((JBJSc#?6^*)2jPCsJmgr#oG1hmDK`-lL{Eq0Y~6Z=tVKhq&>&MO<>Fi-
z>uQe%HLI!8W$V#WFXbf7fyRK%5#}gkCq`MxgqsR>)>1TqHxmgo0x7~{CK+?PJn48F
zzG1l1wq;swS1fF+!5>$V!j?wH*2*K@1(Gq>59C6D{@BU|_@dP<BwOCGSZ`MoZC9y3
zVkto}4KocqEW+j{*?K>e5BO@PTsA%mInu0~jSo$7s%amKBTFHK7m8G*R11+VP*TIl
zBpfP}F^e@Bapc78s1YoC$5m6p)N@(f)|i*X9bA}F+#Od~^e9R&IF7RU`hbUYAW7&4
zYrYmW2C?HBSIREOpk#3;L+O?fEIWi|BcCKwwUn8c^R*Hwxci~87_TPA7BN)i62VTn
z-{fNXiKpo;r!o=77pRm{RXZOkC<Dq~vo<6@O?hgqVXE%R*X%YK54puw!xK{&cXrep
z=EwOA<)W3M%i1e<!yQvL%8K!H)?@Bxvk4!QtLJ5pGwN}K%^ul(ZLQt(ciJ(FLupCg
zta~iy{RNh-$0zn-+2V^Sg5Rz%i2!9Pmu=oChj;oJuUTYDC8p=@*>gm~?J3Zb)oK+i
zrNNL1dK>nTDIM&`lu^JrHaCiXt71tIdE6#2?Uc*wPxxICD%x@NtU}2Nd8ic^QCIks
z-=FghW3sK<q+&I<tv;b`UL_?IOzt$85`M6hoB^hgidThZ)KOMkR#u9KVWGQir~yG`
z++|NS=<(YN`MxQYu(caf++Gp0VzScnq&xML7`MraXW(?RxTzHG`|Sg|%{zMy7g3X#
zp0{HsD?OrCDmBC2tS6Ewk&}4S>`vN2Xr*IR*iX5<j#?Q{+L{4(4R_kBb$_+&q$+9n
zlSg501PZ2JSKO|F&C!p#QWbZ#nJENY=6Xc%HhQUOE76tnM63)~u_8l~x5jHuX58=&
z9W{rXv2odch^uDZjDm-J_Nrsxk;{{~xmU$oa7sQN2mN%tUaSxepT{Z;?5#fKw!)t|
z@HTP|pR*ZvW{V>#ob$xxTG}3M(p(`MZ3hLC@D#g*+c_pm9#af>+A9Y8ExJp{q$x29
zIBI#^nT$}iY<%n<GqT5>uSdZM?aHP=dBjf~R9J2#s(!y@6%wgnrd~}IQjMaOboHoC
zyDkh&-m*2`qH>}oZK8vfV98^V*@RRGNQ4rr$iZZ;YWDVVYtvn5NBpK(&%%clpD8a7
zd5+HzwPGS{Dvw<rDVuD!+^iykL1v@;IqSg6bSe`^Ij!^uqajlw&COBDJ<fIPDIe*;
zN1Y~5H0o_j+U96@Dse#?)-B;4U+Oe$7GE}AWgDYxG}}vdN2y%fl?-u;X>74(<4k{0
zjk{gubjRWdc<_M*_sApvxDdAW1`TJG8%KgwH=7O(a|0jne}J~<DvYbZx2j~tl?=Cf
z#E1sFwpofa-8HCf*@ZtYlq{s8bj{}|6>L;2U~78Vi~<#w+d_X}?>dB9)?0K%$z;I;
zc4M7!4tt(}>?_GRYiuwON26YYVepPE8<11>CU4G|VqprBL#dp9LKm}TCl|1k>rpxF
z?<dEkH60&E$%uPo1_jvk7b%BR@^w35BH9W?e55xVq+$+Jmb|uR&BWO%(0RI9BU;6j
zL+<s8y|I{Pa=4?b5bZ`QS+RKrd23cKxhTRG9DD4(9BWMt+d^94`i&wx$);sTlkq4f
zIl^Vw8m=Up<#E%VQyi4k^_7yfl3cQd#_ojdsP;=qs>ww0km59%%8`B~C=E@iBHhfC
z%(AyUY&6DrtYaqJsQ<{F1AejB219eIQt^4|x+fp*R#RDXQ>wA<NQ0yX!SW;#jWl~s
zmxm9MZgV9ajnzGrCxW|ry>>a7@K{pW?8rm8TXqW<4FeDR?H-}ut7n)T<)H{q#2IH=
zA(z)2GSRHR9wK4K7<a@o8geGS@1YwB3b!Xxkjq)blZE~e4?4@vx+&nd@_qMs6rM2G
z)*RzTq*a&OjV_~r5$&t^Bkst+lkTS^OUL7j*KEFQHt$#D+=z@ZCRc~Idd-SGOP1_|
zPJ|ugEuwo&wPT%5sL<-h$pJ@}oB?IT+a<0W>zCcdPTcIR(W0+X^>IuH=-q-d<8H{x
znI=BL<aUZ6>&Hn`b;L?lZz$@@2TDGLVPxl^-WkP0*Vc-4N*<ar@tnuq&xDG_Y}VUm
zBbk25Stt;dpi7D?(OyRwm8GcN?i@z*<5Z~v#z&^?Ey+GMl^Q1Vo`feqj>_>)z8rFN
zMLy$fjFgGXQ7#WMqgsoqHHOYyDdKTgQ{zl0O$XdQaS$IgTOCrNypD9yCv^QKvE>ZJ
z26mTaROh-R$u|`-In3p+t?^E0uAE3nrDm-gc69JWy4dIpcuP3nHB}jEKyYn09vu?x
zK{Gcti=iIn4!K;Dt~s2pC5Tvy#p$%qW-^t!WIj}*VI$*?YFl7!#CTLl=VERr-64_#
zQ>WT3bez2q_+4Y1s>U0=9?@<3n?BA{h}o0QLdY@5a2-Ea#}n0@v*dB|USF|O2Q^K`
z_`EGL3e$Ba8!0fon5#7zcx$eqBS!TJ(=alqh-FXF(j`otCNb(r@xY*3uY=7HALOVO
z?lRRw{kpf_sO1};Xq+lrLiI54MVHA9ZDg&GFi|NgN3^(BwBl@3@{OL9_Jsn~kdkk?
z`AA$M7+ZM&>3$`-m5FC7_E2LIb$dJkI-Sd8Yt=#-EHY18qHw_zkLMlZp4T4^4Ima=
z9At($+}=o!qHSJs^a^n@Xv_IW?8xm7RtM?)KoE<mK*ejNz4inyFj+fq#;qb%Q<D9$
z+2XKJW=qW%^M)*m4#P*tCL=lppPQQ$sp!Ztb~N}tYfJge4p^9UqT?vxo*GdxyN5Ne
zHP1##&Jv>H@n)k1YP!+NH|(Wc-ZwHcfxO8t#=T@&qD>8H#KNDQ<S9Gl6r8lJnsnvG
zh_4ZL)8o9&(~DN~*;ZKU`cn)WcRQT$w}i@*f{bVD^}0+(7)zZmJN!aPrW$D_;%X-<
z?O5JgZFa`4s@tJ3{j6XPv^?Gco3#r(1;$c|3wX1o0`4Zl`H|I!lku=A8UqExx_srf
zd)SJa=uW&dss=5oOt>ief!l)Cu{#j9fmK9sE$gJ*EGTtvrJI-8a%*TG5j`J8lX0Rh
z6wINtw<WcGDI2)Jxn!ynqN)Whl?rzgI5!rvVKF#}G$bpLV5+W+zc5l_es>bWCdoWC
z=rBdbV)mNjw299vc$A<xhbPpohWw4fkn%xjM;OE<(?lv3YvA<-!^PN$jf4cc*_K<(
zz+P!YEM0-mH>=TlzAD=LHl`8Cy3!f3pO1KCnXH(Lxq7-4!)pbB3Q6N3-6;?ZWrp~I
z#Cw=gSmH}nHsGuH8qO>cvNp#4iej!7<JM#@8?kk|(pdDwCrL75Pc?n1a-7N7nx$AS
zn#nm*$rw#pudQ)|j#$k3OodjC57=CZVccv<dxrtH91D&@tn3`s$F_7^%=DcDo7chB
z*z{O&3T<;`&?8NBBHSy{Z6(E#Kr2yY)E*IjyXeFF9!Bsd%)^e;Hi_eLqSLce5qD0>
z4k^XfO@pmQ4%h}bxTL?sxUwl58KiyVu%}URrMr!a#Z=36GWIOxZ9z~XAu?p4LQ$<m
zIM>Q1YNnx|%5aHp%H<rDN~2UlaPfJ$79t{px+hv>ot=EL=LrsjN-GzwP6ic|FVdhZ
za;QzoVMj@%d*IvQEC*}r4W)RF>JAD$VF3PHs^+bS8m(xgF<?An(pN4tL-l+!lP^@F
zmaK(om|S8OZ1H?6+wBg6K5Hpv@7O@>6a#~BkHMQ_W=K_Ac5l;EvB(iI?;G=zge~I?
zGX8$ZMmPC#Rq(LRT%_)5v^+y7WMnsqUU#xAaCFp6lj-3oV-3Pic*lY@5w3XqLcTO=
z36XBVJv4jRiWF7aBVVOxD#XG;j_=gXC6Be(8T;f=xWN_3R>9UU+3;RT0P>E>)j(x3
z<|1L&wKdbw)wG)iEg>5vZHaJPNV<hk##L&N4R0VAvBr~4Tb?HdBT1^)gN`1Zt7Xef
zs2sHU2-!t3o^-vcII7m*IGc@l8;+i(=B%Zec0De{%E74M2!)1H)!s-6gW$Ll8z)o!
zyjw8`Cm@p{vXvNy=~&!92zz{`X1*RV<4ww&>=o;NCqJa4qb3d3jz65ta^u*52LrE}
zc93+@Pb!QhQy$jc&3csf*LZgU=cJ&0tb|*#!|9|licQIOJmp{@)@3N5hn!pH?Jlkz
zX*yFLUx%8srTn14CB43`PcCGIuq8JPQ>pma?#NWQUNKs0+E{xin9Rpw1-Y9~Sj#4e
zl=88JGZaV`qMY5;C{jH}4)_Dt*6MBdh#K=m4x~!mUg)?}ZJsJqra=}0U|Bp;_qxp{
z@bwk3?lboYODQ(t+zQoh6s*?7xXKJH?y=PixlCzm-%$#b`%yWX%!D(|5tB{hVmMpL
z5KM2VREiyOY;%rA&T6Wa?lr27x@fQRj+__bN+GLFD%<>GFD-)?Qe+5M-DRoC?lFrK
zAa){=bi`cdM_x7>D-I?l+HH+n`{}NS3e~xBFz;!VL$r%is%G94tbzWp)nnG&077mM
z5(o$p7nzY+o8*8Rr~!wa_d9JB+S2kkMsgs>*$3Sk=P%c#W~kCBiw+0Q^@i2l*dI>W
zM=?2+!9fkS`{o+em92Fr{QYe%F$@KB!-_?6(UugY<Z7_fawOU$STbXCrD`EZ(-j#G
zip(fcp?tDbjAru=e~Tl^ZaGa`2fmanmMVkdn6#626IeEE$m)WM)h@6rM+LiWC<WpP
z-kA#qAzj>EE9Bjtem)5|bi|Zp<e&IWtx&kllrrQvWhL_|(MrM<7p^w&;o*$8HI}G&
zz~TsxtA1-h%CPNN3n#%KbB4@ba^MZRyNPs^CCwo_*9r$(!<K1SC6(l`Fd+PyjGy(E
z9i~QPXtFsi_EgrA^w>i!PrZ@1_DZq3rB$Br$y&kGY&Vp8&q3K*T+bOOBnB>~HtI@1
zJ^6Uw8cOmO#gZ{oO;%>iL53L?Iz!nWw2}TsA}KV><(j_~g5YjVw6+UM!0K`rQXm<}
z19KI^;eyMHccVfTz=$@fr0nAGq11rjN4JsjP59bCt|?;KR<Ptu@TcniF{dk(tL18@
zM#wyZztlD^=Uct}n92v^QGcuN%@;#SOGRu1!CrDo43lq40^yF6QL3EpW&3hPkr`>6
z$me32HdS|}$_=x}TMGK}CD|EegDGbb_kbPQu4b>T#Y&TG!4vR1O=EivPjt;zZ$Hp(
zh3IT0=(pj42p!A%@Ltv)mverS?&ODJ%Nyy`GM+*`7j|+jr`+Wt`AT1={h6fC!kf|}
zV8vzv=>)i9EvaR(1Zpu;wPN-(5_M2N=?16~A&=vNpXv}ka+2~EGC7vbijgqkP=K`k
zVWCm%a`}A5!M8xc)a+@ysne)SJ)1w#?IkHray4D4Y(g&jb6#8_(mmSRgTPHsY<tb}
z5dI`c(rt<tTMdU&p*b$s8oIi@R3y!JA_7ydM}vg5#c*QF+D*vONvC8d0>!vnt_ay?
zuHmz^!mWS@!uCnV>L@})smP_|csEuaTO|n5x=rv$**YUvovh%_Obd6~zz?+d`-2=e
z^a(;HY47>WK7XEVMj=QHHV5J8SYtHn4O)n?FJJF=1|Dk}uZA2#OX-D%lWIHct}0BL
zaL}Az67!JNNrlTkvDu`hSj1lHaOtkS5H-iLe#+LXgRUO5#tkVLjtBe|Pf9W4{!xg`
zHsT5QFvZruT;jV<w<}|^5g}2EawT^l4InCFrtBW1&B;mwScfmPW3@!t*)L5(C7Lx=
zn;t98dW5Dm<A|l$Y7?yGt_}W_V8);B;<-U5793_eQG1;fhE={-8Fj-#uxItl=~$9U
ziOs%^t9!_FfH4I<CaO;Og-+W>b$Lg$=V`gebS}|p<sAx@2t@~Ezv81R6ze8APopu(
z_pNrTUFdkh+UX{uL@RA8Ry^iGAnz@>eGsCF`*OC1%UR%u@jMafR)tyu4|b`%udncx
zVL0WJVxxr8u9SzhqQjT#X2yf4&6M#b#};$I+lHkt1uBKOEn-)K1y{${EXTmt@{ZEf
z*wm6HBUS*RPDk@?4@>074JQZ3B#g~&R|+APBZEc7)g2F<nIKa@lCck`Iee_%iVN|K
zqbSwAK~vt<$(t(zO%$Wml*~{a3ts5OqK=rw(wFS6s<&LS`7I{WZmks<2neM~r{L+v
zW7SrCY@TG}e7(t%1i@J=a7bt>@mX^tCM)5FInyclZS7Q;kX-Se)MS8{%hhsA9%Wq-
zzL(0n<Z{WCY;waO-S?MMoFkKsTw99^Ejnq=rACQDf{P`oie%yI1YIyEsBz5}#A$c1
z(#li4G{eS*sfpC73OPqXX{M9uP=Jw2ku2?VCHauABl`X0gx}+L4l;?6FWutPq9bM-
zJHrqfbd&A6Jy3EFx`lwb-zbLyy{cP?j*E6@R7pw0qPft{C47=U5N)*zR>skddwucn
zSf=6Z0FJB*Nw4UxW(E`1GV+BhI4hg;*`aeB9Oq2AV%;20bdpTe24<3E4l(xOD4#Rc
zhC-@d8$$G`OO_ch1O0TAH*qc6BSczYFT*)v!Xo)Cjy%^&H!Tn|C7WEo%m%7tIZ`ii
zL^|oJhq_*|9S%1ciVlQ(p^2?DOoy#BQ%JV#B}XM#9O58H!Gj@&)mGM>lFY4I!D1N)
z!Ay#@U39V|Cr5BvBbX}1Xf!P=NqeHwj8dG}TujnlOOyAH>!FM(8G|!~W`%Gn#hxQv
zZbfBZxni+c2?z>xW1Upb=Jv*7$w{JI%$Wt8@+A}PVyialx2)BSDdbP!7MR%Xkn1C-
z-<046MBP^Kf&7sztzmEwO@j2!ka9FM0?i*Pm?a`R7P|GYLT0>SJl8KdM0dG3YQyrg
zHF7v0T`6DJ!l(1KYMB^0vzAVS4Vb(WS660hgH|xWbp5rVL-6%W!?D-a9(nEc1QoQ?
za<T2n+PUb^E%X|mcB?N2Gkkbd3dnNWWNmdp@Fl9H<RHMNVv+ig_XHAcYA_UtOag?r
z)!lFVai2{n+3ceb!;n6U&l7gkAp%u&mYM!!j5pxGs?%-;izP-j4f_dCJVjH{6p;zg
zqfWA$@sCox6onNL@Oq#L;VZ9oY;x8^o<U}8Zx;MSJ75ZM#E2I?<${GNSGjE6=^mxs
zwMLW-FyS0K3iZXRxmCB@fmIWIspYZ{3+86T;)qVJtx5fcg7=;AQlw|Alq50U9fOl;
z7X{kpDY}VLuR!H&rY>%?HvRoN?ae#-9z{rGI-`!s9TdvxzSExzxrIo9R=VH_!5NI3
zFGWTa<IW3ZM~Tn>e-ypBva9H}g&7ssJ68D>^#m9+qLWbueIxotfZ{mxy#OIV;4G)!
z;XXyZW6kYkWMo8U?zMoQImh_M!2IsGfrQ4^-yOG55B`HhtfPJD2=u4i2Gna!K8Ep1
z@x+H8mi@j@${k^MO7*3_;7VuhIC4+_OHPcw?0WJOwJT(A^QO-qOyusP`HN$#ZtFQK
zct4swJlG<x*gAL^KUpZGOZ~C?RgQ7ykdY=~GnM#FOmvxl@773_cjd_o2;K~;u<!PA
zN*STCLV7bZE|c{V9W*8%Km#fF<OhXlnTL4EhLY!<U=79X>ID9X%=PVNa+jPc<9R1K
z>|~9OBM(XI*A5MF^RX{=47CnHJG1ML5?_wj{rLwkL)AFTCS!;*4A;DNi8WEE=i5<H
zFw6N9&U|AwcTv?cF5hnvV|8Dl*2DYU$SOfuPGfWC_t;eYitm+v_1CR#uzx30$4$A|
ztHq0BI24W@1I3KbD!^25=If#g-CkUO&cwsLjez((y`wnm{Fz5ZS4<Wi<K9AbwEe8O
z>-%{liNgI2bp2Yt{nPAT^N*x%8$(o``<7(&rMVt*u;~7f0v+ENHCaF1N5V=E9f<{{
zTjlKxh9Y?*eLRq&*?%z0A=)2U+4H+)e_(;2u&W*fYB8;eRJ{#jkg?l(H?&9Q;^uvr
zel?=osm*k9UTUx<xraN1IP#&s#elW}>{T-2DrG#sV2-v2P8L0N8Z>_P@-6A4ZguWx
zgGDdzY7H-JZiD9ikuCNlWB6bw{$cJcYZ~V@F9ZK%$Ojvu2r^o>yW-0|C7;^;-N&Gi
zbsYLTt2ZlLZ;jHx^jfb?Zkv~CiZ3uksaMi#=f@)IVsZ90HnNdv)%m7n9xx&LZVR8g
zO<%4&dx!kTS30(?&5zcyeJS4@MZG2w3=rI(caNaiE~)0<tsm=F{oXqjc2N)$yUe9$
zRFls6URrm44z~O|1;ch#&M5;aQD(c^-In-ii+9zm-u}^C{PP(a%cGtB=rt$bdj4XC
z6aJCcywj<ZQrtOLv}j5wj&Dxm-ZkoBzY>>btZ3A4ziv7|!-*Ix_xg=jXU9=DO>{uJ
z^85Al-Ils|?<d`FDBlO~#_FzjtF<d-F77r^%8N%)Kt&InSk6oN^8fwMs*<dGeg6=T
z-RH(1{@w76q2u`MP3_??<jcQ1mu+&(%S5ei&r;efD099}e`sTuD7U&axEhQG`CO5j
zSBJd)?M}`bphP}z{U<4?SluF*iLota<Dx_2O|4+O3i$N`h1XQ}_6+|*Q5^}SxEY9C
zclQtP)QVi_k@UL2<Cm4o(K4JYk3oJloW-wmRMKC2k+G8-1%Buxe+G-Fn0a{_Nk%K3
z?=c5dATu%l>57%OGfR5w>RT?lYwh;Ot0myT=qa1EBDKJWzs90C{gKKbm6wiYgK1Q2
zDeD@|3;BiC_h!Yc{D4D~oD0$MCQr@KGC#@jEVLK=8s+^-GIy9P4xwglHTIi-6w*i7
z?qw^Zfe0ST^7nZhY?VKSuRMj%t4lNjqn}TqizxQ*&CwjtQ{GlJ*~y0Kvy|q$@*<5U
z)NTC!^HPCPQ{Q!&;goQSfKfZ|t?yia$@D`xp-;j>U)a=8U$5*b^bV+UbPX2m>l&s;
z0l7BuxlcaJc_RC@Y9>2d#F;3PJFLPC{`Wy<F`9`rblvo+wqKtKaCK9%G2ia^ozmql
z{W83s=Lr2SK1Joj0k}<UD?j`}(OWTPSL}sd*>D`5cf`SC!m7tT@RCB1Kb&|6C`t4-
zJa1Nl3J<zOxry~*(38DW_{r$s%yvH*B|NnLNE&_InY!>zR<@E}`3={)J=#T{Z~FNS
zA$u=(To`N=juT!Ho{w;lLVbgq<-_Qv<N<woE1w2+J8$l&qQX5xv8ep0=mo<eLbCk6
zBlHgthFqgoo2XcTApS~Z#duVEV?PmV=x0WTx9jod-gdE%k#*O{n@%bRjNXGyGH+)*
z1aCsiAMQNiykipiMkEuxjP7lxmB;pt*i?9iT2$dHtYWplzE`|J!gB5G{=8U;DGeMm
z2D0Ys{k-<#ui_o&&gQl$-OBNe#A!ayYj`7+Ocn^eD;Y(cG$mQ&U!uh~=A-`7W+P8e
zbLOI&fTxkOB3(Bd0pP@ZUvQc+w|+~6ooQW4Ug?sxiP(M?Z$k>NqVBj&EUd`*091m6
zKj-h(H%Se4&UUm3+ZTN<^r4%|ls3o7i~XM4q-TjIKCAibZwUSD<DbUYvO%Q}h9ANB
zTC4L{W>e=#>1*4lQ4Q61%qOkRWFcdYj`l<qjJqWNqb_jlv?(pG@#6uCH@D}*pQ4Nt
z;&U`9(L;_-B7<By+Ui4Q%|J-;`h;Se`wXt#15)Q!-f7IW1Lb00UVt>WQr(&g0|OtG
z1APFI;ucihr|(>LFpb)!l-kDe34?3+^;oBmH;1$u4nxF&XHs-JxxeF;(FjdP)i3{8
z&I7gn`=9ftBS2`&t*ov_%L@}q@-K|zN;-^7S$GvRaZ@GA3m>oN@A4Dart(bk*eL({
z5y){~yKx7bld?y?+7KR)c=D7eU&#QU64bRN{<C&LA>6}=`4cttT}9T*FJuNRa_{!o
zlaj9cc08<<aq0ukx8qu6sjG#M{Hei#u&}Bkm(Mgpp(&~GySS%GaT`1zr>FD>t!-j*
z&~IF~%w!xrkrW!TDOhtISxH*hb2__=RXXyd&GRq2rCxOKs3AEH>)FPz3Vb9@?6($d
z!X!t+-c}&Y#7;M&fJVhx7-)L@T>=~-IU;%v^XBs@G(K2>+`Dfzr_B68fgz)cW4k^#
z)II?NQjs@euzioW!|#2O)va#N=kfpi_doyp{J;O_`G1`M`~Usl|MP$SJ$l^P{Yme8
zW;1`c7nZP33*EoX?P%TbYcnFF>ymt}#yhPlS4MC6{-p15d8A?<K`k2_Ab2cRGlo3V
z4XG6a24Za>Hu`Q<Pm%jNW^a?R0ZUee;O7+wHD3SsKm4N}(P8NRY}R&A{f?P({l$ST
zdioV&?KYY|8|x7_AAe+DOm%;&?o!IB_$2~G*@Sx?sG|={XS=9I!f{^&@V=l1E49%0
z;O5Iy`dT*h(LQD|5#<l|Thq48Z!Jo9>M&MPen@Ahw$z_Z`)}KD=y<GdPcp5ZlSSxm
zbcYF?RZ404eL^bq;n!F%-hI*p2F?^Yd_9Veu#G^snMu>=ZuzB!6$Y0KGz11vhLUzd
z{VDpf8#EA+^60f=?0zDI@VLTf>XJCXT<}9IfBENm4+<Tt;57z?i1T*@^HW6fo}*}L
zy5*zb)QBs;GFMJ5Omd$T2WCVq6LlqnHqlZh`Vel%WbB>yrok^jY3YlE^0$^N${b{n
z+GGA0-8YD;XgE6pP|=buR>6It=BNHg%$TcILPB3Qx6m2dXDQ{yU-uEC0gW-Ml7xpL
zSVTVF?r=VV7e2F=<Oy%SW<)R^huAaP`XbLgAg(s}?d3H-&jP#zB)QuWoZ%w4N>OC+
z)8S1fcD}GTJAb1y^;Dm*|5}>&Dr1`W#9(JwNkJ!~j5Slrkx!Z?GYpo)0{_-1g`Oux
z$AtS<`aX<YInf!)Vf!mX8y>ec53dAOv$4PAJG9Gdf1lCl?mDG3t1))vu6i5i{>*=J
zo39q#@g8M$G5^3=1HuSQq{RvI%#TxiCVBpIOR01kAZ3Ws9~#$|^wY|+qz8fFi{p9<
zl=c=`J|JER`@TOg3BSe{<)}uFWsfmMV9Y`G^vxpi*#mXEKk*9!(x~IWj(CHKKuqp0
z9R~i-pDjGMUDvmnH!b_1esSZ;qenbmI6G2#irl<SGn@bZ=a$VF>X_Z6YKLmtsCV{m
zZhjP3IcdoSicN<ID+tL>Ola*SP%aLSt*;w;-gr_hq=J5k=^YiWma}!vCEyCk`ey#E
z{V3kK_mT9SH!nr%-Orfk*XUc;3U)lcyra{sOrv=c0luac`9ffi$`z9B4B?%`)nv%&
zQ_X>muH9*0;@6(^2a|K&`IH2%E|Gi=JuzbZNa{K??`O4SWDe7S@tXs}`m5O~fRPE-
ze*jfW{6b!Bl1k?1p{ei3mp&R93*dIa1XN%jLqAYn`aJndr8?jp*4M2`UyZ#FF)h2S
zekbzvH{$+F6MdHmSnl|?_8qq)e{yRDHrDaX?i21kJ}ufQ$K93xN&^*nu~)#ga}AkL
zL>=hXs(Ohl^Wz-JHwi+M;XS#&KMe3g=&AKzyDO3XV17Tk@;+OCF#cElA|zVz(V5fa
zk+aIHJF2UF)grm-b9$R(<NCZRmvB|jD;)a}o4e<j9Q`-B*G@`V|1zGv(Tp&c$&}Ao
z+l7>-Ep9;_{a#P>)shvt;;ekX=fA7`AWBl)NZzjqll{oSCosg)z5yTU>;0)J#7Eo5
z&JIFB^#)7`=p3ENvI-S}$8bl0QDD+FnFdJZ4=`cVr%E^X(|vz2FRNS3`v_oHCL_fT
zWc-a&Zm74`U-LGwHQd9%X{EwzqsybCpFsah->(<Fh~0ZJSGE)M?#0?KIH=n=jZ58*
z6|G5?i11THcG-`4>@QUG->JsAd8hy3llX9IZGG6k4pbFkmu`prdq&iR#1vUkN5B|M
zD&a&ATzYDJD%~wKoqV!|d?a>dd^M^;l1}^J0>+knfrB<FkFZoH!z;*#KzsHhv$3iM
z?6?(%{nteD9Cwe-zCenK{v!69x1}$0i1zsq7%+gT(O9wZu=L;ju7Tr57(rS%eKFbq
zr(OS)RBk(O3nCXTsc~U@>)<x4tpJ?H&=x;L@H!R=m{?pYMVQkDej5;GaT4sazIW;<
z-;Gik3HZN)%n^SxcP?*PQW`&~y(t{|FOb=VRc1y0;Z@}ZZfJ=qpJ;hrOzMA6FYNLg
z3`$HGhS<_D+BB-GC3v1bYCiv7rX1Z%drZx8_SY4CH4t`s`dQOI!aFcU&-@nP{Z8?Z
zZXV2u`}+>7_X_D>@Um9qI}ilnls?)0Y8CAP1Rg&3=Pmmjc*b*)?c^RI^vuZiyB62e
zIp*Z{{YGLdjE}h#9UFY`--Eept-;pAn+N%fzMsV#2X^wQ?!Fm2Zz@!rj8IXZvBzij
zhi&vR+`UzE8m9cEM<8>HukzZ%G64w%G72dm+w;xX3Kq!+-v#>(9}prj#OK4qLQri9
zq;>Snd%i+tqb%)(o2%=}xN!Qa5|$18eQx)OUKk5glgN`#{n&UC@6CpszVv6RTCakJ
z+BTS|7QdH09|1x!r_C1(#@52}Li-ndU9#N5wSE+z;N!>d$4MHsBD|Me<q6@Es$99d
zWa_f&y}ZeJ7a_zW5>(9QP6PqxvR;_iZ|3<j{+{QrO6Fivly9Zh;05nNpBMVqz95lv
z%kN;Z6sp{JtNQe`RtY`U91Ow#fPsnc>^*+&rz*3fu__P?Xa?)0TZb&9v+1f4msgKd
z`@JPQ+c;ME7|VAtLDPF=*uhe#Un39o`IcFH?}wkOPomQQeKAX!nH5ZG6RBCxaWl=F
z2KI+0KYUOeA(eg?cAjExJy<bL3e8s0Bx(X;P##gDhpkS%xG~2IMJ=T6oNRKq(-Qxd
z+erk)OvsxvaXxZE;PYyTszJZTs4D^VV)S1^jDJX?LIect(F)AImE#>vAmD(_K8rIk
zNtO@~e8aXUlfx)c{4@u>Q;}SD9kyNk*?#87O^d#KV}+L|Q#ZunKcTs|)%dg)E8lLC
z|3q3Gm!IO#@zKeZ3pn*vrf6B!JhLbAN4)o_v?J*dqf}hdJdLroK9b{5V$LwAXpDhB
zsYmJ~&Nnb5&8AtdB*LFmEN$avi~d)Cu$yz^tt~X*nSM}8c5b?962;2NF<N=;fKL!s
zC_%!<yPN5EQa(8q<eq9J692Hr`oWG6bOhTuMU#!1U(ie%;0oI0S0xo>KvG8{Muk~a
zx}txJwW)y|1_|cigwEw6WDx$jzS2oga}kFu!BvngNi+RhX2MAXSVUnlI&6JburBlY
z?|(Rq+0(c`LJ|Ouzis;6fwsE&%j4#G!x1mSCr38Lh5rEjMR7K>C5b={n)f1n$=hA1
zIhnlVyX=n*f`3J0wl!R9IBoCek6Ax$_>%cZb>X>|$Ldthk+3c>VD3+9H(xR|y+m>Y
zh`QYU_7(rPw_GnvbBe<?d6&EPZgGI1yy`;&pDLF=4ilyAZ1T~-oWs$?kg)13N500D
z5NN>fX)f3zY)^4P4hwL}mMMtjT{jj^feXP0k&g!7<?VfGAI+o^nU>pXnj`&+6X*T0
z{xF^`B=0waTaLdC)Y0hb`MrsL0+<&ESdmSq)v(Qz3fF@(J~HS)09=9IDE)YR?U6!G
zuRs%cO9%Dm`MW_AJ|VWG#Gb8|O-Sd=GeLwm-4+cYm`GCn1e{q;d?fd5weo+#z0E8?
z(A=(w)a5;rOD08H+G7hzy!hY=L6>kk%mqjJfvSGsBwa~gcy>|LYW8}>n^(WE8%nC!
zy$M(FK-QNzk7?xX;8CCOy7}tyCxLhp@xX2Y5y>y=@d<+Eo5xSRhw~3$qVUs1bqB#!
zV7f&7a*kTE9%Ol82ogVE$UIF%9QF}V)~|GM8=Do;M8qclK_Gl`pg<Ws&jf5~g|4Ec
zL=Ont@LSAR`3AwCYuAb4fL|84w;Ki#Eq?!)I3S?o0z}^5)*4&!%Oj#KX$L|_KzU*3
zbq;>7;J@Rt@`PJcCZjfLLTTkE973VeUKYC3x#OQ-+Mx}+q<u{fue(sv2rr#MaE$f3
z3iRyKKst=)V_4()OAG#(=H3Wwcav<Nv47oi_HT%d7hEmoyK>7+dD2XbsCs4<1U&r_
zyVpP%nd(eGw}_p_!5KGluCjPUla+~`5sp9`2FHUO<p<eQzpV(;@yCCiqTiSA2p-s-
zP~BdAiy_#D|7Pn)7mNIj+|=04U+(%RYlZY(#$AXj^JQIUkT2x0v86N~F8&NY5b$Y*
zr0x#drNV2xDWclD-$^WnD*djDk60e8(=h5WZj!%AYCw<LXM|~>x{W3j>?s00vCz*Y
z$6h;sYY0HcyRSwbU{QPN@F(8s&qostK4DG&9r1;|@Wz{VBP!S(jHKvgW!h+OB^A{3
z`my~E*}F2IUS{T(kzjPl{x2V-C%-*1_8#?oug$VG%tZ1YO|D=sNq=k$5Z{1XnX#C?
zU{^CE&qN-;-{0In7YsR1F^3eOD6T4XRk4%$)w$hl+Tj+*?KL<xZYQ%5A~gj$gXKJA
z>p)1xBx^OGGvo=5Z({2?ij9qM8*n*ZDYaSV<N|!NaGiXV-DP_zdeQPjXk&<Tsndi*
z(BQn@e+KjYN<|8mbok%@^wV(DkZzL-L68sGmG7YWvs6V#p?gci7MXpY<4hX^W#=8-
zcEAXD^}ff|qkRENuB>g;bms7$3GVx%er?n#CouabrEPY4U4^492_SBFO%SX{p4996
zue&R4SM8kgjS=nU?b)cx&K|H&`bmgG*R$a&eh*jswU4|5Lo1AgWMNq6cem^tOve`)
z<~TA{ktjXcCvz@Ox07FK1H8An5V7e;;KD&bt5(W^fEQQRceFp{Q}p3~c6a^VZ`AXO
ze{@}gbWCP@1LjjdQ7tvRq*(s`JcGcpx`-Eh;0b1?YF=ZBEqX))<FR_H!<pGfpAcZT
zAv|wq04Jl4_6V2jXh>t<I{Y<`gb-K>*McTai0V0s-dO@C7>^h0{n0P)F8CO{L07zE
zAU5jRzs!rTI&jU{v^lV6YfH)nKDG!u21(zP&(8_8gi`F(gqt>xeZx`1Rfp~&<7<kC
z$L;rlO+UdVvJ8r)B=?-DpL$9wyLS$!S}@l71U*l~OS%8e3yuZ-ErTDUC*$<kWDn?S
zgZ=GnD@?k2B?-PPy<bl#r_byRs>#XUZz&|%3bMrH^aZ&cI-lwN$3Y;Zio>blzf)u>
z>4CoTs=c$L6s=&VyeqZEnfyfh6~#OQf`Z6&JXgzj;sw)jF}Wct;w*?3In4*){zs3z
z+h@^&>lHrfJqr+`es3RB#4~xBTEy6*s|#>MYV7o-fG<^;J9@-x6Fk=c{lovq{rlc-
zf$O8^bPINC-LRJUoB%*pCTFxmK|`$J*wXy>%7F9EFiP-dP3mXlKEy0zDEF2)s{5r3
zOY8w!Lto*3he9WMV{T=cc~h}cTj#DQ#~{@;(%<KaVuyg>`|VB;w_Kh6!ux7W3?x@U
z;n#|)f<@wTAqk+kfR^ri{9M%jhU(^ijP|p^A%@U@MgJ!wKKv`9eEF_Yi8n%*bb}Kp
z&Xi%FV3hC2EMU{Y)kivW`R0!6YpWlYYS-L)dr9j<-@Y~R<&HnJGF;NxJ6q+M0Nxyg
znfj&4bD8kr=<))9SVEVFQ*J2sHUW_W09t-G1`4bBS(-3!8r&5R8Pwg!9VA_T&OnCd
z*PizDfM{PlU#ROwH$Dy!bOh2lbjTo*`P5T%uJaY;4-xjdkUmIZ@;k^}a#%$EklvpN
z;tv@fd3-{HACL8!qhQz2=NS$gwgy03FWjZ8*@d=Sm8L6no7fY0equ7({fH~?iT!P!
zxCHOdD5_0j$gb^X>7%~hVD_<6!+o;c8~uBkue`wJSFSHmT+pb|_w(JmdoMM>64FX^
z$8H1&`9)`aNrU*3%u@Itld;q4n;|!d1G%~Nt3&en;TA|YD~)!mz4*fx+5rh;f6=vN
zG5blfxuzs)laj+l+hP)RbpI`#k3T0l_<u1-eabsCeGGhJ@{G{Ld1?r;@$nS$@f1Ed
zM2@sy`n@5;fzdBo;N~<C)GAyL4NDT@gK$-yu+XHyLr(MUWTx4^0dSH?LtiZG96GvC
zpAx}&^l0DAreBwxj6oy#dqQZiJNJF)=QuL0U$tPW`cXb%XnX4NIonZGM(z&3M$TXo
z_aR&l+>P}j)y4I7|K#+>Uq*M|o!_E-+ii$UtzDYe^0CMS%l~al?}H0f4MOD{g?U1o
zcCJ5uCDKk|p`89=T3WfUU+MWB%+f%WjQfF_YB>{cTX`maPboT4UxL)8p<AwQzgbK@
zFJZSUfckUoN15fpvLQzdnhUK}O!N`vXL&6QR(FuC63vvHKM7hraVJAv@yBP}4o2)4
zH1@h?f9moR_5|DUzPzp7-shJi4*L*tH}aD-Fzto*9i|jO{=M%o&e+w#ZdpgjtW>Mo
zRKYWoe^}T{MX{W?ImQcpl0<1qvtjkWpYzF78h=?EV}JBJ4H%mF5oP>z6UQte^#nh^
zo@~aKXf4DJG^wm8GUa0-u?_O4z@NZ=X~RC6wGIHGI5{51Jb4<52ckl>O9B6Gj4>|j
z12G!l>MI|?Z6bYBR(%177e+fzy|l(FO&@Li`*tW^6g)N@@m?!9pIN90f2n*<hqTlq
z4;^$wS3luOt0L(KS5m0wo%&+fiAWmzq>x?zJ>T~Pi23+xiFMEy)uPNA-t_80=O)gV
z#7BYq;z*tCL};#oiO-3_kwWO&A&U~5WeH+;-|0)db2|X4a_Roi;(6%T@qG24S5aqL
z%H5VM3*rBLnU@$L5om5&TvXQM`Le4xZ(HsIxH$iNuW6WyYV_hg^g|I6k|rM%NZX}X
zH%a=S)_e3zw}zF~(CcwTQ4;u}7r>a62(L_t+5J$1tpDnwZqCnJjkm}rxez;A^dIoL
z!l}=i-e5d_HxcV=f!s(EJ)5Qh)@J{%@&nvKr)sb9F!_w0`KQlLsr?0nLl57oHvRWM
z*RYF^p}Rs{hQ%YTBOc(RILPId*S{kl(dYQN{n4CIS`dEcZ7}{RL&t`7R8w5asILv<
zTu4!+vSUU0Vx&}SuEeh2?>_HRYgYCrrbbLec^F5s2hP>iK8J38HVf|Ruf>G&4_^Fm
z6WDJ}v@D%ZfFpWPcRgv}G~t35W>SA_$-0An$EjF9fV=zoSEj7qJ=X6EA9ctbBHkrt
z$RFc+p1+!+W2l;d^mp`Idy%U*LmU%Sm#zSfv5R*2IXtNE`~7Z0O2yX)ok96byrC+V
z>k-7+LA94ZlqDX#b&Z2!c7J#BIBCy=doVw&>_e-u)Lf6>C^a+LwD5xQ+*7YR9w%#?
zXMaOP7KKiwMD#OJnhGCiY;SRd-UOrZTMSF?zT0DY1CX(Ic5Opn`u<qnFq~Tp{Fa(K
zAQ!nd#-i4nkUYx&1Yu%HCMy9|_Mg?h=|S_z{D+U!q(hB+I5x>Gc{QQ7?lFpR0AzwT
zg~{E_diN|ahX+Vkybs(d#Gx4Qk_pIWIHaeuX32}`b{gv%3#J3HZ0IGaN6biu5LCf5
zU!{QaAAh*#Ucd%X-loW0qj}kph{+{h-JN|i1~$?6jOq5!q#^kc8}|){A;AnZx|ZMY
z$YwKrp8naIy$`pTsL)@ZpA`?g^GQf@(&rt2hZ2J|H&QmxH%A$NCPPX%I)vM}ry~u2
zsvcGeJb5&4{YHa)U!GvWgb%X+#KpO~AyMc|cf!b?z|?RfX#gBdG>{&?aZ`$eeEP^O
z=AX)EVxCz$QeZcAf_^7Mm?|>mh~(<Hz1CF!_s|zEFU3ZM)BJ()7$CY0{Rhi$BUuRr
z{<iy`Np>4*-yG{kOrCQFrC!$D&7i3NEFa{*gB@+3)a(A|?MtEM3zw#a?y*(V{<c@c
zTHZL`ZAkxL`jGF|P{8rmt4(H;#n3~7Z3wrmHC@0IQxbd3(OUe0YL;goHd23GkK3{E
ziW&aOGghlJ_=%)pBa*U3&~=zhV7$<{n(gf<JV&h6H>Xbfhk#+0DPx0DJElO}`F<sY
zdEMBee8>f=j&=OzB)(C;Bvdxx4;QiVvS#Gq)HkSh`l|)-c=#`C4?p>F?A7ZLK7wAR
zHP#9pK}xWcIq|zY1^b-8U2*!tPDT^t8{Kbw%j&={yCeMfI{jeSzJoBS58L$0TzntC
z)D#Px22g|`f#5L0{lHb3cIQ#`gJQ9jnfLWytB?K6TMoNeUjji9(1t31M!MbrwEG2!
zZ)>Ng9g+)z_8rk^m{D6uIPWB4T_hLm#9GhV-KTYC>k1GFmd1r^!LPP(+ifS4uC#c;
znI)4hE-B)By8)a;RAVY6QnXp!U%;1Ao=td*zf+>ueM~lfRIV2+-_kB9w|IZKInTYO
z?W%p=y>V%gX=>+H<f2B|ZimZ9`6=uVJJ%JD<~z(aFI;K1Tsd~_w&5t3GcLQ+kM*Vo
z3czsV!_4G)eVCQuweP=b1yB(Cm!k@%KqIdB0X#1#=8jP=F~aUwvF<{7(eJN~E&VSx
z2Rw4>O87>vP!+aU=0qMHWi3A`5HOAL9#?3UZ-VhHu3^Ruttqj;H<Op}73b5Fryw_=
zAO}wVU`wP9`NMjPqJmn9?`3GgwH??2ZOHSbMEH8zpS|VJ`Rk_Ypbc*<;+*v6T_1-3
zCsOX~2iN%Tw$w|FLD<sagdTstC7tIWuyC8fM{p*P2s2r++pf!K>B&*D-WYD(W5QWW
zZ885<O|ym+4mrg+EJ|yAev7YPS`h^P@=umL&+AH~irX{jix|E-5P|qL!xq5RZ2*}&
zlb=f12S3=deO;s-Z8InQL}gczz93#<X3q-%Ko~(w&eO6hEpcCiTCA5_V<sWi%HLX<
zwY0xtHgvLFfm`X)3NzGma40qz4_GL@vMh7{Ifh{VJm}5IwP!Lz>G&ilWC{;;=!MKt
zjfT%IC{8{p76@SMpfn=I?ODHWFJ+d-j+X6yi<d3m5wW~=@*~GF%gIsToFRHl-9@2u
zp51iB30BJRs>t43k3E%1)7v?~DoB<+i4Si6<l#*x2o#R6?_0>J+><?^l!zuNDe<gs
zP=g&5^vo6P$G@<p$Vt>e>A#aWSEl+=XG>6Q3`V$nqtOGmsPk_d+)jDE^|F!HZgP!m
za86^U&amV1Jf)@g1PKs9|1c^0SmCkD5^`3c?C5;CIEgI_IR>emlw?Dcy)9AqEd7Z&
z3p;hr-Y}~!9pR1h8iy^3Yn;J?);Hu3t}jsk&6c71pL^#x2-)gbEW|f)991iM5r*js
z3ZaYRQ3o)Gow+kT?t7`#@=Yr@P>tz3IjAJpN{b@^R|wBbZYU2d?5zDpQrGZw=5Y?E
zx(oRU-YoP2b|MCwS8JQeM4l9Pf6^Nrci%tH0=9!N&J+#33Kca%$3D9k9A+?skK@t4
z7=({<Q0fOCYl<hVqPkFOWu8GR>opR~fxVcd#l!ab0q%PgG6deNl&i_(KjQ4{b+^9Y
z+Jd^)5%prdE=WFG<?&UM+v9Qmp+7zyC}Z|`jS0;kEDDtBK$bO86v2jnfc^$?-|J@#
z8Y}w&UB)9llNT^!W>0}T2Ka4HWc6hf<$c-d^dnX(q8N>d>8}UfY7O=oj_`ZAwr@}<
zSG@p@&p+_^Jw!g6^yJTy&x)O82<F8<5Hz4^!?>l#a;K<Ift(XJQz<0-*{>lG^W;A7
z4?qX{eHb^7h4*d8f$roqp_h=l0lXbLxW{XhQ<T(CCSsty2f7L(2s{OA$p4b>>%gH{
zCsYd*w4`y_r$5;bo5zRTCW-d9Q<b162p#-Y`o|sb19dm=P4)n{M?H;4s{TMny<5cz
zF(o)G-nFGEBP7BoS~?YbwY*<@(kL{7sCFJBr~7coS2xB6;|U2faQvD7j;EivcVQ-t
zt$TpBer>pGe1!q#9|~+&Ux_%jb%aKkj94Y>iaWB(56!2g3bp-9#SeQQ?#oiFq}y<p
z+mV;UG-Uia7b=tW%hwnRCW-U+$Pk<U;7}danYZdKRVs8fsHSp4@a)vk@(A^UH}F5s
z6YWB*a`I7wVN<ah(wIr|4a!e>np7%-Q!REs>qy8usuLl!x@E?x{ytms`0;CfaIzCP
z%)-pDi$l7)F0!wQ|GH1tV27kmQ4G*+H>9MSif?FK8>-5WH=vRvAA;rP4El?-Vdg`5
zr6)1bEnA6wbgMpXv7%5FxDo-y(c-HVh8T1!e)|R42R3fr2tEH78xUIdT@MPsR!>ts
z&9@(Exux{J`r&ml??Zz{n_{!@8z!Ahwa&n6C!tEb2=EG5DS`Lb@AXa?5~_j>yj5S6
zh8V$W<Z=tobh6alq&Va-n_rO?C4z`iJZ}x=ewRpmZ((IK<ghuCDG)op9+)@uym5VV
zTA<%$M+u!(3(F4-4hF0ukAw`kHoJ^0g$4-xesIuH|1!}Awc}0m?HzklkN&$N)e5aI
zzRgu(WWGg&TPB6nnZijU(R#7=O%Cfcp6=Si2H>z?see^}>F56H@09ic_V3PiE1Lm6
zGAiF&y(dkNogPjBhU;aiO`ty&vXU^bD@yqOv-dV8)d|`gR-`!~%YX*1Cf?fbz^Pyn
zKiJs0{g_??u@YF^XMw$E#(<2%O+2uUdsoWJakI_cjq(kxZ0i7AHnxLC{@{<iZwR;<
z15+S7bB4l6D8{N8Ybxt(QqVYSZ&CawEOno8l{3fp({5NB{T@c^(*qGmrCmKAgm-ri
zX972#f{XnCay=}*S@syp$NRxVbEu7IpR}rs|HR>0e8o_@mQTcVtsGa@{iBgfELXVl
zB<mjrG<csJ&_pTBv@bvzlJJMm?Z0lOl_)ZveOV+~ADQ|=<oWq2K7HP2@_;9k{`sk4
zl1mL_nNvdiDxLj+ekeuZLjBo+e6MFg)8+G&Sdy6lk++r^XOk0Tsl%0;8lcf%{!|J8
zE{7Af6YVFvG3+R3f1kIB({nRy5N#B{IV-gkJ(Mz!3e<qH<2Wr@bGKk<1VNO2XdW)f
z$j))um`&fJO~`9&T^XXV*seK0&?y1vtj4!`H)sBfg#)6)$MOskksmc+2IGxhT8lI+
zxy%v~<znQojiU5;CZOX|-fW%ibQsZaV{Zx6=(kqpc@P+)WiUsLI3*sg*$)KQ|0*3y
z&;~F0kuF;7HUMG<II_~eob9-Om&cbRt{=3Dy!q$1Hf8Y0eq52-P;+KGjHF8s>RkzG
z3-ce<j}>8!Pk{VKU9uTINKNWjHIyeB5s3CyiJJE~xW%TFe^6@RhaokaHuZ)|E}7t$
zkPcEIRmspORCYF{Wkt_ujGi84%SLgd?nRJ;rtlq<=9596!e=Q_6>=cmEg^lj`#;@2
zN|o6`(T=T4PrCi@|EvCw|7S&z?8c?^1Y!J2NSLo=0)O?{qJQ3Scnoz9__zzV)W^KP
zBB*84wF)<*^h|Ew#zDW&z3_*Ic~b|Pnz)86^!vju%jf<ENWREG91q;NSn2&L4VGxL
zHxA7#S!I60=bbIpKORw&j|$BLq~7gqSZ3_j(&?M|Q0h#A{KHYEzQ+n|r(4GV1`BYZ
zuPCh-v8p9D^6XexhR7m8QQ2;cC*ZRXej1o9eG>e1H?C$5AHOa!#!Q3Ys9*HA1OehI
zhrI-PI@G^If0IqHlwMGf-j~=*9CBPGUy|>@1rf@=(kq>yuR=c+0XDuCjy=)AW8KuW
zT`SKGbPK<;yi1vT<TEcX<<;zNv~sMUUem*F-r>MDdbP*Ij6}bwncqWbG#~rDWjApJ
z$K{ig*p9q<(8+AQ#gdsd#hN2!Rfk&;2H!5SigYgqK-c^R_XhlQ9H0iK1N{jhZ~@9I
zd<P^R;8y*5WdNT%^iG+ddEa5rUSCa^SmCMPs;BfS9_b=G!?ynLN3`s8)$M<T|AZuM
zfhwlA8xNwXD9GTqRX0le8>~qtn%aA+;aL6tKp%~L<Y)F}u?%PUsC?@4pgzXP{L=Wg
zB?`UC@!|phw~vl~lW)y?b`|{lf^IL@{8~Bt;Y|;kJ|Ufnb#@7E?EEK;cuZ#w0yk{B
zTCDzKXDJ~h`rT5$(@xn)lquk?BY8i$xXCfp^w^K<a1$;=3H04mxp5Iizk28(W88j|
zxks$je_}pmeFTfG8?_C@h@fX{6JN{I%SR}~n^r+Wc=NgOpNp}HE`7y&_0?~>^_qst
zY&55<Nwi-j0F>w4lhDWjDePZsTx`8FsK2{s!+BPogoy6zP3oR-pVf>bdpNZ`lrv<j
zu+$6}8$zx@RC<3WkIYSuDXBI^%NJU*%sY098*}_Y9=Ip-yOJTVcXmRsbQ+$DYs8PS
z+yrMoeJt(Bo-?iZ*CCC>O3Jj$1?GwBr?van)Z8vi)*cUX7Tf{s`Qe6xa>@14o5}P9
z8u=jiM$ih|BKSDCd{*DSgLTZdPTh{<mH%~bCyyTDzYe1Pz@gE!a9?90xl>lBTm4Du
zG$f!6;fwmBMe}qJUpv|po9cX09KqDaIR!+aJL$Ego41!xW+*vZO(A#3rjG7SYfoUI
zZufO6p%s^(B#>4fDbQ6<%v|GS{|ikAJlUK~uKsjsgrtpowEzXB=y+Fhe#$zKfP6d1
z5rNn$!=+2d573`n-57!Q`idKQ37?+{uYi~l@|5z|0aIF&fSUNEZTPo<Ly#${6crj5
z{O^|s1}kUPO&z7ACo^atc4BJ7?)c=;JSq9#r<r~P2y@!jaQ~qjyS#*R0XQ3=j$?iz
zlWF0PRe!ami$KaR0b>&)`_O9p$zb(9B^M&klJcXxj&IG|mB4*e4lW11KQ(lR0Kf}B
zkn1fMK7TZJz7>Dna9CuV!OwioV&6Ef4)(zur9>x%c(;gyP+LiZ%|A52P;J(;FCFrb
zB>C@upiP-^9zSiK=V&>zU${2ToHVlxAeezWr0+Z&OCh-)i5MU=zUda->GfG$&JqzN
zrrj&g&HmWvfodAEN7o%!RHlu&*#Pf|YTi)5N@2R#ps7-a(XfLw83&V{a?bhH?+A0g
z(i`*|ooE<}RkbQc7Y1buQBOmMOu`L^kjP=HtTXKNn=|1X#I1;{=&KC+Eb>LhT~586
z{=5($uZIowkest)eo(#?EqJXF-<9$|5`3aj=xJ+8@7)0y${}d^ecd(l`BAO=-s)U0
z4H|sdTgM<M)INljwJ}kbxMXz`PIz}c09vgBoAhamY=1;fVSf7?aX92#Vk!H-fBz9y
zd1Ce6nm&BSXw{+nO?kN7!Q&namqr{63iMTl<4(YizNnP`7$_X2O?wsAjRO?J-`j}_
zF$0Pjh+RDreR=td>CS=m;hyNVV#}ZaHnsak>D|1jH=!o@=>`%^XzuS%FeEnc-_|HW
zB(?t6<aypRg!xj@(|A4nhH61Y(+FXZpd+Ew`>XAWI&KXU6O;@}!R>%W$yKlng*Ebb
z7avkvVacy&O9sH`)5aj_H!F}Xa)}Sj)B+T|pF-pJ$n)8wkiP8#uQL|Gf?BD!e7+W3
zv~J=Vp<K#XKQk)lAqt*+wa$0B%r{z`p;br{wMq5@*u%K2?|&sIx;$Smb{K@+jd@Xv
zk2_t@gnd(NLFXbH&)oovQy<n3S6BoVw6f=xxKsD~Z$$!E5%;FhZj|aa6o|glPct?U
zK4kRp9*-uW0|U04nT6N?qeKMyH96Dw-vbuiYojBQJovpB<5o2sk6QOlbeLL;1SqBQ
zZ$&{s7(B8uh|ifc72SP%%|$<j0zy0fl$HO-ub*`z=;s<XHIdx86i}$QYihRTq&l4R
zG(4110H9+yc5eP=rsWJ|J$ut@>wEJTION-7thH(qZt+(y1=C#;vQAHoy5~?29R}=!
z6d~Dvj+X2{pTRdBdikhhMApu+oc})it&te(O<Wg@5*O5HoJHB<+q)bCs7J|Xv5>YY
zz8U9<%H{)_jlgG*N5|Vq=X7^L{nyVsaUF}?Y@y1;Wq?FPl~4~Rn4>2kL%{`wQuLu&
zcWEG@nd}+<SV3tCP>*Us_*)P*+?ql2@zcVQqv<~D{mNf!CrDy|ny>&o0wxC|aFn-t
zg@H^tuhd7HpLX-7v2meg6lN>k25H3Aa+e?v#x#?oqT&WEZLsOWHx#i9XmoRJ^)g?2
z5s6EJQ$yw8=`;94fS%<U4!oB#mnR{U_+?L2A$&TUdy1IT{-Cst;$?Fr(bmhHr}z~c
ztVh#`5oE&Qsg`j%%}*3Cr#B>4^AKJz&r-9(Vqcr|`OhmesTkfl?uJ+`hoPrnecp#R
zM%g3D{vH<^Sai9-*8$#8_{DLMjow$LQ``sDqwrSs0FV?^QK0mH%u&rlgZ)}NLysQI
zch;_ZdI5-Olox@+S_g^yR}?<Rp}Mt@LO|#2K(??FCF6Ya#{(AfXIYbQp{3GU9M>NI
zbOYF5dK_O5cDf+?;6(UXx(io3dmmaN0EdV^FOO;3P4(cIfnVAXzrTajsQ7dWJ!2$i
z=L6j36BOM|8=P5rZS%neeZ!?c%OKZAu7kWcTj*MazMO4(aU%6lu0ricxBj~wPy_c1
z6y1x+7n;1BuZ2t+%(0Lmzde2uNZ|0=vx{>PDircuwDsofeo`w48<~A0=K<6|Qs*M=
z*YPua_uV~#!Dr;Dt=O&&U$9~a_Rra8@4dPPjf3K?+cg}5#QK5;#J{@Sw}qc>&&Xmv
z{@Z~3Qzkr9^*G{=YR!GEq&9o1Y|^Bl`quBb)a7!J2peKI(5{h+5Tf*KY|yCOD6I7%
znZzS28Bp^~_ScCqO~H2s{Bzor8~ONwgtOL`*&v(OBb&j#f%t2mwC5J#P~JZui`SYb
zG#J=-?tmU}VOk1^#$gJVeJWUS^4%_eG3l`(uht@_t*%09u}RhWJ`v-Gar$6m$(MF;
zkeY9;oP<fj#eV49j_YHDu(6SG3iPV-o!qNmqWI!}zrx7X`ye}1!UY}ZEuYXkHj#Sb
zw!E$jUAj;*`L#Md400%q!<g(4X5%x=$DXOytE=t990)V}KnDs8(t<Bi)QlNCK?*`9
zDte8@tP~k|aAqFqNQaZR*E4R-a6rMKP5#~|9E~EtAOSS<3cDzQ@-{mi^71mDl_}-1
z`ZFDQRp4#EdTuisV5!Z0L8q!+E=Qw>ztzFyh&CPYUD+EE`)Vz_4=kN;Nx_$ckUd-w
zmfpQz0GM%_S$Hz=B@-()?`<w@e@8dsUt6s!{S9TmX2-R|LED-T)dCMKQo_yTycPf{
zPn;U8fjF-Dz4GAR%o=>8L=MtJn!f5S(`OIz?E%mo@W%>@TQ+_`V-cJBuyy}JS;MjK
zD39<z1QrTBN&60p3b1tiYiC0<f#_Sct0#?%c&`mc*$9nK?1H>v!gkFQxS^V=Kz)3+
zg1|g~T3szKGF<Fm?oxCCeOBoOs<XVA8Ij98O+P3#?^8&^!M6`?kE*-eYFcP7KWn7y
zE%0QE+iUy5*9P=I=1y`l1BidZ$=5NM<BQ)DA(<cvPfs9=)goNF!?+$O#eFZ>?*!<e
zfODlcgY7vKP-@;hVUOyL5p-Gn<@r|qG4g=9;a17>$Bzj=+#7hUy-{Hg<gTtyTT|Qg
zhKoW#JO50+Pi~(6hP5${{d?$!a}3IqUvM7IBE9gh#4*X`?uQA`!y)}F-Cg^k!kPQy
zTrDdx&TTEm<prOJ$K9%jN3)Y{qj|ywA`;8~z~peTu4zApiY<x<3SC_Bui)5>PcZ<x
z{-*Z(T{Kf%y6R_F=>SI~Lspo7(z@%J<r)*G0_CRXNNa5y-Be5xvFeRJu<@t77|(oG
zc39h|;XQTTzK7OaOwo&O-&s@<5BUg#JH4Q5`W}ZgrR19bK*!G5Gxlwb?pg%xhX&Zr
z-9n@p13hKf3%E>7zC3OmyWYzUC)lCJ^qH9yDrYq|iBA{ehqy*SIt*_nM>r&KEOCBb
zYfRnV7IFZ(YuSQ;#^x8FRsZZ>$n66m-X@vXl%CwEdx6J*0_X2+0`(VtnA<7@`7y(r
z-T?4u1s6ZCQ#XWX*)s{tqELf{dc<hyQi?H=0EiX!d)*aaP(N`AF~}Evo8VI-Yn40?
zDY;?>^I>M(?w7QH8c^5w$rASOE&UDp;E-E1ByaU`zhMj8xqmi^+-4&wiB5os*Uhv+
z?3j|N?8GSIpwtl>E=>!@%H+lftTh%d_l%$cV&0#T{&dw4F$;j%qN*M|#y%7*LKy3-
zv~i=`cF(QzqfqwGU5uXx<LV%`m%WfD-|q>BH+wyve=B^I;=_ucVm6!S{qtT`1jWMv
zYFG92p&;o={{LLPIj-w&`mG1pnMdHjaBwq&0egxHt^rY$C{k?1Ruo~t5Id1#--r~I
z?A#N$<MA{cdjg(;*ZJR@*ijX#P`^57Z;AZA_g(9G4D(0JtfI?^xl>WfjbDGJkncL*
z`QKy{eZG6B#8=`Pm9goY9xp#yKWu&;Ezj)8zct#Y@T_-U<&vM>-oC|0Z!nFZpPcs>
z^z+ykw9w1L?#pe}RHy)R?(Qmccz_3vG=-SCMC4D=>4O){t3GP)xGaiM_!d`AWM5P4
zuSc`zaH4A?)2+kvR|D18=%3xDn<&-W<IW(<`v_;zcQc(pG{jFX@hUlpWTDl6YCct$
z?i!EW?pN<dffQd?UPlg@SBA~SeeG1S+`$are!ow%APjK4%Q!HCz^;BAJ4|##nnt;!
zHayo|K4yHv1H?@{OyH=u_S{}Z9GzH4WUF~t-UGiA5XY5u3_*I64{6ZH(l7lXED>vk
z`xT9q;*~t9(+Wy}bh$_#m`PkND?IhI8Rl7r4e(&qQP!^{J@9yWc~OVA(e5Fpok^b>
z-4+YH?qvjDs#yKE0Nvv^y$4+Ju^Ye5hmWjLScmMV#vhQRvcDEk)PLpX>y`nHLXMx|
z{t7?7jJITOf3L=kU=QvBDHUi}hs6$z<7|oga`ojexUzQIvQkg*5gUC!K6yFC`jUxN
z5VtJ}z0@vPbaK4&s7$6wbh@sfI@F8d9Qhz&!rNI5{*Va}XBL+%JlgloFRAk33f@mq
zhT`vy6}Gn79Qm*==ZU^PnC-NeOOs-{cwjvWU>b=pu&p?siE@oPREW&@`?-On-EFr-
zo`e;v#bhfFIlN8i+MeigvHkq8`+h(*OhqgGa|%rUZ}oj|(rzeRuWhg-g7Z9NP~Qlk
zZDzk4y<ax_jt`>%{#9leiKGQNW@RSdWKXT??@aM5=JoL@n<TpR!K7ago0Rtww%uy5
z3*R^Q#~r~>jE`7&ChA?#yoM#$#j?e}NhmkUd%sCCv6v2gqVKXpka178AW$>5)D3P<
z0-C#{tyk9_(fL2v3x93|5`3mawb*{NXuHhS(=JC8?wv!tl!8RJG7bbU`%Vj7=_v#`
zD<y!JTy^~t*=<%97FYGVh5y5``R5>MFN9(}WZK&qnZudg0KRm9rPHz|-(op9@*rJX
zULpO?RP`|IQ%PMb_q=<#;P8*vplUw|EUwtDf4yUUZz*qmo)$VgJ5d855>HPn;gQk&
z?~YZK%6q|r!TTs;^Y^|ksrreg7Z7mI@*6dTIKBc(b$hR6tX+8L!?~o`uL)t+=YGGP
z;Q68U0EFD5cSZ4}m?0MVJo4PE)B&id2qqu-6lmwAI`i{%v8eY{o>eiN4WR5ruiNOd
zZ$8xtvx|=~{+%U*L4VQ!#PKoc>_d3J8i2a+vmbVR5;JEaMl~M5x7Q1FbU*AKmXdPj
zqa0KI;mPi6hrFtwx0-&n*t~9RGIXhYi&*~}&L4tzmy_JM3aGZH7r#9WGf*Qu9X>m9
zcX>UZfA7QPY_vxCT$tT_IFPs90i9HVwzN!jZ@Yw)jV!XvhaC)W0p|j*yVJ)VUeiCo
z=`KI?$}cJ%G|K>I(x~?+kh2PEp+U7R`-^|-WV8Z-gRV@P|9#g+FW;Z<{_dZIH{3#@
z^fwtZ5s+yKx|DzaRpat<1}(Nf14<=l_>j_T0J!TV47K_+?|UqW4eT|K{wmFvhKL)w
z1_T}$Ao`|Q(n~X5ttKl;7EZVa>KrY2@N$rx`n840;8x*%HLoA$T!#ebtrqCl;c`uN
zP$ijLsn$B1UD5q2YPP;4$GTyGNYlY%6|6uS@95Q|E(Mfb-M;Ni1sarjFc#=3nU24#
z_YH5^>2d;g>N>jKP}~l<&Gn@9IsbOwl-u{d!R{#m2!hwZu%9Pu0WkH)TyEv_=%?2%
z$?95n7Tok^2Ys=FGs!enN9s=*j`(l;GwQ<@8}|1umB$^R5A#RhHjBN0Zx>59M;D}x
zxV}rJVsq_U4a@0#!unq_=?m^m-7<;!r6#{%Re5J$*qbaAAs~KGt$_XQ8_CP?ayNSm
zx3|N|8cWZ!6S9_&wZ7E9B#9KUzU-^BDQu8c{M@q{-sS0XBG;A+_e6M`uu4peitqq_
zNHs8lm!Vr|`ay}a67`&N>1lpP_Vk+`+MIg_T<3;X_&-H?{3F+kP#g?=sQ%M``cMB0
zw4FdCu#C(8m;dws`S1SM|F)q*S%KWfx8XgRS4^XI`t!Y5$A+p!VtQc@rJ_tS4^Qa5
zs-)ljix>ZU!nX(YjeXXAZpp8aMGG4$a>y28fKWS6e^{Gl@n@xLJnw!_-5{1YjEL4{
zGnfM^p#v^X{1X<NymtgYD(7P&LYbJ)bjZ?doUgzeW5|hATFn1Oa?H~gr#9<|58D1}
zSY{C`-MYB!*mGgS2+nTXHdOR>=G1>@N7mwlpVxfK+se27x1s9dYPhG<@0F9|;QiZB
z_5XE4#o*^ER1fls4-?Cd%YFY{)0L@PmI{M5ohb#|t3AfH(;ng;!FE=@dC=cTs;zd$
z-ohCS@I2`Fc5`ToyclcS>u64>W=x$gHj9d@eO_ABQF`Lned~s9ZVjqYj5*G~zqIO?
z<mr*u?RKfg7g;hJh=2A$LrANVP01@Ts1E&cH+3jIcc~xc$xGhCMeuKe4Pu=2ZF~T6
zQhyJcTPGg)=I9)s^z}V8SuquJv0y1sCMK_C+B0z}1HC|C%J04M+4aQVh1igT5#>d2
z(0Pd%S|e{i0VtqsJ&XJAwkw|rY&8y!@k?%{Xfl`C{<BX~7Azuhcn9l;HkIou_XYUd
z0`E#<(Gbi>erLrNVW!t3V-^%xFS72rdClEkK6(6F$E6zE`HW7Mi!mQrVd8F+%_zfS
za>FI3S<wp$4UMbEAE)@jCoNZ#aE~HPb?cU2DR~`CCkzGBlU6`1SGr|Nyw^?fDeLqi
zYgc-`FQ>&klPTdoK47vFR7B9~QyU$(9CG!*mhTpPr0YpB>o}og<*S84PxYT;qdLE}
z+E;UXK<m%Nb9%|iz{0VA{JHFJ)a5&H4wo_|UsyB~-rZgXjb1I`aM?f8RVH3bv%63p
zJ~-=xiH5X%axYv7>Q<&Loh4bEq2JG4D6{PJcO=Nck;ZSf@DxF+Km6!k105u_|K6dR
z%D?Zr@i;xC!$=%SiknM(tadjA6d(axQH9V`gE_cUgPqI4egEs>DnA?lR*%WKqM}PN
zS?z<%g}J%O#CogU<g)Oxb<f`PcX!+UO$q(Tz*g`Y;&QT3TRepVXXURedL|F^3189V
z%MAv0??Bj*>O2-Zf|-~8EeKTQzP%TNmZQQK^^Qw(_N-<vfh@l}_5s&Tw|>Ph7R{B=
zP=`qTau4=`-T4;0<yt7}hUC1zR#%?zN`>KJmqU$t2p*$%4Uq78zD}#fok>F33{IFo
zxgPy6^~8@n60(~$-Khov^Jq1<>+K8O0<-({*V4SM4WZ6<bhvM|VJcKZP0iEJK>jcV
zBZ^+_>v-N%okYZ~DU-z?wvmBeU2AQ1f=1ayBzQ{v8Le+HcrT0-jI|1_`ce&F@0IXZ
zR6KbU=MO4>S}ERkJ{O;PAinoOX7MrM;ZlGR<yX7v-4BQ3K@(;0tC@q&emU7BVy7=l
z)fzvc_YM@+zg_-}!Dm;Qm7W-E`}iMW5dTOI74JLa$-Jo64wJESbW4_JHJ)GOn6W^c
zP7dJe-V@px_SK{E_%78-rwt~&hHHoJXl8b@CeE}ku0@tT_e_?~=-B+Gu+qeADA`8^
z+fW35*k4qZAEEor81$Bh)bEUp``V@qV@E3A?Vf%4fz*B<p4+nxNnB0Hjiv{4B=U#e
zHdD#HqX|`BGd<HC7K@mh0+bCKmUupXVO}DTUR!rM6WS;EJWpV>5UvvH=MJf&PqX~K
ze<bU3b&=|4pef>Y?lpZ@SYcf+dZSV2WZ8WCzWMk9Wf&poileU@mwdY`fR@hJ=F|3f
z_aDvUkO4O%caPic1+^{yPpfnjU#q`hBb89rn)R(g@#eKVW6F1Z;W@H>;Kaa&N<7|A
zzpnp=09O(6ZYQ5*S|~6jc_6NG&S9n9VY{$j@3RMzl0Rq1Z%+NHvSUyDEj2Bu3%p-o
zUkm4$zQ6HmdiycB@?baMao$p=$O(G#Oj-ujs_+&*re7~zdDe(Z7aOU+Ni0fY=d2*z
zQEg~p<J>(r!RJKW2BEUkdn5&)c(i;{e^hRZzzg_5Y)LZYmqWIy0o%q8%1I~{*UTk1
z;twH-Z_4w%9qL#y?s}9g|7hWuwH_H5(9WI?KV-zN2eaZTw)_(w4(|V?Or;2O%&dZt
z$1aFWHL$jt3i@pYqYT{?`zXAe;5IIk)Vn_(o-IY1_`(E^ci-Z%lk35r`1GeWgv@}F
z+t+F~r8uv$aRwc-r3Ae?Ac=3JpoI5^_|PeZ`7W#USLkcuw9bSY;iXWu-=XjW;`?jw
zMb-b-0wMlXRH*&GeX!Tb@_ldUvXx{u>OA@ZOO}<a3Jtu%`?07jmrVC)?gnRRBwbUX
zuRWDY-vZfUlyr9Y>y@4|CD!LTxC3o*Zf~!@t8nh6>0RFn&@SS6T-yqYj~yx*(G46Q
zR`TmwaJi+ngX;Z@u-nLLL$ndh?j=0*wB$H=#Tne!7SKxe=oki=YCvr5)#%C$Lwo({
z2OFjlOaN(v-@^PYYeWq5G*NI?+y21>GCk?bMBa{wty&y=Kzu^bQhTj(daz#el~2=<
zc+FK7xIavzz7G>8a(>~4^c|^>24=uZ6#KY)KlqI3t4{=8Tj6n*{B81N)MVpmLaeyH
zyW8ceKLYgfFU4nRjuxV3&p-XG5r^VU-Q+hZy-hrA!%mw+UxzC}RH_AKL6cH0LzNV#
z?E~`GM5P!9IhRbCDXVbmM$_NchU*Q93rfy!W4xcTF<|S5HxZg+C@Dz{uPz&?-XaQj
zit}gk0>|x8d*lB52PIMqT4QoqRX$Xc`Ck8qX8{y&PcQ&!9KG4uUC)T1b2y2<4?-eI
zg+A|F-NRuYHbdXNCA9*oSn}ESzBpX**!fh_`vB|JQ5dVW`2>&8<8hV$ZK87h-Ng;A
zI4@Z{hTwnwVTl=iwR~dF@)3`ngB(!^#<<Yk5{KY>@L)~+#@!?JCxw`u`7Ltf@2zjX
z28igs@%4uGcqAp>;i+~>^ou3;+_=ViddHJ~0PE?AveA3e>^dMs{B9iaoL3kXgNEL+
zF8G|BEw1f~KzR83zMFCw$_JlM@&F&k;ndbKx<!bIfA8pgfHyf>6V{@`hkk}|2T%1m
zJdLZS+>1Hts<e0CP?x?NrMO16@KFrdB#Mc3*uGLt5qQGHUrXQo)DaE|JI{Mc;G;V2
zybZPqnRk$Tanik7$a(e_UNDzcwtZXYE|KKg1Liwy#I4NfqjUT5s*KXWm8YG9yM32m
zR9E~dF!ACPeZHOM^Y524jY%symqeP3F)cMGdxoDpzmn4V=uP#voMQU$mwMmKP$&Wu
zvK;_hD9-JhA^)ZgdDqSaSF@jE;v%b39S=iZ3nSIY=W}901Q((HuRc4cyxEjJ_|qBU
zKWUKYizyc4AXReXeT>&GF|HXdEQRKVyT?#_&iBQLfwMd`d(WT$xJ2084vSB#@OtCA
zf#rNEQujhir92-#IWk_MkYAQ4`hp0BCB^mp?ALSDze)2*O0Tq0#QC72KU)Deu?M>?
z!Y03_!3_0w6rPOmk-Z4P*qvgs5NqiQUtM|en2V-os7n7~$NRiI?&0kyLG+UBnxo6D
z--<YOoY@t(Iw$7fn#+%y@IJ!gbB|xJ_8I;Up{zp?-_9u>+7()8{i^SySaqHdRi=KC
z11Av(oyk`I6@a`zFf)SC`<r#C0P=O;=R1z}gKzvbI{u8t8@*ww`X0YnNWEzQzVEN8
zz|M`;JfLwLB)qT;fd5^2o2Gt`zo%E|!%R=W)8p|2j)Ux@x#=&S-*ycq2WUtmjQQQT
zcX8bxzUN9i;nKZU_*1S}>v_I@Uk@k5$v%IuWuA-^RWA6eZiekfAA>%Z2PnE>F?}Ki
z&kby!A&-;nP0yU)@3cmb3HP)A*gSFP=(iK(LB{k%c>eo@o^bFDggt`O(^g<tA`>1G
zj_y-<7r0m<`acKqMh?kC11ms~{(iXb?49+b&TVp&+`cWEi97eVX1wI|>t}9vie91<
zI>J@3Svd6xDAZsXo+Rbw$7b`6`x^5gUcmZ?BH@?Hn;2q6?qS510R_PUyH3wK`lwys
z`ImMq?pdr<j!Mh7S=SGLTn7TK;+uvQ>%{Z7KfjCbWG*N`nc(MyS|cBGj^mM1QW<AM
z*|yDm;6g6{QXAtnGgBn6$n^xQFy$!cpV=MlDuC5+dMg5=55@Pm+p$9_#YYh-z)(7`
z-<z*r^F25uJT3nLKvQiWo>YsvyKgbM9|`mVk)C6}^%OBk$~eeW=7}|*%T37(dhQh{
z6f?9@Q8)IgJu&4;ohzi`fzk5G@k>yf3fXD}A!=88nf;t~f+?sQSbeU(dv($TOZ_ck
zeqKKNe>sS7SB9fc<>P3D1nO8vxPjE*g06tzsy}V6L^#=aj}4d-qO{Q>ngqoDvI<2j
z2;I+O8VtmII2x3~omMR%tYH^~QH1PJ=|~q`{TN68d|jFDx_c@0*btT&1hn5c9HKuh
z{$cbi_4oG5R(B+fV%fINg=+uo0SvNzryHO6ixR>&PJD@&qcd&??ya8WAGw6jlsvn`
zMqzISr#HeC!U=5X<a8ZFb`>8@kJg=0jsLNDI(;ry_8*HU)1bjxUq+Gp{B7^E^1U*V
z*z(cK+_kjo1Ge&+crqOa_21?R3`=aDp1->;`V7=DMc`Nns-ULShNw`&nDA|dgm-7}
z_kR93Gjj$!8O0pRI}WIXQu~e+WpxY|Pih8>bU+qU2d#xQzc1=vg3Z&08Z~?*hvzi$
zF&(@-U!$!JwR238K|t99o;7^I8g`R^o-ZijwRo7){4v(al{zMO-D_WsqQ}o_F}Am}
zj)N?qXiPP>z*Cu_7jy7v_mQm(Fh@S!>NdZ>Xk&$7J^bO=|GSMysR(3H+a}hfCUK^8
zQh2>5!^2$FQ+8A8Uhl$PNguV49etVi)OxsYkMKl+%UxD}cbN*b714ZTPhalsD69p+
z;-~WyY;hdtB%98Dce)>*_mW$(OhZ8W4su?Cgz$=&{AzyfFFii``ufn<z#JdWk#bRx
zLnf8+{FeS^Zu;mYu@C&9Y$hIdh$dgL3`2OAEBz3VZylkRBo`*6?4sv|(<I@@2!bW@
z8AIR_as0TX{Dp60uB6e#Hi$UvY3@7x<-t;8Ie}|erk>2#@peDp^<Ly!A@FuZRpajM
zd?hEQTE(TkPsN|%$tD=rq;iv2pZLCT-w)FI&olH_k}r6mn|a^uV_uf>eR3tbe5>cj
z<D$&6aic=}4T%SRKTdk#B*d)#>G|yG)Ic?iB-eE&&`JFPJ`Z@YjTIw{x^u2asFA&#
zF*TaD3X|mWx!-lze%z*%>KMyC<OqiS+~AT{a~MrU8%`f7kPQ39XNPFP(Yr%R>=n+Q
zrvfnh3Tj3pV^(1Ihuh!jz#W6m1s^bfz^+nBNh9rin<3XGp6fG){hG?d%(z#_R@O;8
zICe-bLUs!9zmg`}DO#i6uSmni+9_vnw^T81#P5>bFlE|9hg9XIM!cHBm8dVQa~i}J
z7-SsJ7;jzSClf3oJOS6*<yaBCL~Do+lK6UmgTl$1Nx51oC*P?E{8fBMQAFNZQ})8>
zsC!*_!fe*3WIjd=`c_Mfhev_W!s0&yXZH92rPnYe61U$+Oy+{-YkRw8QLeX$C9oIv
zchBG7v~bzUY&THIAqI}&zMy^%v_HL4NSql;-pyz?D02(IO(J)sUM%8=EI<6%`5{Vq
zvqMnpzpkC63sT9ymcZTQT4Jd?7UV6tJ<5sA9?V<bXC&#l<Ql6IW$G6B^1U~>5#W@b
zgOhYn!|C`-E#lH&j+oBekvCP_4D8k4cOl8*^AaC&tR_K1P^YoC`vd`5EJS~8eXh%a
z^FQ|5VqAOoF}PbYf^R|K5=qjnbsSs2!oejur@U)i_o^08$4CD2N~)IsyPRHj@=Y~G
zK1nM2G^Zlt<W6zx2wCI(Hj#uoAt#gg14Fu*Y|~WF)n)?}a<wo&xh8)a_Dh0I1^dFe
zrYrmTt@o^b8VwLRN7H$TY?0!DBAhfILo2vropC|IW;G#Px37O>j1%)$LLKhr=3L!8
zcBYk+mrkC~uTL^M9SxjWc)Hnh*f%$Xym){J6v~6sTQ&UrYM+1Q4^Izw#M((MHGAL&
zS~nrT9t}JWAA`(1`o*ht96O(&gpJJk$5<DhZEr`JSN~kBOAGG&dt<Qhx%BW7{DVYK
z-OcPX105T0<1!kE#v96?m&8u#;>SR3aPtd?-rhuYQ*4`uMxm|lmF|(i()7Yf!9270
zshV}pMDv}<%bUgo<S@-UbU*;jmrysDa6*{uKZ)MX?oLOK@Y&<Cpf5A%Y-WXk6x}0g
z^vSgClfUwr-~*S9xni~G`6A-6vn*M_va+e%k>dXBPCFnBs3txA(6cbi&1;F?rZi)W
z-gp(N^vMLi?Rc2($*zXaVBFCsBJ}jX_FmKN(E?kiF>qQyQCz-HW5d3_W>aJFf4HdJ
ziB$ci_?aIsJumnR^a=_(24iQI#cdd)%x*}t^GQ^5lo`HZDFn!|As`LHUKSVT`DmR(
zsLa0BJx&sBnmMXPrdXNpMDUkA^p9X!_!AsDr-Bavnh7XVj(A$BItYJ_-j&=wDEr!X
z(~~5~UcN^hcFo9_j@+GINV~a}q<WeVz=`jWDN|fBJ{@_VzGc7cgy4pg(8qECTbd!C
zJ-yA-Be0!7FSG?4s>J@?pL@W=@4s&>TIk1ogq`8e$V(iv&7(e>fUOgf%h);v!}uRt
zC$7NODOJ<1r82an@`ScnU-PXT_lMW-<IU23x_nR~GNx}mK5@@`t%crG(u#c;eF!B!
zV?$|M_&odHb^Kw=83PR}MaF1mk*E01V9S!s?KOfAwqjnJ^f>Ac6XfFTF9{p$O0qkw
zc%m*diYSs=@Jxkq5uFm4m8U-*mxafk`VIwZ^k4q(&wqdWYv$eQm)yy6?oZXbc!o)x
zZQS#znr0lWaF!!z`=ZBr`gT#Rb<M^~x}vZ|MvQy<t31{u-d83P$$EFu*fHaF&BA_k
zzP-Z6nSrB$wy(gZEr9euqH-o*<W2d-HBcOWVE+F>wADo9O=r3gD)@ah8ePjF$7u2d
z77u)%o!fvAP$P@QvyHq5a}n<9dW@&oQ=@%rw|N-6u}Qh<)dCPSFpKhxf82heNTuK-
z=hO&a8)NW#1j&&2U8na6<Lv1~Iqe`h#vcTj(kCG~#BJXPwE9Vvs_f4+lR=+Po)NMm
zVy^47HpPb2xx*YpV}6*9nO53oH{-DyfzWX|JT0k5PHw*xc4XZ}-?6;WW|3gnfnBsJ
z-HUzQ&v9|wW7w}qqA;2(Yrcf(k9<#8zELwhn(-^dKllp#3O2%Qkovm|e`61z@*fq}
zbzkHUxJS7<X(AaOVC?D~O8Pw83dLw3{~y%nFi@GE)sfU32O|TTvnR(GR||<M^K>Pt
zu|cu9D(?v4*JrS)P2u(TyI$Yxm+B*3_YIj`m<Sf9_7bb^C$8y&I;kys+e*@|wl};L
z#`|iLVI1<=i2UK<_z~vV6vfVP5UU~Lp8Mne*iNxjU%H(W>Dxg+uHJRslTM;Ay=94I
zGM2~ZbkJ^Kka~Z~u0rATfBPT$$G7Dw+?X`FxITDqYR$(FEFcx}LHyyBWp|SS`w}C$
zvPgznhcd?3KKlsNaawOX=E}Yfy3wH82o)UiQ4gR1`_zh}E%k+XED!$km?=~A%qA~*
zZ`C1eVexwcP7&wV^+t$w0`ZG7zD9;w1%FF>m}xn9E!D4eGZ+mkYqQ)rr(FUXN<usb
zO?gX7e|TgGLf-kI`|W?7JLac>xrV~w>>?gH{~m^h&J&kzMmM}Bo~t?9LF3MwtmpeQ
z6~p)^K8?`Mz$e4Q_)aKTHz#YwoSW%jowxnt^WrX(p`G3Lp3E0rYAc5-Tgomg<!(b~
zUmV!k1=%va<|5X}DNGM0URHi&wUt1Ghn%_Fit1L@F2SieyimA&n~&T&ZXVGAZ2ngv
z+S9R*1N2==%~zN_kdbQ`G~-5BK=w{@3}bE)@)E$*hF#l3pqsv8;4d>BtEMK6`@@Sa
zUjM6Z5>{@_EskbabNUv_lY)cCWp#>8QRxGH(x(=qtDa=jC#k$VD$YN*zhGNb>9Zjk
z@RYTn5ZHO~5Y=>x4_hDzFrg;#;zK7T8ME8RB@BbDKYSkiEq-N&ErLF`r~N^6-tqaM
z4s>PM!igy)wjN&nJEyVM^M5S={c)=5{=OUfp9q!Z5p;UTvncNVUbQC{YI#bps(*aN
zmfuTw$G;YXjle^dCHzL9;F6duS<p4W_>ZJ-kMHMwL(Dt!UdOzEnWo3^(15*SH{aLh
zDPW!WIM3HN@O$;;ER*g2cm$CVQll+X_^*lnKX3C_L+iJ{@aV0$<jU*(cs_<D1;NZ6
zg!`(EGd_zz4zYWvhnW3#y2E3Lzx>;>SA3^|u9$!1t3w=N8=*|wrZBygs3xU{XZ}#1
zP*#sxR=FRI`G9O&2KK$+HI+O2`z!NVwa4&NgVOk`w6J4Dhej=TD6$bz+-wD1xF&vs
zWk8U64qu0F_D_nVo?%DpEOfjK05JkS_LfP1qxRo2#QP+6*@4=y7G9g0GVcQ3@%cLI
zGo3G!^`Ou2DV<H#yb30YqM;Z>DTsWyWUZcpp<B(Y3GQ*l@2m91{Z0tKXlI78Zgg~r
zdg)`Dz4EO${6cw8=TOv(_5FN_x9(ZlR7&I|b*cxLSJ2AX_~B1QX5BtI{98>Xemq9F
zz91Xsi?0cqjp4ms`z-}%09)g~2n;sPT%ghGiB&!3?d8EZ&txIxWjejyEc8(TtlL1B
z<T>|nzPdU3XcO0D{K%vilmB$38jOd;jj5Q;M1Kf?OCnThiDx=q+DA4OzeXn!>&`Dm
z8@}fJ89!XQVULSSxZl4od6X`Mk`2mnFv(*firg(YzoKTBroP=HbDO&fKVK;l_0|aD
z2hU^56em^wEZ|qxokeDxM&)AIx94*VBIWzLaiM!?Y(r+jilB1q;_~z9VIF>QAZgSc
zR?p>BzqeEQ8?Jw!>)Q)mwlm$4+)530W%#5x3&SZ~;-Q(y<nmQAX)rbKU$Mhg`TiU`
z-MXFe+ql~?cChn5b)o1$PtgAWU@?3Dm>pf7^F{k!g224H{R60e{fm~lk9zG^Ndc3+
zulaD>vOkdf$YChVX0WdpU+x^b@o)skAg<}`jd<aQ?77?5@P0q0EUtYl?j+?+<$L`h
zFZpzY@ceqYJ5PsnH0j&HLKhWAWo+9boou6DYw}4A4tk9`yEqWGN!>4NUZ=4xyPr5l
ze*w%HJ1qQU@3z0_^~L)5hoZ+dCNR&XL=**UkKJJZd&D|NIVcR;v^@6$B*Qs{7`oVj
zAFRXjMv%%~`Y_D0-9GJ%cyxAX_6&%$%H;^D*8@A?3xonOd;K*pJtD;H@id6f->*9>
zXd&hl8WZ#KEpybH{!Lu6$6G6?>}K5BE-3bU-bMBVa|(0lbms6r9`(8jHwlJ@D07$Y
zqlMJFIUD1oDgJwDs{IaWRYS!1!ny-PdaPdUZ5@jBGsIC~fIkM^iOME_3%SDzK^R~7
zYNFWo%w|T!4NUA*s39QxHPL<OuUBKl9Yp`%{s(Fxt^HK3U=jDvQi(tGCDb#!eO%OF
zbU*Z-H7Zi`?SnwMzTJ5I9xMj^GEA!V39=~n?DOIBFoc>CT56>?)L@S4<hZHBpeZTM
zM}dz`4+@BZ_}oO|jb!18rH_D+wAuLk9XEI!Uv*Q(zKb_seXO2X#go{u$lRO_(Td;n
z7o9@@bA5{^>+h!VsRq7Pz41xi&tHMQ>bH11p6%d#)MXW)KedU;My5kkAv*opv3N{|
zjWfo}I85&s-k`6|DnEn%)l}g^bRO{Wx%*Z@qC#Qlg?Cq|zfvpMzpnN!*yPm>Sz@o+
zspy;-4z^{bC!OCQ?-%Z#JNt7_;r4TMHPTh`9rZV+*OUrO5I}_SLdDXM>!>~S<i5DD
z-h8X=@9i!X1ZnSBch{fz>;AY-c0^vo<r0F&^G{Z`*~jl7opY*LF%bMDCQRG2Iq6iS
z^2hf0l*F;1^(alQo7<;YdYB?cyaG%I<6l2>(oe~MbR<ajjYfe<wzptIhOi1c*)xM6
z)(01QCN$a7=V$TxJ;^2O!Mz+vrzxMEWU#OL?G-#a>bgad0{LBx9&)^&D_;@wiNfhY
z;pZ-o!+^h1y!+nvNBeEF)p3^Ye}Q`jS^Oa`NnY)nqI2&axh+R+!=B^IJr__4v@hY0
z%M1xLx{WU^gI}jp+TlP(MxJy(V6UT%C+loUE^rb(RmOm`(2-Y#HDr^&m`@vvlhvpO
z4lMNduY;%cv>;FReC57>=7$9qe>XkZOU78w<e}~w*hlW4N0?NXqv&dsb4tV#0+M_M
z{S4Ke`^3_AlpO@s(Z4V+?s8#m0p=N|))xv^vJZfgo<iQhRb0D^SkS7-naKM()ype4
zWj$4x1FIgmQcM3jCu>6!b3CWJ+6RKeM$JG#Ew-=sI5rOLZ#eY92Cm*dlMxiirzsCc
z={$3g)jzSP>K-wSkD)Xi?<Sy_W8I7r^^+8Kt?x>Ct|+TM;T;KIRs+@Fb&CkwPY&8b
zUFG9bF8`1VPOnU2>$ju!dL=EK?dfCkh8)$E^={!HzU+X4?9b!(VHrDtA3NH2En6ba
zce7nyh8RruC{ESZxi}JI$C-V9tV7b76cyE>3=2=BPvIsG=fi1s^_nIG87sX$%$`BZ
zLV|rvH~+T?#Ia+3R(OlB9C^cteZ{<{ny+I2#d~^pX2R{MoKjO7h~_YtL9+euxjFej
zWa(eDTwYY|{YHm-YWyP0c{vuJU@#I^nxpc(C-@L{!29VzpI8@Qj$@Y`l+;O`zml-m
z)3PhX!}U3J9sHQ);owom`s^old-U65Dw_<Bn7vLJM~!B$hw(iNP<h_9edL}P5i&&z
z2Qu%{rOrgpHvODWKdf8=P*_(X96I;tOVzcC9vf*L?%gy&HvTD>52Du3KYUY_a&YJ?
zZOPT<L`CxH@)1!CQ(2MI3t3AF9&hgqZ|E=K+S;6J4h42<RI>;7WP>E<h~nbFYP82&
zrMgSc3-WTimsXs>FMMYBN;3eVmH)6->9(bBZ4+HZ2xWP8D`ESq{9O!C{h8Nd4*cnd
zN>?zS|IFC4!5&bj+Eo!%QQa1A#&HAjeY6;xdBI(QV(7KK4|TwQ^)P=5gEk#UM5zOd
zw<p<+#@E}cW7y1#FKdxB(;32^zTxt|{VM&3+NW4E(x2R3;CTo8Yu{iYjPG=3owUT9
zD%S_+ofk@v(9&v4W@N#rGrQ_-us0yOzfdGbHaLQsbd{oRUNfk9q=){k+Je6NYx_#5
z1ee3$m`j>Q;i&l?7Z1fBoJrEeh#eMVb~^pDx#f(dy82NPSewh4;_mNyaTJ74?AS!<
z`RJyie>jXfwLL)i#WFGYNF(58rMgkC_!-{2B`rKhk@4KA4`cv`>hamUF4oxEyZ-sa
z>E<3!#Pj=V-l}&DH{0y!F)UQ`Flv9R9G-W3R-G_&9+7;v8_p9nQKzojO;z6?z088N
zo_rPXf_(ac<F6h2?X;`CsK|G^{XU@`@KtDp_(eqLJUkKcQ*;M()n{37n^to^EWLHj
zwWLR3yV=4oQwHs)>1R?gPk$Z>YaPTJkhbo2QfCACj8A<)JC4&o?#z<~nO@$_*YI|F
zugeMj{_NB7K$0nE*wpKu+CChMs*YvX98c1Dn<T6Vi@K<er-iV>A$*>vGh3tbB-jWC
ziI;e*9}o=<FTW_Wzx=z_0K_R^J8oz1_SpBBdA7fBVT`%k(J`HZWLXbmL~B17@n%Ni
zAG3|gc)Y(<d=2LdsF+~|V92re&nEH4a?}I0d_1j6dHn%p)KSa=HF&<q_5AqO#+8un
z%i?8N^LOH3*2Qk1o$$r6`rF=OJc;}<QL?2oh7!*?c9nFGDCO(+P>YX?+2HuS_az5K
zR7>vNj@X~_;zje~;DDpHx2h^tKpxG>GK8o7$B;95o|n5BFh;|hG4Z-=h&b)yU6Otp
z@CE)*t%`+}^IFzg01erk{qig{^>z0yQ<>)Cr0y*Jb$btaN|!Pjnm&<76GRA$Vzsb`
z!KCgsc5l%2)1zaYSI@jRMo^NFYmOLYv0t6Xst98L`c-EUGS+AfZD6I_BW~o^_uq@D
zIOIdGiUFF3ilTZTd0+m3Gko<?y9&e4XRGnY==fe)5mrtYnL=M({j>W+CgKLwyFMV5
zOK9$Q><J>!KOqVw-*+PDXZH$;Ct?=2>oXSUugVqkE!p;XZEJ9CG@JFP{)FGF-qV=2
zw7(4Th);Q#wRaFXYQ9y@b0RbHaTxR6v_D2qGOdu_cJZ%>sIV&5Ca;6)nmZG*Q8xdF
z5so`AC30AZugBeQc~SsJ?4QC{btAt0e#Q6{UyiX=BzOR79`$(3|MSTz_3+<y`c{zT
zPJsunK!bohq%9O|-GBMN{*V7aMb)4m?h=OtKTtaX!3Y=Prh`uFhHhVZ+WdoOWbc32
zunU><Jw5LBkG{F`G6vEK{7WdWl{`Go{_*<#h|-Inw&+McNn&>so?}Gj0xXgA8ex92
ziNUQFk<a317DFL_;l{t5;1QRrgwN4g^73)3M8JB9@<O<l<^i-vk^w^^oGm=utgTm?
zgZ&(SK;=e6eXEOPDcm$JbIPQ?$As>)=o{~nJVN34j__CJ4f{iU^M2|3MLcKt+xHOK
zvUuho^4{L|&0p@xr@c0Q3`6j~j)($9xMmJ<D^tgNe3r7taa+V1We{OPI_4pC4n<zx
z(vVbsXhk$=F&}p$Ro#Lk!)^KLqCd4syl|fB+!dnuU<;VFw=YL79Lc;s6H<o^^ye$Y
zQUd?x{=fm2gXx0#+bDnBAA-b)dO-ELyOTIGUv{_4oa{&#zaWZwBTxC1ej(4oe2A%R
zdm+jzWFzA9gqs-{PP@8J(+;`ldYQ#Q3y-eB?Y|6K%*T_>^%gypr-*Zg=_}0sF<R8-
z58G|?$3Q7zeYg@Z#@&H<C*X3R>29I=d{j=+W4)5?-IxTgxOJ#KtZs}EEud22q}Lu*
zb?XM{*KRu>A1OyiQ@>&yC;eOVPyU>Iz3L>eoQU4j&aLivfW%WE*W!(%smT7L-R5@g
zW9+<r=I|}ZcD`ULQz`c2xGU%64u<u$NiD?o8HCwBoWJ{4g&)J@fe8~1(I-<dm&O=^
zIeJb8U~6&n{M~#6&%6hX({%^Px_okW$D1eruy2W`j9We*{W1%X4|ad3P8sKJ*g<4@
z#Z2J%f<<cEC%m7Xy!<4yXQXDhL;SBpg{}ZA4i@62`&}tV*6%uSAfDSvpuawx@)HI5
zrrPuR<8yexDuOe{#^tE_BdzyvP+um#c%|cydETbKNTeUh{r$(I0(1fphl_g~<}?JJ
zO~7efxV4_LJvQ`3r9{w3If*#(%0oI$3k#@hxl52t`w0hrsrziV){jCn2NgHOi*Zy3
z(k^{2{?xj!UJh<_QKk;!CDnGEw|If6<q;mY%MEGP^p7~m<yt(Bo21I`KIxojxG77q
z)6NguoyF`E-#&|y8NNeq8u>e057KM5o2X}8(4S68cDh;6B1V|F+;_cmn^9>yV6X|p
z8!qbQQs8^d40nFBHE!$f3En)1eH69|f5dJ2@Yh^e^hKr%&ZDogw_T+SjC8oYodCOL
z<^?#gS8t41Ys(^tQew7YA%4~C!mP#I7uRwstDj@~vAA*SXKC+;=u^$}p6SFrj`|Jv
z@MtdIBWES*V@Bm;VV2iWlukx$V=)Igt!jD?V8d*H;psz(!$UV=^Px5S^CL-MC0*my
z8g|R+Ib|N-%9WJ82)W>GyyG9<?k$h6<06jR-pM#C8j4L^^_6p6=?lvP@XvVSy(0xr
z%Tb?f*ER}G$=-v5CZV7(>h!mVTmrGD6norrke9r;3g$kD-%@mDspP4>{pFcfEhvK}
zyUobOf}_KJI=mf|VEhV@Ax#Ie506@r)9lGi%Ia-?&j<I!+x6=`jIDUEuB*rqAW3s$
z&(7sfFV8+8QYeDXKsC9V_-+n9B44UuQxC=mGrv(M2`*ZD|L~7bc$uGVGUE}|4lLdV
zKR4VD{gx<KBL$;Cv%HDht<eK@z^m5Ojb-L4U)@Q>O~r&CCM6S=sC_`Y@=n<Tdhz{g
zwCiuU6Lb*$-Q^6>L@VkIET?EDTN*j`c6{p<NRPSv57PEAd=g@6jH7g{;0pH~2_e-{
zrkgW5muy@*y`=n;YPn&`X6Kz&l8O$zP~JG70DT55x8LIT$n4bQhM&V?ADm@HEK<7K
z1A_QSa$eu*=e2r3A2+v2dMLB)Re^s5IGTaj>u@jo2t0;(tsH{9h1e1J3KJN%BH>K2
z)B_dz2El&`s2xT}eNoqelKYQAU~#?6{9l%ncTa59RL>(?g>Vu?5-~gLBxI=d-_9&P
zzy5{ml>o^D)H#@E`))An((80QrI`L;4cC9La3CKWhtt#L8u7*0zS4j?-u73KNQ`mO
z^z|AzGK68Zu~<Y(<_66Td~m-ogWQ)X=)bJ^oMuX|MtDgPNl<D$o6_)aKHPFWU#>>7
zJ1l|oJ~m3SGu5Mh^TVp-At*EQzRn`d55_En2<CbWOai_X5w8=Al*rgua1Qd%Hx|oO
z2h3qy&20o@<OnfrK!;RXdhQ1!*46NgU1AnN6#BXVg;6O!yZvD}_$#y>x;tLYO!IR4
zeINV0GZj!E)&%u$OV3<UUU_uGl+QIMU*7%dh-BVfbq^nE9PitTsD7|Q#}UsoE`QHu
zpKzA&+W``4$or4UUPRmw;~oY*AG>xJMxnZv)%e6ze{>nAx)9dV&c>a)-L=YAq}}Pa
zsu?2*Dy3v``(&wRS%ZQ=iNNpGX?vSb9L{Sj5~J~=h0*(TS(w-t-Q$cLcUHs!gKBG<
z<suZStd=%w&^W!39vEfh>B5$KrTt9OlYaiC=Bb{D%JJL`%bDcg0p_x&KCSC*`Ae|M
z;pw-*;V!QIE85n*vN-7f#6q|Z7~^EeS64G#g&lw`hOyW6R}tDLiSFh6K+W1~s|oJP
zA|T;QSE0wSo!VDumLrM2@~$5d;osFWr}J5!k5ui}sHn-tDZ+ANok+O&!qIx)!_zBG
z`bfEgmxD66Zn@iz)7b(gRF0~4muYJC4$GTDh8_2Styyl~=IqWWfI;t+_k92WdAG0%
zd3{BDTOBZQ+hU<@(X-CGKL%1%fhzC#7N6Kp9&@8tg8$J<ZiNPf*)VzP>@*Nzsa4~l
zHQN0B=XKgz;gw!_AfFk&y)&4v&Y%N~1x99g!PFPQ1i<b5O}2yb)%7}Ao6`rMjovxL
zTk)ihDAr$fKN*R?Z5#c(=Y|zhUd~$1mYhJgarX{-fXYk;Xzn<Q3U@3vr!4AmW;oCg
zL<~Smpl&<m@;%j1F%izy=v(LfD5IPTc<(#21AbLc^?M^uNg%i0N%YI;GJcuU>)-bU
ztxGe1?Vo>lmBriRNPEYVv<&30{z`<7UI~HmU|~D9_G2;!$04?}Rs4l7tADd=s#i`W
za;@2((0EEw0g<=7{?Zt3{oQHV_V#z!%D&5fp*GoyObBjegbiEkH(h&ZC<jbMwmWj9
z&%c4@<wbQsK>>-6rsU9<iX$(CIFQxO9AO8=BuGM2nuPoLSrAaY>LZ?R)Tbh-oS<E@
z*WmyG#RD}^;wOb57p>oER}C*~Vjs^{UsUYbkP~`o6MvHIxEsR!pb|#lZI{>akK>sG
z4WiUuDSmnlF1^)RUxNjqtW8GSyFShzbN!q|NZ^jh<>;UB*U4?il-7VX+;L#&Pnu6>
zW(co-WeU%Cd>j16+Bx-)WN*sJlH&b=S($f&0GNCHoSqfCyFg@MD8f+-{?5oBIMq%R
zevhHNUF|U`Lm);O=|eNxhZ%sO^iP}*FJ0cfcEY304zk2Z*Gv{l!qShz<%cn3sJPEt
z$%MOq?Fa_jtr#@#*1upD;15xHygLn|X5DAeUk~}*s_5oNOL(~)|FZMyIW&%IbZjk|
z@vZ{BNs-`m9&Ui51slVzahU>Y@_RRh1PF#v1of$@z!8Tk@*}0*+9ezjk<8Df_8h}i
zuANj9m{U-Kj@g=5XO+%uzG4Yw)8^jU9qhfjM8o5I6Mxa)@I~%Ra1B-nM-)`$s1!qL
z**#KPj8ua?_n(Q}BfQ+7uVNY6dEvRZ1T15_yEPDQ;&L0ax*VQzO8ri2A({F&Z&>v_
zt@UqgWH3YG!M?#<eXXi6f#?a-{NSJ^aFRrZ?*D0b(%-X9`s^2Dxa?ra`4??=yc@}?
zJR!Zwe5!eD3ZwGMXX0-((_Rg4<b%7KCkq2%<L<5O^GqorV7~8Go2-?EV#K{m4FT+E
zz<m<Ju$qC|rI(1K6bY|C*){2ny8K=p{TJ|o_A$Ge+G(%LAb%X(M-EBLqvb{cG-xNp
z_37majE7B$t|y_|bT~hvgcEFIpIzZ{*bGMPazr_&$2uWSR-C&|DR)0ndWR(Ljb~B0
z<Qf5Y)=#Bt%@QxrVC4{qH-^*wt%z`CQ}Zh>hwVP*F1Q;r-I0WhSPnE`$-U!`*o$gh
z^s`)V^fuFciGuD(x^4fk_?os$Y+0{fV_{nAh%zS;Ys{FM+}2vwScoO3SB3<&?HsY<
z;!^5#?Qk;=Xn1~^X#FcwQzZiAnOztHKegJe;t-ZKQ7ck6u`4tB2{_Y%xc80HXJ2O}
z4Sxk=?5^qa1r6EncMtfdd{y4fJ>eq-c+&Ih3!?4WVeh;$U-SbiXJn=02|hfX=UyVT
z74B&DRFDH5zW(ZBlY3LcYNgLHp~DNtq!G4Utm>Ar_uD?&$&V20fM@G}g7Eu{%I5(W
zi?;tdQebfMnSc+1qHPfqD87TmYRx_Jd8iNsW%<B<Eq9hG()h}vwi=x`PV71p09D9x
zB80J$Kfh{b%*3skS0J6&54W3*zRIY0_3=qw@8BlVYSv6Sk|yTbUfkOkDfg@~+xyqc
z`=9py*2>nS%-Ac;d82FWT)QY7{mVG?$9EwU(({Z~{5l}SwCLYjGB4SHUs1$RHO_p0
zpaAH5M~L^LbX<g({)W}rD5gNQU^1t8Z}v~<cC*gj#W|AhXCDqZ0*>{*w1Si+-eF&G
zJb@=@GaswdjRqOso^cP{8!cRRd)L#>!f)`{*lut2`fJ>pr~Os}dSka7h5esx&&P1u
z4`S@SGg&)db|H67c9M34MDvAj=O>Ts3Z)>**&mR}z+E2QTTQ(V455)%p<V2ue4mMs
zF^^2OOZf(tL~XCOgN-nQBev~Ddsi<pQz!gL{5JxtvU_4%+zv1epZ+ibtkiVy_h5dU
z7erSL4<O>C1Yv1y)sd3nNiGtkLK%;yz6q%APTo`0ZE=$Qe%kLhm*`ycw%TC}>!z{a
zIf)jSPzCelOQe;7CB2sxh4OQ6@@Low{Ku#BX#U;lsaq=1-}PNfwALhtXW5GKkL^3J
zpn+E8ueu7zQ5eW11l6txdnmfgdZp`A_jR|7*0byW(&IY>46=4hXZA;zCjQeSi%g~g
zd6)d8;M#jabb>#Z3k<Ed#!v1<RA(Rf@NQ|eTo(Ss_gi_npyuR>^2jKY!&W@J?A%WK
z-P(d9S0ZRDx@k+u<Q}&t$vpM&hEGBgL>YGtkRyDOsyM}w>giwCW0J7_W|*Z43mV@=
z3%Ed97Kd|0_aXP(P2V!s5)MN)QK}}xY5&L9fBw(^UqZ))k)SrC|6w}*^WS*BH*>6r
zNkEp<!#Fyhwv7~Ko;brV8T-5$g7D8;fk|MSGWD+u+fF(|BhxKxo`HZp9gLQsU)UOi
zXe&)Cze55!%JKKu-<oI~@d#y~EskRt4}LjZmbw2r9&~u)5khY{lSyp}n@>v}Jr@%a
zS0rdSg}m1k;~&zQ1m2arUAFY3-8^RZB+3mzy;#1GhWImyyqM2b%A%!wuMNWN;^1nT
z#^v+HIKp3&o93ky#)EVsuNV&=Fdm#R9z6dW4<0cd{A;pM0U!JFNTcd4zxjAcmTG$A
zNYiY`HN`DX5q6w<X#>7D=hJ7najcYIaN|WrpZVqg@F!>1>!IB_&)|Ke5$j>WLmV|c
z2HPGXz~Dw{laAo)g=CUn_4K)~!(C1nZW)`~-;oqm^KOF_OY}^%Q>pPMcN^Ap-;})H
zxEqt@&y}5-{_*6nHahKBulzuZjG!G12q7{38xLYgw%xES{{M{!rTf*+0QpD8<$5~4
z<Vs|QE~*OUvtEY1tKHY~??DMGPW@mDr0F+kGL|{-l^03|%_;Vh2YWJ9XayaR#?l9>
zBAIguzg5K?G&HleF>kKze07RS`9y4e^;h9Lun+H5akdAtw)E%~%*IuuxC)-ZP!m?+
zr}|_c!$t4%VqBTO*Rw`^)ZMUc2BDyoms>26hl6TV>$!!vk>c6pY$aCp{%_;KF#b}7
z9dFbd>&0u)B}}If@y6Q>HZ!=960A`-c+tc>^Ll#kIq0qFpuU+y-h^dxN`6tM{u>Vl
z-xT;FBnbZ7c<>r4{<8a=!KMjl5PJUKvfifGbu~@Tt9DgR1BPMrphu2vpxb~a9FQU<
zk%R#^D2f!nC4N(ifr3Pe6hA~#6iJcvK(JvTv&@X+f!#kK8DyB5CywX-BOV3_0td*w
zs+_oi6mV*Aun+dydufsH`#kr3T_Y@ngo@durSczv*#Z2I0x-F;llF$cQwg8QJP4*}
zb^QDoJZ=RDc8zI1JZ5hI`?6BKu2}{R*25>a-?jHPM+um|?_{@M5ecK!%hp`?U*+Te
zj0ZC$NB;lgL6@;!N4wpCkNHt}Iaep_5$?n0WXm+KNpjKcamue45UWv+qt6fEUt>Wi
znAbUa-V6k`pRnBeSqT{6<2<p)^CRCI+aqvi_CbL`jQAGr{v#;;V~1hV4x%u7^LZ{d
zZN&xT3HBO>u_|Q9Vvp}ZY^TJI+*{IxEXJ99%l9(|)TQVt<0sr1y_J7LEU}Ft6L2OW
zZkLC8sG(#!D5xX`)*e!4ZS-(pzL@*1*jtu+Jb$%=aXQh=e)EF|a}X7^Uq@*U@kf@L
z8~+%rbxUXIiSDO^mtx_NH-W`Dv{Al0#{OB9>+O4X1D7{LLZY-`WN*9RF!sF#z@L2C
z?g{8;c9Ywi;>3;Yoq?Kh#X$>(z-8=jzs_@C>Hs48rgcsnWRf8C?r_vQ3vFa5SMFjW
zeS6ah6w#&nj!#bp==djlM6b1=N)i7wKx;b6<mCbY2xb+x6p!L}{P;8#;GR}B9z5V#
zV;~_!@&FRLA;Q6lQrz6S#G-z_=NCCnw~}WjChTM34bZ^)@#fk7B>@}ucA9sjX@^Z(
z>cYMc>I&j7mtX5YJA1J90z?{IG2Pk=MoOfX%F)^4=Jz8h43ppO<Geoh2BqGU6kIHR
zm2JQbEgm`JL)=PL{ak&GGR#xql7Ul_iu;v?dyEV)ZVsCKI8Rhq>D9A__pLB&)FB6^
zU2d(%$b{Nx^XUNl@dS-3WT7}LPXNSSb^!<$J98&bBw7fa04(h=bi|QK+G?Sbj8E6-
zZeJ=jO@c7Uz7#}H#q$~oZhq~K3!kkhg1CIxSC;t%lI<W4(doO;s^Z*8kF-17E^)IR
z!+Al;d-OVmCosR?JG-&2l#F@cKOUW&FR>3U(`x0xN698jcWmtUohdg22<g^II7BDs
zB<PI`y<GNE)`I{apNb&_{29>J_3o>=a-&0@UzcX5ixpsP!HB3M%kgm!Bk1koPgw2U
z(<8TpKy0-4_})zXhRXF`_6d1a>z#1puEtgbql5@O>T%{(;^KXt0y(hws9{OaIOpKY
zn~yu2VHn<Tk9?|wXJlU-<Zj;b7Enw3nE!xF8P5$y+C!HXlC^h3tsuN^^vnJ(UjcyI
zm0$pfhR97n<H6PcHy+GhZQv^F*S^n_W%=WHQ2N7o&=3DG9<<l#H>&;VKyF35u2qMB
z#j1^QxWocG!aBR1K~3)Zu|;a3JIyO)b!m}~b3rjKh-=yMZ$@R2P_z*WYs$sD&ssnz
z+U0r?Z49OB^>mLhJ$~Z_5xh3?1`SW$gMjJE#hj>8`4r~Z;4kBDpWi$M#5Z(5)2P^P
z*gM_HKDENHx{WYpi207QZsi*9Y~{3;KlAHYoL3(%9qm{~U(hHw!YbrX#{#69f=iS}
zeHt%Q#?J2XUMn=xY#PewgVcyW25%XvqG|=fELDDI@tUwRrh_S_gU@PPgLz<?heZ$)
zS`=99V6ieG-7D{eY-8xMT^623ttu1f?)O7`zT;|t0Mz$~QutY`L0xE|5ug?HR90(c
zR%|1FqPQJi4kcK;h=kRlY#%QrYp~&)?!Ny0a>8KgP1UgnvvYd~N0~l)w?e%zpFnkj
z<7@xSjbj)>9(W<-^B$Co@baWOYLO^EkY3>49+>N?%9+>En=+%!co+)=@!7_BqcOYN
z<v^gA8Nvn2E@!?x571~u+<YpXCi=n=@?JqzhL-omeW2CZfZk0-d`xcQ{jGlwC@~5_
z(zXX(U8*;3n8O{@!4s}ry|tF!$)1bt6?X=oelOT}7~5m;%6d1ZpLY!gi`>}~Khr__
zkREgCSRMiyyCk0UylqU3+&3{-@ZdPc#X#DQt?|mG{lSR57tY`u@#KOZn=&fQ%lICq
z%gQ;_3|jebNC@j=ojP?gUXlmBj6~ROKwM2r19Pp__9aEDLfIQA`v!&7>1{kPrE7-A
z<yd{#Se9P{TJYG8y=<Qf&SDnCPbEi3ACr##DU)9`{5(Kw1`Dx|VT)-dd!{M&Ex+!>
zxfypl$T+v%I(He)VASgAFRh5e4K${l#+{REHBg7a^vFsHj!T!{b;nG&Eu1t$0kkpZ
zz|6)};Ys|0Pwa-e>j{L!5aL@uIdSt$7kX1>`&Vn$o}LvhIJu8U>aSCw*!u5Ey}qk}
zYGA6n5IbF~=%PP^1+BE>YmOi<gq2iBefXBZv-vdeI}2<)?29v91p>1wp3j4FvaXwZ
z3y@`W>?h$ELPKXiCtS=057oSCbqUZ$552leYHJ(yq8-zx6K^4NRyO>+2p0SXiFctm
zWEceN<bRI6_`zT9gxTQ8N=1HrFW0TT8`aJV^yr;y@pM2Kx_#9y3?126qhKgZ#dn1G
z7TQNmd3}Zh*lT(+?*<mOJQRehz&g{{hke|@?<P+fK!^M49*y>!lA%QPvBm3R1~hd(
z8mLLTmPnnZ>o!TA4<j0flsliV^#Dsnb@*(q<ugD&Y*SE2YSXoR1Cf`*ea&Gx(pptd
z0iMbC>lPH1>xoM-_rYBA)y>CJ+W2ACrRwUsJ)IK4Q+<?5J|t*K(*@<1WYq>v34v)Z
z6O_gx?iFQ0?UhHtJh$<T^dB^v=4{F~CSSWxx*VP31rfXML}0}Awc$zd`_JnsVMqjb
zfRV@B>%qIY;pB+y`*_&ySSwLw-LDTA2X^esG!p}F&)ia@xtHsOX2FSY$LgbRo7Jc2
z?F8fCA5h;Gh@(n{1oU;|y+DPoU~c$Tw?Exa@^o-?Z!I3bx-w$_w5g^OqMyh65~fg!
zpKk{ctU+u8OThP*Ch=;i^=ljD;ZmYuIs|3*F5lOU#+s+W0TpwxA2jn)FYO&!r=>*!
z&CEBC?MJdK)fwI|$Sc{wpT+ZPR{~@g$=oM;G|u}wwAf_jG0>yIss5w^sYX6lBJ?$0
zW8H={cD)h{6F<{4>`5J;yaYzsoY&?_8epVPt~TJ*3iUM9IXsn$d7P=m)4FS+(^7Q9
zvaaS4Wa{dcJMACn$9L<DUIR+0?iS$Fg7%9@R8k*fR^~Aq3>~pegm~11JH2AZEMIdX
zQzIu*6zHh?E996lmMN2D4IvkA<#LfRG_7WF#Ql7y%@<)k0>6<VKjrGtCwBwnA3G&=
z2sy=U_&g#bzo9>9KuP8F=t&2dq-ZqXNko0Z?&cn+zcCt|DlW`08gv(o2EC)}vfkVu
zGwx*sEc~y4p41dZg9Dmo-jHY2jtA3Da+3Q{vGcMLH%!_KKcm4kuth5TY~FYW210`I
zQs>B8=RQ>8`n8e$8jlrbbBS&{5A<PWeRIVe;4)^#`7Qm71}TgNLm&JEJk;xbj14ls
z!nQRWEdk6K6W=W-U8>~Doo977TWeK+3{HFK^AClF-_{=1-U@EpQ!|n(zHPN|^QnHT
ziYvnVu0dydhI~qj1x1Vm0i?6Jp=vfnOVd#9*J(0&#H}k=C-v%rao;=Z@r+aFHoZRB
zB0<USM!p{#E{LGflo;|}SQk+`5!iR%oGW>TMkTqtBEx<W^$OH?>Mji=u;~#xlcD#e
zPTgtCV4{`kN8x^bMDB&57)4s$@o8sq>gBZ-*AL~7qd`EtD<1o&$iZlEYK!?%p>p-#
zMuR@poTGPI1Qlx3pMorVY#2Q#M=%=f{>EtVrn?P&0hIlV6h2#V01QQZzg>5;ey~Fk
zv7gm`3uW+2JZxc*CE9-Ex2yY0-6o`QoI)@T-&^+5QSX<P_7~~W+1+lLE_cdT;b~@h
zZ04KZ_h0Var|~)BjKw-1H|67=$kr!V5x3hXAAITb^{QfjJsEKdiIVzeUomi#ga0<)
zW_YSu@x!f-=PX?Cf$b>qgZxOZZ=WB0(JISy`fJ|n!~sC1fMV~LdksvEr`EK~)r8N3
z_bU<`gL`2Ut7QD1s&g^N0Yr{Z;hV*1r^~iTFqR``4MAC^oM<EBdH(>{lZ|^!63G(j
z@uU2?xfQg(cR6!Tc-f7feFx2k<e@uK)t4nuX$8kUF17fc$Y2+(yoXS3<!zLEKA+%r
zL0K)un9*#8rNg5yw;wMSE{E3qIDDHKX>i*mWRkg<hrBvPl{W9yY;<<F0FDjb{ehUp
zq}CEjC1H_x*(W96-gAGOTIS%%=m8CzEu-j_A>4>C4OPM4b$3a^4e1f4QY@Y~>d0Mm
zp(hb;@MSw4=_wVi#K@QC7swfX>AYCI(wtB;WX9D8BP=%TSQ8>DM&9*$S3m;GdI(gT
zP0S-M_XsN%A3VO}iW%|pzI>R2rNthi0#H`4WUzL2LqCVZi*4*(5bKxzaL48UIfqB>
zWRh7*JsuK1#((*jfA-(Ny?-1F{(Aifzx}7*&ia`O($pE%YUuQUi%0aWr?>X)Et}v1
z=JVByTNidS7$dZs(ew){`8IRwNEWDw=3O2Lh&dQwy>Oy=Vj>3F{5>bVl#D9usCaUy
zqW1!PRSl;GC)O^vqf(#}@InFM;b$tCAoWBCt3v}}rKxZjPkQO2;GKi?$Eje$k|+7+
zrNAr%3n#({N@)B7rgS!^v_5<GwziO2%NuVT&xa|@YS-lH@O;Rv4U<$i!1alW2F^sf
z2l)z(w8yPIL$OB^H|5LIyHv81$Ge=4va;N)c*yeNjDD<sm=efJ6@9@jqW4;0zI|e(
zs0FutfHubN{-&bn$A?-WyAy>uAUnrUhkRk<D;q~ZDSrmb@GTEUJ#h+2kWrHj^iO6`
z-Op>-S0r-tt#9w_c~>4z!sd1hi4$rMN_4IHTgpTLCWAgTCi0p=<Qq)coq3$d;BqU9
zV=V2DHPPuu;3NHMGw@>|U;Ag=zX@frtUh^saN10E{ewdEQq;T$;N6y_u0H!~s=OxK
zQ8v=_HCTB_(NuI+44AaH5w>^>d8L+?uBe4NVrc5f<&yP5dT)`3_vIJ45e=avA1S`t
z<Gf+)=Cj41d|t$9XPHxkXoK+Fn(=i4x=yRHN*o?5toC1pmSDUb*_{H%2a5G{roX=W
z-E)_l_M=?Nms;)D$jbVndv>QR9GK(25L?-yDUO(>YtNWUC_(Ug_sd(ut=C7z3U?y}
zODavY;Q?LOe%9Z3Bkt4Mmm9}gJ+t-a16Zjut9P`nmL4nf<95>zKZAm;7ku5leAR~;
z?jM5~v#jCs_ZWdy`~fowce#>tAXTP!L`>xPF?x5V^qym|Myx0*8J}J9CYGNXofgjd
zvag>%6T#q2Y5e;FoZqLt-S@(jI-#u#>`BOna7*g^a+z1>1V8#Oz~iJo+pd5^!B9d&
zG<0R~G4*gP?G-F83dZ-eyZIvd@E_(w_ixE$Y}Yvjt=`CIF*ExOxpv<|si5Se90hDS
z0-}-~?UIxizlj7q^nQ#WN^WO!aabRF>4VW<8Qi(Y%<=;Cu9^6eE11R<7x36c0Djeb
zy7~uk?}kt4^mh8Mfx#)AM(J>(HeA8y-I84=O|B<nQL=@e&QM!zV|UvKSh<}_aCM_u
zhh29d!-=hT!mZHJ$&dF|b;Xy6ap2Fq&NvO^ch{ax2k?K{J!8O@AK50Ao?ZWFahhb*
z(ddIohvzj~=TIWY&POW+zzfX#%WeuCTWZ@0N%1f%O<TQ{RCELK+CWo&)t6Y=yK7Z=
zS6w5Y+4Qg_;dnx~M~#JPa@!}ykQY3aLz`PYR=c}OwI$m0g(#nYLW9Nzd)!8Rv#ffH
z?`f5_r2u`2i0e%!NRROJjNU@PtfN)-B!t<^Cz|+RK<w-vNDDsR1)rFh039~VjWVd%
zKbyUZ-Sz76#3hM_cu6uY&-?1zCfzW|A^RZajVy}O48nx{2osDo^Y!JJmPfl2hBCTp
zx#&jYF09u@m)9|};BVU@ug5qSQ?^4uY<GGFrCb7VcibHwipB2J*C!|M;EqGd!u0d=
zfZsEY@KgvG^duZzri3q`koAtzfM35H+VQ#5g;Cy;vml4ziw-XzJwNu%c}U=^eUe=n
zzzc7CSodGGIggZ!Xw-DpI_C18HXW}VbA~kfItf1Y_+w{*Gioo;EQtRka8)Xzg0V;N
za1nW%HHx0PgPmaq6e5sO8*)$d#ED+TGG+p#(wkIeu?eU8PzR{%>VA_gpr>{AbCdl6
zAU^5x+&Re6s#cR3o<Xx8CN-nabbo0W=AJKoF3oOj$NX-2!c34xIhm%J9l|jc@xuNS
zd~(jmR8Z-XUB*f3CFP_$CBI)Q*{QA>yy)EG2d=oRmSb?FQEE)bWi`{XvFl22&t|(7
z$4=I5N5c}nCY}BKp>k>jy8l`+hbxy(>Nsl+JnaaG68olJ^g_yRJhotuwKEWp=n+ZZ
zS!GW!nBt^w^}QNc&<}jd_7JM;Of;*#ywi@zosf?%DGs@G&EskNJRqmDQd(r~tDnH&
zw0xv+x3Gf$Sa<lykhdduF`BFQ^1;D6B?op$bp;d>)+ss~^Be?}fN!c7CJ(=!WGtiR
zqOE-DldE{x-9Pp9Fz+w&BJ^VXp78e>5tiVBZKut8@lcc#k^w`^>P(uE{24lf1B4jc
zg2s7p=DWgCp&AR{q|`0%KwC|?Hedx_vd=uTPUb3Uv^}TCm2IAJ1p2B~Iq*HB81F7g
zBago&X=#mzVheTrn!lLL=P2Ij1VaD~K?<`&9b$8gQ-zxCK8E*(y=Two2%Ew6*7)0`
z`rK=zL$bYlf;Iqr``uB^?b~^c$IhXwXNI<ph8+=6se>Q7GrlXv2o+DPWr$B^cdbj4
z5WVqUk$v)99d6~ry>PTEH#faJQ13~@*430_{KNzl9DU5;3}W+XDf`7Lz#%|5B_6`-
zwpFT6EJ3oWWvn0O>~UxH`%ia$OJ#m33fA>kuoM$d6MBi(T#IL94{z;&>Khq(xBV4u
z0+cB9#=4?#B_#*54+!JAbO~!3Qn2z8pfe@MF(9-|7g_96>rQR9ZY7+U3{SMJ-S6!)
zH>E?aewf=nujMz$Z21eBrgY!F`;8f9bWZe-FC3dT->QxEskY1GpmW}j+Z;Os`2<$c
zw7k+FZe09(NDO2cPw^#kH`a#li@RpYe(QS`XGK0Dx6!I0bgA+DC#@{^>)CyVVPul*
zOu`0#IYukNezodG8()Xit9c^75X1J{xd7@P8rCP*i5%<aS3Ue}<ez%LuHmBHY!s1z
zl9xVIm~G<#>D)iC6jX5=CQ0bCA<YApAA+Oz!K)|r0g-;eRBe^jUV8EYr*bD{_|%|y
z(bd#z)r>eoNHhBTO|?|W-M*g${@xfE>w3reO3V%CaA9$(=&`b=MANdhjCbY<FWnFM
z6pfEtGKRNMAvDWe9>8u`LqmC$Cjjz3o0S1%06Qt9ZZ>d=N?iGll??^k*+ybnP8mac
z=r-gA5ARYsl%=r$CIRsTd1*sPB^;H9b$Q=+Ez8?_Hf9e;emt=AO|outexq>4wceV;
z3x1|VAMZNDp9k%&l%>m^IngjE@u)tPdi;^cCBYh8|6;t;L4cJ^{d`JV)oR}Req9tP
z)ux>qTv_$YuEPf?$bLf=5u_sgeZE^pzdgC2Hl=ypX#`T=yLY>zSKxhv!@6zed*&E|
zqP(u7V}GXg8=#GyaKQPxZ>O>Ov<4N((F+m0K=L<KjH1NDcUO0oSBoueza=(2#_K|9
zZnpb<7v5RQ<GJvVKD^Pa>{`K~hawSZ=7f%4Ywp$nHqxkn_h41QorlyA-D2C4o5-E)
z5W$tn$r_gG*^&ViDC$~ko7YJM2z_Jd#N&t@>&C1Zkhx#-q9H4GEzlWT22%@;)E7BD
zP905hGpv>ZK&U$cWW>hy(NbIP5NW2Pm1RH-HBZBhI?cLqevs}0b-CAeaU?PwxenhU
zu1xbvOJPR#7t#FYxOZMn^h%yOmUkBwlEM<SF+hL%s$P@Z#Lxl%HK!x}Eh+ohE8Jwr
z(YlFa6`Q3)cql)Jz!Z+mb!aZ}cT~guzJ2fX6>>zlBLvQCD+har*?%~<c;8%o7aIPD
zetShivZb$Wb&&h@hbJ?d)A{v@hUwRjmqO10c+t^dLy2*BxEjiLV>Y-RSmg4GUy5um
zbq126mvV4lONEEHo5h_<^{55W-kkKfKjs)cP32q$*0KIXx4|4Y#&!`uME^2V`}|Bp
zb3X4UBVmN+7~l?n>#d&Yr<bqW9=)UHmOI*xC9O@+n3pfs&!V4+w};xYCo@DyJRWXg
zqTIp-xOrNtGe|UA03b082^xY`d7M^b1|E?W&LnI)`*T>oX1x?Pyp~tS6^!G@;H{S&
zkpT5gE_nsIs?lk^%1Xl)SmbR<J?6f3+l#(QzlSrJRLi1`swkFk?(DuU$GLMac6#da
z+lB0Gwy}U__1x=WwG*PNol|A4NpU}x&jblp;dCP=?-^habCO>S7L1r2-C)KQwugMl
zicwqpEs3|7g)CJhU8yZ*`i=LYChp{TU(mNeg>5-59O{U&P6df&!uBN_Adn%E>QQ{4
zoe9vv;_A9RD32R-*%8pI1_Us7_=iwnz7v{x!|aW4@@94J5P@s^R!q@=-nE<CZWSoc
zUaIS<!r-!Kby(9@@PqB8El%y<NiG)mVOh%yhMO{e*Mq^CMIp7Vm+*CRtFE|vJ)5n>
zxzahMy7_E0r;onAJa~YZOy6B!q{CKe^C-gIXJj`G-upwwCw+t6D#Y+mE^~EO=|`x5
zN31gKXU5m8yly<!yS~1o?|;(#80`ooelV~?5E#;II@|3?R1k-&sB93oa+Q@)9q|gq
zNmiqI1WGR3#i?g@BkXY6Dw7)o4X4vpQLMH~_E=vOvuPB$*?lT+1Ha?ZSoW%mC*HQC
z>D{&C=X$CcOZg5evMO1TxYh1)<fwM8$>=n0d;%s*4lAA}xVvE3@1VTwFT}b8UJviU
zFwtm>^h_i#K?vH~<h&;3N5rnS+iu433MrKnb3o?Lsa_%t*E0rtn{hLjAMKVBG1c0c
z8~_K|d}A+C(jPfj?X+FSgmqhqI13LWAgQ<%UbOYnc!6;?HkGzZ=XERP10jm{fLQMg
z_oPPc``O+4aUJ{fn_e@(%I^C!W~)~x&w^8~Pz=Z4j&1wYrj+KD-FUnnq7aPUGn4X%
z@1Z;A$pid1+rD65J2d-S_MxHJ%koNPMU*9#TmdtuZsYZ{iDE^XNcCXDgn=yi6w?J0
zC@-x&Y3cfOL%Lx*jq*v9gr=z@C2xf1JE`yQbnL`_#;G-Pb|=B;>O=`Kahs!WEgU?~
z7S@h>0Sl+nXqRLj%%jeQyT1!0=*-vrxdzm38Qyl&x}IHCsrC{gdvR#hh4B>*0{*gm
zM%W>%yW%U<c(lY^0(-*_@Y+vtuvAy8`24=l0k0X#o@#M9zw**{Ih9}kaycdZ&4x0N
zVkG-apCcKluX<;bW{Dh7*RUHdOg6jv+_K&|N*%k@wj@I#q~@Dtd4+v|npz9E`%*k^
z@AYx}sHF`Eq{ek?(8jB`4%*aw2!=%PI37~wgW@)czhZxNby@}cUH=*B5HA*S`s8F{
z-2BAI^>jnFgjNW_=}_V0-n#_|=NFWf$&V=91`ns4zv<!<f2MhV^<80@$4Y;Q_zlNf
zgyI+JUZ`NLWSEzN6|<9N9hc0&N}dTF`50}?rb5B31ToVcW0DH|Zk`gbVojbGlTN|a
zpd&gZ-;356bj&8ms6u>Rzj^rVNA{&Mq+F*3@TT~@rsa2dqBC@h+B0=8-pAlg<_-xE
zi&PZ2ir^t`ZN}-tjjswm8iOA{Lnd0=QsGRAE6C6e9Ufl}c>+BCl&`qMM?rNDLu%Sw
z38($*8lAlzaW{mEj4oQ<y^rgPh{MfoPiV54Y_H#175H4nMOv;2>SVqH4ucAd=JF4J
zlMQ;st@9IZr@fi1^Oo6=(%l9BfT?<VLJTnbp(gO>qC$eiOlQeYB&aWm%OWL@Ouf}>
zL}M{*;!oU(bRztn7L0OGFGac~&l4As?bpd?al0g|J-n#ao%=wulpPt=f8luEdZ*QZ
zJ|I=?z+za6%R~q3Nq-)A|03T#PU5M{+-!|kSDXPiGT@4%M0dEfd_SM)wW}eOtoSj|
zbfO|x4XvpYlO5Pp8j`qqnZsEnzV!L@Il@*5zXZQy=$4h$cT*@=MZep-Yxt&^tY@sZ
zi|Lmm+u^b*WqJovHet~A(>}TGvzT!V^-Qgls6~$5*I8m71wy0G3wY6PYqS@%y$|a-
za>OC0uGV^1u!o>23ZTQ{OC|bzms%vhHsv0-ZU2;s{-w_X^NQS)kt4b)T1?%p>=px~
zY09;AM$J@nDgIy9AyOCDqC*lAs6#nawdd5WuhZMcamWJ)zcAj?CLX1=x?JjEuC@RL
z&;w<rS#&O@?=$b9L_iUL3Z&=$Ezo={Gky6J?|aJf^S%i9(|<sFnK8Dl#YI8X>r~bf
z*1wt#@%t;>VcNOyTM_MFp91;!)4^9b?6u{2$4_(?i*m9lMB7Mkj2KRk$oDMO$$NU6
zCR+R5RTVHHZ{cvoz5S&UAt^k<mD5@CVIORcW3@dbyHUVK_Kc~|@q>NPMZv@JM0^hT
zOm8vFyE#yItIlyPxIe=@S$5SkG5hE9{;B`5{fkQfe{KKf>vJGdo=MmfPn?zASHK@h
z7qc^r<>B$R2O3curjsN!NUL2C@0*`bI$k$L$}hZRmOt@6`g9a~KU*Giz1cA-gW>oe
z;(af~`vd{wPAI34PwE60gQBb^%EyDSTYuHC%1y`HTJ6e~^FBXyDek+MV{`gUf-IJm
zCB4q9lqwulydw{IV>aqC8JB4PK0hvZJ$5p;fa!l+=T0{5Z2J6hn#aZ@c1}3}kZin$
z5-__h72fl{cyd_&qy1az{oCkv3?jAezI(Z%V~;zB`doI?X8rZ?5*1z|#3qGxzoAg@
znt#Sn(E$i}wC+<3%*6fUw8=hu_dwYv_QPdS=&#-N4OR<-q(@^bZ-D-U%Vo;0&Kk(5
z{&0Mxz#*r`b!{y&c@I89AFrgcJh){sr5c4q#r0wFqxWTz-_tp0U?{lSMQ7GR+fETv
zco~n$<*j~pC4qX;hA|Ep`0A%Sc40v^qmI7h-VE?AYy?g!GoaX`{QI7>XREyB1I;Iq
z_SqeYg;S?3|KZrnEs&olNrNi5-AQd?<3b1(p?Y!RNX_WIQ=Bg@c(3^a0eFu@o-+X!
zXJJ{UuI||Q(bAKSa;+Xy))&^kv^wk$dEB%MW#=qN?EJPnIQf%ugb+cP@vY(Etu<pF
z!f0~=Wp}ae^Iq5>e3CAIjQXYPO92$Tc%6qwea~Lc265$;y}Bc}w!}844g1F}7QL1s
zb6C6v<&Mw}sw`T`4^vgY)<Mz{JN$It)SLLi0|EF#d?iodiqEE(DBY^q91JJZYi{?s
zAkuSv9)Y>pCtk}Y^#yjU4h>-26mC1MB0t*WB{;EOXru5^3J%Iiah~=;*gLM^1R~dN
z@L^L5O&MOtcR6sLvNosqZMn{M^Z9_jbtVU~g8%Wd!Bv&Yf2J8{kvDif0E7ps(e0Z)
zNHj^}wexgOWWKpw`|s)_cc1Ms+-#Qey}hq5#GbiU-n$Q}ZI2Wj*Pw6$Z@7@1C52eL
z7Re>cz^G}%<y#dy(BS!aJ9PLwGb%zQ^-bym8OICcg~sHSn{|35$M6I<4P46cOGxtl
z+h|$t8ar1MH@~ZmX89k02>Vmm8CX=LeTjKfkq|G?*E&4hPg~mALG`V=h9h=noW4r(
zrg<yS_*=L_ANhiqyYeaW(0Z<;l!%tbcKo*6rPn93A}t+NbAe&7YKhwx8mX(`%`3Lb
z%Gb3BzLxY{C2xIS9*W{D+(;neg8d;7qB{;#-~wKDv>){!7Ixbgb4Nu)QE)K)5F_CG
zt1+4HV}myC%<WhQ!^{LP+Pzj^lQp3@+k0leQ)Df!+tCL-FI5`!5wy@vwj1U0!E3u4
zzJ%)I%HTZAR=4vAyMEhsYw%0M8rJFXB3PH)D^6J?bit0li4ZW+jmDgA+(y>7fN!O~
zfV{ZBrE|9xN@D^P`Ko)|`&jl@(?ImkTs|819yi4?j<0c(bnR<nCGYNR%@L;gn|G&!
zyr**}bH>-m4RI(@let2bBIBR$g~~Nb8Hvm+^6Pec>L>IK%T)oRfcM!^xE{Hm`H2AB
zWjG3Dr3U&FXDQM~srs;gm)SG<@J+X*m%b#A0e(Px$p}E_+K*?PT59KeYp<7qZh}}|
zdCo>Z<OX9Bv#~4&7wvKEyDkZ0+gd&zw523gXbbpR79qcO4lQ&mB|QufU=sIts>W+L
zM&0Byp8dH3QZy9gajrRe(+D)Y*(*2hTcys-j!1!MjwPpmuD&n5AU`r@m|JcFdQ76n
zmw&vq@(N`S^ntS-^ipxXySP;!;Nw)IN0_pXQP&T`gY%8N(IGJWnQ7pxi+J>Vi--4f
z-jg6Hj@_>6h01w#H)DO8{5xBAkN9#_cslXk-9S%|>^ozbZpP6t8J)wots$Jx?-Oa+
zEI3i!rqPk3ekO-Ba?j=>_pErZp7%`{ZfD^t>Wr0gs)miKy{he(L1Dn>C3~zbf$Jc&
z)~)jG)7Op|pXXMJlF*QDr1>x*I<S$FaNXV=5J=)T%WP^tJORy<`>(|p`gR0BFTaZl
zZ*gI}H3sD*IxqS)BJ@=aAEBaR0dG+A4QgQJP7nQxXj0VdCh8VBtQ{v6X6r_=e!SGp
zbU1joTYX21<Y~e-$#r}eQu6d#Sx6{-k#^qG0aCg9IOso0)2+*^y+7I5uF5bJMOB>)
zmoVWNwz%A3!4kUR<CI$HoG}7E!7g#OU=JuIW4m9h6@_Zm*pPz~l2t6@=yX_y@L)0G
zjvWrTz0aueQK~kH^$Sov<yL)71wJoQZ3$~u_TznbrA-yO$^A-;`~2BJ51JnG#5)I1
z?f%wQ>N*_mI(?qj&kXP#UMb)wI=&?**aFqOLK5+85cUA<)q%d@n7wt6N6D#>Bt{$9
zY;XM@$@`4M1cMNj=0Ja=dt8C07F;TQ2v6sdk>1O!_xKXtEuMB(w1Ld>bmYL2QoS3x
zBU#gjciz4+!<76?Wc~Ldu23;^V{zjuQ8jAl7Q<fq4x3l6V&}HK4P{d|;!F4Sp@$&B
zbzEH{=jOEE#ora^-H_xuN6f~wdrqL?Lp^gRd<9CDo94n&@hhZsx-#G(I)eM!X{zDo
z<!wK4Z{nhUR?UN?KA)HrY5kJnF@9+czN1{qX#}NtvRDZpJ7wRt`!FaDQsj|&)9wmC
z1I_=YekmF^n0g>W$$!9dcHPCL@D@RIj017Kg|Uxs?t>XMNf%!@58DH@+_>DflifbF
zXa!0mW50iKLZSsEEsub#uEoRI=?|zKZA@eiI(xMkZYq|rv|how@Q_wcc3o7HXEzhR
z7gW+eW8v*C+J+y@>%+T(Gso0EO9aQ3Hy18mQhbRAgoz(1Z4y4UPS$r06*p@fBjfRi
z-3{%IxF*m&mAVJ9!0Fc$KR3g(j+Jg7qMLI*zU4e+vYQ;9Zm4X9%}O2%KFyqOd;CfB
zgpYH?_>Wbnv##mm8n$1_d%fMxbiJ#+m{Kgo%A%Anw9|@>u&;0oMem006-n-$>}30i
zCnIr6aHE(qZPDk~VU4>;v2f4Zaw0HwG`1IF#>7O+`>N30eq-Hv36$>Rd^ysi036Et
zQ#pHjBHlF3Jd9+9c9u9iKT8>hxU;}&tpI;yx&F<YpGJ01suy3CKs(~*|8{k>;tSEO
zAS1NQ+hgV4SJZWibj!WPJ$unBQuOeews&H(_9#)$^=9U$W)DjG<&&qDdiO=fb<!=$
zCuo1UuU>YwD}De`Aew5wafxe}Y^9CVB9t3%X~@jaN1wo;Wgng_ldAw>a&f6Bw+VIb
zOLRgr>3|Qz#OlG!?&R~@!g+2(>MyWi{QJF8kpN1}>g#@txI7KIcWR&DLE|3C-wYO{
z$RjV`t{KCw&MXt1wPLuQ&#&8u^fySYQJQYiuc4BF5z7FI+@FVi;FBeMvfO=L3mUUE
z#lGI2nQd?T<0Zo0v(uSF614KN-Uy8qN9+DbT)rUw4OiO>v)xv?llgG_IKm$6ztMU5
z4VyhXwlb82kM`XV^|uG;e-pN=BjGS0naHM82dZHnRpynMu^prCB2KrUF!VzJ_wq}9
z*;w$CUVqu@&)thY_FNK6p<~!f8!*qv|MJo*y7ABiZuP~N@jW&0P3X69z^S`OIEOy>
zFxH&?iFO{)eUcL~3_oLZX|N(w?$!P>dslKPTV}P<-(ZKGR=|~E&C#D6QTC0t<bx=<
zy6Ck@sB)9nvRDE2u0&k*au0R-wfYngS)r|V=Q@5(mxMtJ=5yY#1zttq>s(8WZTLQk
z6qKp(vI}N;1!m1c7^_QiO#`{;Ny@4&y+*{$;mI}wR~i;U57E(R+;_)w7YOB){KpK*
zX{!Y=tAV098Np&=#V%jNWFvkea;%o4)itfJ;^RhZACpICQzDp{%-QO76&b$OF&hfS
z5nzieqsEX)f=E&px$T#mC3yMjn%;=J(}s%XKp^nYXBx8AUsgxeVB^np>f9@Gm%K$o
zDi^pM!L+j5`&SoMpk5RFy;w5`#EZ)xj+{D2mqy(ohN!D@c)MDRAO#E_T)N{r2+Gj<
zN`UeP-lYV1Quo&FEUZdV&Oj;3>B&mwF2l+rd)Q(EA=W$otiC|d+A~7Ccx%WycRk3B
zC~g~H`iNfmq!3$jfkW%AUY<PWKcun-7*rn%ZVM{JX}`TLb(`EJReY1De%H|saX%?x
z6dv*Ovgo~j?tHpXSDarh$6_SM9Z=z=blFkn3Oq2`dpv?fKPQL##xw0>Gyq^oPRVrr
zEqT8DkN@efe~W*9_rLw8zy9z3<^TM<Klx|>`21_2{_^ks)_?h5e*HhxU%0>fAOH2g
z4gYulFaM74=fC-l@aMn&6YUq}pZo{*H^2G)?*W3k#*h2&e@~wDKlxjK{#*P4{O|wz
zAO7+AU;XtzJe>dPcfa}h=WqYv-~RFc{_lVBpMU-L+Ar*H>(B3g{Xex|%-;@w_NTx8
tuiP*H=-2;V|K;!e@pu2{`Y(U)x5Quk>97Ar|K;!h@<08>pW?s$e*iOpHS_=g

delta 8928
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

diff --git a/tools/flashtool/esptool.py b/tools/flashtool/esptool.py
index 01176af..d1d62b4 100755
--- a/tools/flashtool/esptool.py
+++ b/tools/flashtool/esptool.py
@@ -1,20 +1,8 @@
 #!/usr/bin/env python
 #
-# ESP8266 & ESP32 family ROM Bootloader Utility
-# Copyright (C) 2014-2021 Fredrik Ahlberg, Angus Gratton, Espressif Systems (Shanghai) CO LTD, other contributors as noted.
-# https://github.com/espressif/esptool
+# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton, Espressif Systems (Shanghai) CO LTD, other contributors as noted.
 #
-# This program is free software; you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free Software
-# Foundation; either version 2 of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# this program; if not, write to the Free Software Foundation, Inc., 51 Franklin
-# Street, Fifth Floor, Boston, MA 02110-1301 USA.
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 from __future__ import division, print_function
 
@@ -27,6 +15,7 @@ import inspect
 import io
 import itertools
 import os
+import re
 import shlex
 import string
 import struct
@@ -68,7 +57,7 @@ except Exception:
         raise
 
 
-__version__ = "3.1"
+__version__ = "3.3.3"
 
 MAX_UINT32 = 0xffffffff
 MAX_UINT24 = 0xffffff
@@ -84,6 +73,9 @@ ERASE_WRITE_TIMEOUT_PER_MB = 40       # timeout (per megabyte) for erasing and w
 MEM_END_ROM_TIMEOUT = 0.05            # special short timeout for ESP_MEM_END, as it may never respond
 DEFAULT_SERIAL_WRITE_TIMEOUT = 10     # timeout for serial port write
 DEFAULT_CONNECT_ATTEMPTS = 7          # default number of times to try connection
+WRITE_BLOCK_ATTEMPTS = 3              # number of times to try writing a data block
+
+SUPPORTED_CHIPS = ['esp8266', 'esp32', 'esp32s2', 'esp32s3beta2', 'esp32s3', 'esp32c3', 'esp32c6beta', 'esp32h2beta1', 'esp32h2beta2', 'esp32c2']
 
 
 def timeout_per_mb(seconds_per_mb, size_bytes):
@@ -100,9 +92,12 @@ def _chip_to_rom_loader(chip):
         'esp32': ESP32ROM,
         'esp32s2': ESP32S2ROM,
         'esp32s3beta2': ESP32S3BETA2ROM,
-        'esp32s3beta3': ESP32S3BETA3ROM,
+        'esp32s3': ESP32S3ROM,
         'esp32c3': ESP32C3ROM,
         'esp32c6beta': ESP32C6BETAROM,
+        'esp32h2beta1': ESP32H2BETA1ROM,
+        'esp32h2beta2': ESP32H2BETA2ROM,
+        'esp32c2': ESP32C2ROM,
     }[chip]
 
 
@@ -124,13 +119,37 @@ def get_default_connected_device(serial_list, port, connect_attempts, initial_ba
             if port is not None:
                 raise
             print("%s failed to connect: %s" % (each_port, err))
+            if _esp and _esp._port:
+                _esp._port.close()
             _esp = None
     return _esp
 
 
-DETECTED_FLASH_SIZES = {0x12: '256KB', 0x13: '512KB', 0x14: '1MB',
-                        0x15: '2MB', 0x16: '4MB', 0x17: '8MB',
-                        0x18: '16MB', 0x19: '32MB', 0x1a: '64MB'}
+DETECTED_FLASH_SIZES = {
+    0x12: "256KB",
+    0x13: "512KB",
+    0x14: "1MB",
+    0x15: "2MB",
+    0x16: "4MB",
+    0x17: "8MB",
+    0x18: "16MB",
+    0x19: "32MB",
+    0x1A: "64MB",
+    0x1B: "128MB",
+    0x1C: "256MB",
+    0x20: "64MB",
+    0x21: "128MB",
+    0x22: "256MB",
+    0x32: "256KB",
+    0x33: "512KB",
+    0x34: "1MB",
+    0x35: "2MB",
+    0x36: "4MB",
+    0x37: "8MB",
+    0x38: "16MB",
+    0x39: "32MB",
+    0x3A: "64MB",
+}
 
 
 def check_supported_function(func, check_func):
@@ -139,8 +158,8 @@ def check_supported_function(func, check_func):
     bootloader function to check if it's supported.
 
     This is used to capture the multidimensional differences in
-    functionality between the ESP8266 & ESP32/32S2/32S3/32C3 ROM loaders, and the
-    software stub that runs on both. Not possible to do this cleanly
+    functionality between the ESP8266 & ESP32 (and later chips) ROM loaders, and the
+    software stub that runs on these. Not possible to do this cleanly
     via inheritance alone.
     """
     def inner(*args, **kwargs):
@@ -152,16 +171,26 @@ def check_supported_function(func, check_func):
     return inner
 
 
+def esp8266_function_only(func):
+    """ Attribute for a function only supported on ESP8266 """
+    return check_supported_function(func, lambda o: o.CHIP_NAME == "ESP8266")
+
+
 def stub_function_only(func):
     """ Attribute for a function only supported in the software stub loader """
     return check_supported_function(func, lambda o: o.IS_STUB)
 
 
 def stub_and_esp32_function_only(func):
-    """ Attribute for a function only supported by software stubs or ESP32/32S2/32S3/32C3 ROM """
+    """ Attribute for a function only supported by software stubs or ESP32 and later chips ROM """
     return check_supported_function(func, lambda o: o.IS_STUB or isinstance(o, ESP32ROM))
 
 
+def esp32s3_or_newer_function_only(func):
+    """ Attribute for a function only supported by ESP32S3 and later chips ROM """
+    return check_supported_function(func, lambda o: isinstance(o, ESP32S3ROM) or isinstance(o, ESP32C3ROM))
+
+
 PYTHON2 = sys.version_info[0] < 3  # True if on pre-Python 3
 
 # Function to return nth byte of a bitstring
@@ -204,14 +233,9 @@ def _mask_to_shift(mask):
     return shift
 
 
-def esp8266_function_only(func):
-    """ Attribute for a function only supported on ESP8266 """
-    return check_supported_function(func, lambda o: o.CHIP_NAME == "ESP8266")
-
-
 class ESPLoader(object):
     """ Base class providing access to ESP ROM & software stub bootloaders.
-    Subclasses provide ESP8266 & ESP32 specific functionality.
+    Subclasses provide ESP8266 & ESP32 Family specific functionality.
 
     Don't instantiate this base class directly, either instantiate a subclass or
     call ESPLoader.detect_chip() which will interrogate the chip and return the
@@ -221,8 +245,12 @@ class ESPLoader(object):
     CHIP_NAME = "Espressif device"
     IS_STUB = False
 
+    FPGA_SLOW_BOOT = False
+
     DEFAULT_PORT = "/dev/ttyUSB0"
 
+    USES_RFC2217 = False
+
     # Commands supported by ESP8266 ROM bootloader
     ESP_FLASH_BEGIN = 0x02
     ESP_FLASH_DATA  = 0x03
@@ -234,7 +262,7 @@ class ESPLoader(object):
     ESP_WRITE_REG   = 0x09
     ESP_READ_REG    = 0x0a
 
-    # Some comands supported by ESP32 ROM bootloader (or -8266 w/ stub)
+    # Some comands supported by ESP32 and later chips ROM bootloader (or -8266 w/ stub)
     ESP_SPI_SET_PARAMS = 0x0B
     ESP_SPI_ATTACH     = 0x0D
     ESP_READ_FLASH_SLOW  = 0x0e  # ROM only, much slower than the stub flash read
@@ -244,7 +272,7 @@ class ESPLoader(object):
     ESP_FLASH_DEFL_END   = 0x12
     ESP_SPI_FLASH_MD5    = 0x13
 
-    # Commands supported by ESP32-S2/S3/C3/C6 ROM bootloader only
+    # Commands supported by ESP32-S2 and later chips ROM bootloader only
     ESP_GET_SECURITY_INFO = 0x14
 
     # Some commands supported by stub only
@@ -295,6 +323,9 @@ class ESPLoader(object):
     # Device PIDs
     USB_JTAG_SERIAL_PID = 0x1001
 
+    # Chip IDs that are no longer supported by esptool
+    UNSUPPORTED_CHIPS = {6: "ESP32-S3(beta 3)"}
+
     def __init__(self, port=DEFAULT_PORT, baud=ESP_ROM_BAUD, trace_enabled=False):
         """Base constructor for ESPLoader bootloader interaction
 
@@ -308,6 +339,7 @@ class ESPLoader(object):
 
         """
         self.secure_download_mode = False  # flag is set to True if esptool detects the ROM is in Secure Download Mode
+        self.stub_is_disabled = False  # flag is set to True if esptool detects conditions which require the stub to be disabled
 
         if isinstance(port, basestring):
             self._port = serial.serial_for_url(port)
@@ -343,36 +375,62 @@ class ESPLoader(object):
                     connect_attempts=DEFAULT_CONNECT_ATTEMPTS):
         """ Use serial access to detect the chip type.
 
-        We use the UART's datecode register for this, it's mapped at
-        the same address on ESP8266 & ESP32 so we can use one
-        memory read and compare to the datecode register for each chip
-        type.
+        First, get_security_info command is sent to detect the ID of the chip
+        (supported only by ESP32-C3 and later, works even in the Secure Download Mode).
+        If this fails, we reconnect and fall-back to reading the magic number.
+        It's mapped at a specific ROM address and has a different value on each chip model.
+        This way we can use one memory read and compare it to the magic number for each chip type.
 
         This routine automatically performs ESPLoader.connect() (passing
         connect_mode parameter) as part of querying the chip.
         """
+        inst = None
         detect_port = ESPLoader(port, baud, trace_enabled=trace_enabled)
+        if detect_port.serial_port.startswith("rfc2217:"):
+            detect_port.USES_RFC2217 = True
         detect_port.connect(connect_mode, connect_attempts, detecting=True)
         try:
             print('Detecting chip type...', end='')
-            sys.stdout.flush()
-            chip_magic_value = detect_port.read_reg(ESPLoader.CHIP_DETECT_MAGIC_REG_ADDR)
+            res = detect_port.check_command('get security info', ESPLoader.ESP_GET_SECURITY_INFO, b'')
+            res = struct.unpack("<IBBBBBBBBI", res[:16])  # 4b flags, 1b flash_crypt_cnt, 7*1b key_purposes, 4b chip_id
+            chip_id = res[9]  # 2/4 status bytes invariant
 
-            for cls in [ESP8266ROM, ESP32ROM, ESP32S2ROM, ESP32S3BETA2ROM, ESP32S3BETA3ROM, ESP32C3ROM, ESP32C6BETAROM]:
-                if chip_magic_value in cls.CHIP_DETECT_MAGIC_VALUE:
-                    # don't connect a second time
+            for cls in [ESP32S3BETA2ROM, ESP32S3ROM, ESP32C3ROM, ESP32C6BETAROM, ESP32H2BETA1ROM, ESP32C2ROM, ESP32H2BETA2ROM]:
+                if chip_id == cls.IMAGE_CHIP_ID:
                     inst = cls(detect_port._port, baud, trace_enabled=trace_enabled)
                     inst._post_connect()
-                    print(' %s' % inst.CHIP_NAME, end='')
-                    if detect_port.sync_stub_detected:
-                        inst = inst.STUB_CLASS(inst)
-                        inst.sync_stub_detected = True
-                    return inst
-        except UnsupportedCommandError:
-            raise FatalError("Unsupported Command Error received. Probably this means Secure Download Mode is enabled, "
-                             "autodetection will not work. Need to manually specify the chip.")
+                    try:
+                        inst.read_reg(ESPLoader.CHIP_DETECT_MAGIC_REG_ADDR)  # Dummy read to check Secure Download mode
+                    except UnsupportedCommandError:
+                        inst.secure_download_mode = True
+        except (UnsupportedCommandError, struct.error, FatalError) as e:
+            # UnsupportedCmdErr: ESP8266/ESP32 ROM | struct.err: ESP32-S2 | FatalErr: ESP8266/ESP32 STUB
+            print(" Unsupported detection protocol, switching and trying again...")
+            try:
+                # ESP32/ESP8266 are reset after an unsupported command, need to connect again (not needed on ESP32-S2)
+                if not isinstance(e, struct.error):
+                    detect_port.connect(connect_mode, connect_attempts, detecting=True, warnings=False)
+                print('Detecting chip type...', end='')
+                sys.stdout.flush()
+                chip_magic_value = detect_port.read_reg(ESPLoader.CHIP_DETECT_MAGIC_REG_ADDR)
+
+                for cls in [ESP8266ROM, ESP32ROM, ESP32S2ROM, ESP32S3BETA2ROM, ESP32S3ROM,
+                            ESP32C3ROM, ESP32C6BETAROM, ESP32H2BETA1ROM, ESP32C2ROM, ESP32H2BETA2ROM]:
+                    if chip_magic_value in cls.CHIP_DETECT_MAGIC_VALUE:
+                        inst = cls(detect_port._port, baud, trace_enabled=trace_enabled)
+                        inst._post_connect()
+                        inst.check_chip_id()
+            except UnsupportedCommandError:
+                raise FatalError("Unsupported Command Error received. Probably this means Secure Download Mode is enabled, "
+                                 "autodetection will not work. Need to manually specify the chip.")
         finally:
-            print('')  # end line
+            if inst is not None:
+                print(' %s' % inst.CHIP_NAME, end='')
+                if detect_port.sync_stub_detected:
+                    inst = inst.STUB_CLASS(inst)
+                    inst.sync_stub_detected = True
+                print('')  # end line
+                return inst
         raise FatalError("Unexpected CHIP magic value 0x%08x. Failed to autodetect chip type." % (chip_magic_value))
 
     """ Read a SLIP packet from the serial port """
@@ -512,22 +570,24 @@ class ESPLoader(object):
         active_port = self._port.port
 
         # Pyserial only identifies regular ports, URL handlers are not supported
-        if not active_port.startswith(("COM", "/dev/")):
+        if not active_port.lower().startswith(("com", "/dev/")):
             print("\nDevice PID identification is only supported on COM and /dev/ serial ports.")
             return
         # Return the real path if the active port is a symlink
         if active_port.startswith("/dev/") and os.path.islink(active_port):
             active_port = os.path.realpath(active_port)
 
+        # The "cu" (call-up) device has to be used for outgoing communication on MacOS
+        if sys.platform == "darwin" and "tty" in active_port:
+            active_port = [active_port, active_port.replace("tty", "cu")]
         ports = list_ports.comports()
         for p in ports:
-            if p.device == active_port:
+            if p.device in active_port:
                 return p.pid
         print("\nFailed to get PID of a device on {}, using standard reset sequence.".format(active_port))
 
-    def bootloader_reset(self, esp32r0_delay=False, usb_jtag_serial=False):
-        """ Issue a reset-to-bootloader, with esp32r0 workaround options
-        and USB-JTAG-Serial custom reset sequence option
+    def bootloader_reset(self, usb_jtag_serial=False, extra_delay=False):
+        """ Issue a reset-to-bootloader, with USB-JTAG-Serial custom reset sequence option
         """
         # RTS = either CH_PD/EN or nRESET (both active low = chip in reset)
         # DTR = GPIO0 (active low = boot to flasher)
@@ -550,36 +610,23 @@ class ESPLoader(object):
             self._setDTR(False)
             self._setRTS(False)
         else:
+            # This fpga delay is for Espressif internal use
+            fpga_delay = True if self.FPGA_SLOW_BOOT and os.environ.get("ESPTOOL_ENV_FPGA", "").strip() == "1" else False
+            delay = 7 if fpga_delay else 0.5 if extra_delay else 0.05  # 0.5 needed for ESP32 rev0 and rev1
+
             self._setDTR(False)  # IO0=HIGH
             self._setRTS(True)   # EN=LOW, chip in reset
             time.sleep(0.1)
-            if esp32r0_delay:
-                # Some chips are more likely to trigger the esp32r0
-                # watchdog reset silicon bug if they're held with EN=LOW
-                # for a longer period
-                time.sleep(1.2)
             self._setDTR(True)   # IO0=LOW
             self._setRTS(False)  # EN=HIGH, chip out of reset
-            if esp32r0_delay:
-                # Sleep longer after reset.
-                # This workaround only works on revision 0 ESP32 chips,
-                # it exploits a silicon bug spurious watchdog reset.
-                time.sleep(0.4)  # allow watchdog reset to occur
-            time.sleep(0.05)
+            time.sleep(delay)
             self._setDTR(False)  # IO0=HIGH, done
 
-    def _connect_attempt(self, mode='default_reset', esp32r0_delay=False, usb_jtag_serial=False):
-        """ A single connection attempt, with esp32r0 workaround options """
-        # esp32r0_delay is a workaround for bugs with the most common auto reset
-        # circuit and Windows, if the EN pin on the dev board does not have
-        # enough capacitance.
-        #
-        # Newer dev boards shouldn't have this problem (higher value capacitor
-        # on the EN pin), and ESP32 revision 1 can't use this workaround as it
-        # relies on a silicon bug.
-        #
-        # Details: https://github.com/espressif/esptool/issues/136
+    def _connect_attempt(self, mode='default_reset', usb_jtag_serial=False, extra_delay=False):
+        """ A single connection attempt """
         last_error = None
+        boot_log_detected = False
+        download_mode = False
 
         # If we're doing no_sync, we're likely communicating as a pass through
         # with an intermediate device to the ESP32
@@ -587,7 +634,18 @@ class ESPLoader(object):
             return last_error
 
         if mode != 'no_reset':
-            self.bootloader_reset(esp32r0_delay, usb_jtag_serial)
+            if not self.USES_RFC2217:  # Might block on rfc2217 ports
+                self._port.reset_input_buffer()  # Empty serial buffer to isolate boot log
+            self.bootloader_reset(usb_jtag_serial, extra_delay)
+
+            # Detect the ROM boot log and check actual boot mode (ESP32 and later only)
+            waiting = self._port.inWaiting()
+            read_bytes = self._port.read(waiting)
+            data = re.search(b'boot:(0x[0-9a-fA-F]+)(.*waiting for download)?', read_bytes, re.DOTALL)
+            if data is not None:
+                boot_log_detected = True
+                boot_mode = data.group(1)
+                download_mode = data.group(2) is not None
 
         for _ in range(5):
             try:
@@ -596,13 +654,15 @@ class ESPLoader(object):
                 self.sync()
                 return None
             except FatalError as e:
-                if esp32r0_delay:
-                    print('_', end='')
-                else:
-                    print('.', end='')
+                print('.', end='')
                 sys.stdout.flush()
                 time.sleep(0.05)
                 last_error = e
+
+        if boot_log_detected:
+            last_error = FatalError("Wrong boot mode detected ({})! The chip needs to be in download mode.".format(boot_mode.decode("utf-8")))
+            if download_mode:
+                last_error = FatalError("Download mode successfully detected, but getting no sync reply: The serial TX path seems to be down.")
         return last_error
 
     def get_memory_region(self, name):
@@ -613,9 +673,9 @@ class ESPLoader(object):
         except IndexError:
             return None
 
-    def connect(self, mode='default_reset', attempts=DEFAULT_CONNECT_ATTEMPTS, detecting=False):
+    def connect(self, mode='default_reset', attempts=DEFAULT_CONNECT_ATTEMPTS, detecting=False, warnings=True):
         """ Try connecting repeatedly until successful, or giving up """
-        if mode in ['no_reset', 'no_reset_no_sync']:
+        if warnings and mode in ['no_reset', 'no_reset_no_sync']:
             print('WARNING: Pre-connection option "{}" was selected.'.format(mode),
                   'Connection may fail if the chip is not in bootloader or flasher stub mode.')
         print('Connecting...', end='')
@@ -625,18 +685,17 @@ class ESPLoader(object):
         usb_jtag_serial = (mode == 'usb_reset') or (self._get_pid() == self.USB_JTAG_SERIAL_PID)
 
         try:
-            for _ in range(attempts) if attempts > 0 else itertools.count():
-                last_error = self._connect_attempt(mode=mode, esp32r0_delay=False, usb_jtag_serial=usb_jtag_serial)
-                if last_error is None:
-                    break
-                last_error = self._connect_attempt(mode=mode, esp32r0_delay=True, usb_jtag_serial=usb_jtag_serial)
+            for _, extra_delay in zip(range(attempts) if attempts > 0 else itertools.count(), itertools.cycle((False, True))):
+                last_error = self._connect_attempt(mode=mode, usb_jtag_serial=usb_jtag_serial, extra_delay=extra_delay)
                 if last_error is None:
                     break
         finally:
             print('')  # end 'Connecting...' line
 
         if last_error is not None:
-            raise FatalError('Failed to connect to %s: %s' % (self.CHIP_NAME, last_error))
+            raise FatalError('Failed to connect to {}: {}'
+                             '\nFor troubleshooting steps visit: '
+                             'https://docs.espressif.com/projects/esptool/en/latest/troubleshooting.html'.format(self.CHIP_NAME, last_error))
 
         if not detecting:
             try:
@@ -644,11 +703,12 @@ class ESPLoader(object):
                 chip_magic_value = self.read_reg(ESPLoader.CHIP_DETECT_MAGIC_REG_ADDR)
                 if chip_magic_value not in self.CHIP_DETECT_MAGIC_VALUE:
                     actually = None
-                    for cls in [ESP8266ROM, ESP32ROM, ESP32S2ROM, ESP32S3BETA2ROM, ESP32S3BETA3ROM, ESP32C3ROM]:
+                    for cls in [ESP8266ROM, ESP32ROM, ESP32S2ROM, ESP32S3BETA2ROM, ESP32S3ROM,
+                                ESP32C3ROM, ESP32H2BETA1ROM, ESP32H2BETA2ROM, ESP32C2ROM, ESP32C6BETAROM]:
                         if chip_magic_value in cls.CHIP_DETECT_MAGIC_VALUE:
                             actually = cls
                             break
-                    if actually is None:
+                    if warnings and actually is None:
                         print(("WARNING: This chip doesn't appear to be a %s (chip magic value 0x%08x). "
                                "Probably it is unsupported by this version of esptool.") % (self.CHIP_NAME, chip_magic_value))
                     else:
@@ -656,6 +716,7 @@ class ESPLoader(object):
             except UnsupportedCommandError:
                 self.secure_download_mode = True
             self._post_connect()
+            self.check_chip_id()
 
     def _post_connect(self):
         """
@@ -751,7 +812,8 @@ class ESPLoader(object):
             timeout = timeout_per_mb(ERASE_REGION_TIMEOUT_PER_MB, size)  # ROM performs the erase up front
 
         params = struct.pack('<IIII', erase_size, num_blocks, self.FLASH_WRITE_SIZE, offset)
-        if isinstance(self, (ESP32S2ROM, ESP32S3BETA2ROM, ESP32S3BETA3ROM, ESP32C3ROM, ESP32C6BETAROM)) and not self.IS_STUB:
+        if isinstance(self, (ESP32S2ROM, ESP32S3BETA2ROM, ESP32S3ROM, ESP32C3ROM,
+                             ESP32C6BETAROM, ESP32H2BETA1ROM, ESP32C2ROM, ESP32H2BETA2ROM)) and not self.IS_STUB:
             params += struct.pack('<I', 1 if begin_rom_encrypted else 0)
         self.check_command("enter Flash download mode", self.ESP_FLASH_BEGIN,
                            params, timeout=timeout)
@@ -759,26 +821,52 @@ class ESPLoader(object):
             print("Took %.2fs to erase flash block" % (time.time() - t))
         return num_blocks
 
-    """ Write block to flash """
     def flash_block(self, data, seq, timeout=DEFAULT_TIMEOUT):
-        self.check_command("write to target Flash after seq %d" % seq,
-                           self.ESP_FLASH_DATA,
-                           struct.pack('<IIII', len(data), seq, 0, 0) + data,
-                           self.checksum(data),
-                           timeout=timeout)
+        """Write block to flash, retry if fail"""
+        for attempts_left in range(WRITE_BLOCK_ATTEMPTS - 1, -1, -1):
+            try:
+                self.check_command(
+                    "write to target Flash after seq %d" % seq,
+                    self.ESP_FLASH_DATA,
+                    struct.pack("<IIII", len(data), seq, 0, 0) + data,
+                    self.checksum(data),
+                    timeout=timeout,
+                )
+                break
+            except FatalError:
+                if attempts_left:
+                    self.trace(
+                        "Block write failed, "
+                        "retrying with {} attempts left".format(attempts_left)
+                    )
+                else:
+                    raise
 
-    """ Encrypt before writing to flash """
     def flash_encrypt_block(self, data, seq, timeout=DEFAULT_TIMEOUT):
-        if isinstance(self, (ESP32S2ROM, ESP32C3ROM)) and not self.IS_STUB:
+        """Encrypt, write block to flash, retry if fail"""
+        if isinstance(self, (ESP32S2ROM, ESP32C3ROM, ESP32S3ROM, ESP32H2BETA1ROM, ESP32C2ROM, ESP32H2BETA2ROM)) and not self.IS_STUB:
             # ROM support performs the encrypted writes via the normal write command,
             # triggered by flash_begin(begin_rom_encrypted=True)
             return self.flash_block(data, seq, timeout)
 
-        self.check_command("Write encrypted to target Flash after seq %d" % seq,
-                           self.ESP_FLASH_ENCRYPT_DATA,
-                           struct.pack('<IIII', len(data), seq, 0, 0) + data,
-                           self.checksum(data),
-                           timeout=timeout)
+        for attempts_left in range(WRITE_BLOCK_ATTEMPTS - 1, -1, -1):
+            try:
+                self.check_command(
+                    "Write encrypted to target Flash after seq %d" % seq,
+                    self.ESP_FLASH_ENCRYPT_DATA,
+                    struct.pack("<IIII", len(data), seq, 0, 0) + data,
+                    self.checksum(data),
+                    timeout=timeout,
+                )
+                break
+            except FatalError:
+                if attempts_left:
+                    self.trace(
+                        "Encrypted block write failed, "
+                        "retrying with {} attempts left".format(attempts_left)
+                    )
+                else:
+                    raise
 
     """ Leave flash mode and run/reboot """
     def flash_finish(self, reboot=False):
@@ -798,12 +886,23 @@ class ESPLoader(object):
         return self.run_spiflash_command(SPIFLASH_RDID, b"", 24)
 
     def get_security_info(self):
-        # TODO: this only works on the ESP32S2 ROM code loader and needs to work in stub loader also
         res = self.check_command('get security info', self.ESP_GET_SECURITY_INFO, b'')
-        res = struct.unpack("<IBBBBBBBB", res)
-        flags, flash_crypt_cnt, key_purposes = res[0], res[1], res[2:]
-        # TODO: pack this as some kind of better data type
-        return (flags, flash_crypt_cnt, key_purposes)
+        esp32s2 = True if len(res) == 12 else False
+        res = struct.unpack("<IBBBBBBBB" if esp32s2 else "<IBBBBBBBBII", res)
+        return {
+            "flags": res[0],
+            "flash_crypt_cnt": res[1],
+            "key_purposes": res[2:9],
+            "chip_id": None if esp32s2 else res[9],
+            "api_version": None if esp32s2 else res[10],
+        }
+
+    @esp32s3_or_newer_function_only
+    def get_chip_id(self):
+        res = self.check_command('get security info', self.ESP_GET_SECURITY_INFO, b'')
+        res = struct.unpack("<IBBBBBBBBI", res[:16])  # 4b flags, 1b flash_crypt_cnt, 7*1b key_purposes, 4b chip_id
+        chip_id = res[9]  # 2/4 status bytes invariant
+        return chip_id
 
     @classmethod
     def parse_flash_size_arg(cls, arg):
@@ -813,6 +912,14 @@ class ESPLoader(object):
             raise FatalError("Flash size '%s' is not supported by this chip type. Supported sizes: %s"
                              % (arg, ", ".join(cls.FLASH_SIZES.keys())))
 
+    @classmethod
+    def parse_flash_freq_arg(cls, arg):
+        try:
+            return cls.FLASH_FREQUENCY[arg]
+        except KeyError:
+            raise FatalError("Flash frequency '%s' is not supported by this chip type. Supported frequencies: %s"
+                             % (arg, ", ".join(cls.FLASH_FREQUENCY.keys())))
+
     def run_stub(self, stub=None):
         if stub is None:
             stub = self.STUB_CODE
@@ -860,7 +967,8 @@ class ESPLoader(object):
             timeout = timeout_per_mb(ERASE_REGION_TIMEOUT_PER_MB, write_size)  # ROM performs the erase up front
         print("Compressed %d bytes to %d..." % (size, compsize))
         params = struct.pack('<IIII', write_size, num_blocks, self.FLASH_WRITE_SIZE, offset)
-        if isinstance(self, (ESP32S2ROM, ESP32S3BETA2ROM, ESP32S3BETA3ROM, ESP32C3ROM, ESP32C6BETAROM)) and not self.IS_STUB:
+        if isinstance(self, (ESP32S2ROM, ESP32S3BETA2ROM, ESP32S3ROM, ESP32C3ROM,
+                             ESP32C6BETAROM, ESP32H2BETA1ROM, ESP32C2ROM, ESP32H2BETA2ROM)) and not self.IS_STUB:
             params += struct.pack('<I', 0)  # extra param is to enter encrypted flash mode via ROM (not supported currently)
         self.check_command("enter compressed flash mode", self.ESP_FLASH_DEFL_BEGIN, params, timeout=timeout)
         if size != 0 and not self.IS_STUB:
@@ -868,11 +976,27 @@ class ESPLoader(object):
             print("Took %.2fs to erase flash block" % (time.time() - t))
         return num_blocks
 
-    """ Write block to flash, send compressed """
     @stub_and_esp32_function_only
     def flash_defl_block(self, data, seq, timeout=DEFAULT_TIMEOUT):
-        self.check_command("write compressed data to flash after seq %d" % seq,
-                           self.ESP_FLASH_DEFL_DATA, struct.pack('<IIII', len(data), seq, 0, 0) + data, self.checksum(data), timeout=timeout)
+        """Write block to flash, send compressed, retry if fail"""
+        for attempts_left in range(WRITE_BLOCK_ATTEMPTS - 1, -1, -1):
+            try:
+                self.check_command(
+                    "write compressed data to flash after seq %d" % seq,
+                    self.ESP_FLASH_DEFL_DATA,
+                    struct.pack("<IIII", len(data), seq, 0, 0) + data,
+                    self.checksum(data),
+                    timeout=timeout,
+                )
+                break
+            except FatalError:
+                if attempts_left:
+                    self.trace(
+                        "Compressed block write failed, "
+                        "retrying with {} attempts left".format(attempts_left)
+                    )
+                else:
+                    raise
 
     """ Leave compressed flash mode and run/reboot """
     @stub_and_esp32_function_only
@@ -999,7 +1123,7 @@ class ESPLoader(object):
         self.check_command("set SPI params", ESP32ROM.ESP_SPI_SET_PARAMS,
                            struct.pack('<IIIIII', fl_id, total_size, block_size, sector_size, page_size, status_mask))
 
-    def run_spiflash_command(self, spiflash_command, data=b"", read_bits=0):
+    def run_spiflash_command(self, spiflash_command, data=b"", read_bits=0, addr=None, addr_len=0, dummy_len=0):
         """Run an arbitrary SPI flash command.
 
         This function uses the "USR_COMMAND" functionality in the ESP
@@ -1013,20 +1137,23 @@ class ESPLoader(object):
 
         # SPI_USR register flags
         SPI_USR_COMMAND = (1 << 31)
+        SPI_USR_ADDR    = (1 << 30)
+        SPI_USR_DUMMY   = (1 << 29)
         SPI_USR_MISO    = (1 << 28)
         SPI_USR_MOSI    = (1 << 27)
 
         # SPI registers, base address differs ESP32* vs 8266
         base = self.SPI_REG_BASE
         SPI_CMD_REG       = base + 0x00
+        SPI_ADDR_REG      = base + 0x04
         SPI_USR_REG       = base + self.SPI_USR_OFFS
         SPI_USR1_REG      = base + self.SPI_USR1_OFFS
         SPI_USR2_REG      = base + self.SPI_USR2_OFFS
         SPI_W0_REG        = base + self.SPI_W0_OFFS
 
-        # following two registers are ESP32 & 32S2/32C3 only
+        # following two registers are ESP32 and later chips only
         if self.SPI_MOSI_DLEN_OFFS is not None:
-            # ESP32/32S2/32C3 has a more sophisticated way to set up "user" commands
+            # ESP32 and later chips have a more sophisticated way to set up "user" commands
             def set_data_lengths(mosi_bits, miso_bits):
                 SPI_MOSI_DLEN_REG = base + self.SPI_MOSI_DLEN_OFFS
                 SPI_MISO_DLEN_REG = base + self.SPI_MISO_DLEN_OFFS
@@ -1034,23 +1161,33 @@ class ESPLoader(object):
                     self.write_reg(SPI_MOSI_DLEN_REG, mosi_bits - 1)
                 if miso_bits > 0:
                     self.write_reg(SPI_MISO_DLEN_REG, miso_bits - 1)
+                flags = 0
+                if dummy_len > 0:
+                    flags |= (dummy_len - 1)
+                if addr_len > 0:
+                    flags |= (addr_len - 1) << SPI_USR_ADDR_LEN_SHIFT
+                if flags:
+                    self.write_reg(SPI_USR1_REG, flags)
         else:
-
             def set_data_lengths(mosi_bits, miso_bits):
                 SPI_DATA_LEN_REG = SPI_USR1_REG
                 SPI_MOSI_BITLEN_S = 17
                 SPI_MISO_BITLEN_S = 8
                 mosi_mask = 0 if (mosi_bits == 0) else (mosi_bits - 1)
                 miso_mask = 0 if (miso_bits == 0) else (miso_bits - 1)
-                self.write_reg(SPI_DATA_LEN_REG,
-                               (miso_mask << SPI_MISO_BITLEN_S) | (
-                                   mosi_mask << SPI_MOSI_BITLEN_S))
+                flags = (miso_mask << SPI_MISO_BITLEN_S) | (mosi_mask << SPI_MOSI_BITLEN_S)
+                if dummy_len > 0:
+                    flags |= (dummy_len - 1)
+                if addr_len > 0:
+                    flags |= (addr_len - 1) << SPI_USR_ADDR_LEN_SHIFT
+                self.write_reg(SPI_DATA_LEN_REG, flags)
 
         # SPI peripheral "command" bitmasks for SPI_CMD_REG
         SPI_CMD_USR  = (1 << 18)
 
         # shift values
         SPI_USR2_COMMAND_LEN_SHIFT = 28
+        SPI_USR_ADDR_LEN_SHIFT = 26
 
         if read_bits > 32:
             raise FatalError("Reading more than 32 bits back from a SPI flash operation is unsupported")
@@ -1065,10 +1202,16 @@ class ESPLoader(object):
             flags |= SPI_USR_MISO
         if data_bits > 0:
             flags |= SPI_USR_MOSI
+        if addr_len > 0:
+            flags |= SPI_USR_ADDR
+        if dummy_len > 0:
+            flags |= SPI_USR_DUMMY
         set_data_lengths(data_bits, read_bits)
         self.write_reg(SPI_USR_REG, flags)
         self.write_reg(SPI_USR2_REG,
                        (7 << SPI_USR2_COMMAND_LEN_SHIFT) | spiflash_command)
+        if addr and addr_len > 0:
+            self.write_reg(SPI_ADDR_REG, addr)
         if data_bits == 0:
             self.write_reg(SPI_W0_REG, 0)  # clear data register before we read it
         else:
@@ -1093,6 +1236,10 @@ class ESPLoader(object):
         self.write_reg(SPI_USR2_REG, old_spi_usr2)
         return status
 
+    def read_spiflash_sfdp(self, addr, read_bits):
+        CMD_RDSFDP = 0x5A
+        return self.run_spiflash_command(CMD_RDSFDP, read_bits=read_bits, addr=addr, addr_len=24, dummy_len=8)
+
     def read_status(self, num_bytes=2):
         """Read up to 24 bits (num_bytes) of SPI flash status register contents
         via RDSR, RDSR2, RDSR3 commands
@@ -1191,6 +1338,17 @@ class ESPLoader(object):
                 # in the stub loader
                 self.command(self.ESP_RUN_USER_CODE, wait_response=False)
 
+    def check_chip_id(self):
+        try:
+            chip_id = self.get_chip_id()
+            if chip_id != self.IMAGE_CHIP_ID:
+                print("WARNING: Chip ID {} ({}) doesn't match expected Chip ID {}. esptool may not work correctly."
+                      .format(chip_id, self.UNSUPPORTED_CHIPS.get(chip_id, 'Unknown'), self.IMAGE_CHIP_ID))
+                # Try to flash anyways by disabling stub
+                self.stub_is_disabled = True
+        except NotImplementedInROMError:
+            pass
+
 
 class ESP8266ROM(ESPLoader):
     """ Access class for ESP8266 ROM bootloader
@@ -1229,6 +1387,13 @@ class ESP8266ROM(ESPLoader):
         '16MB': 0x90,
     }
 
+    FLASH_FREQUENCY = {
+        '80m': 0xf,
+        '40m': 0x0,
+        '26m': 0x1,
+        '20m': 0x2,
+    }
+
     BOOTLOADER_FLASH_OFFSET = 0
 
     MEMORY_MAP = [[0x3FF00000, 0x3FF00010, "DPORT"],
@@ -1366,6 +1531,8 @@ class ESP32ROM(ESPLoader):
     IMAGE_CHIP_ID = 0
     IS_STUB = False
 
+    FPGA_SLOW_BOOT = True
+
     CHIP_DETECT_MAGIC_VALUE = [0x00f01d83]
 
     IROM_MAP_START = 0x400d0000
@@ -1389,6 +1556,9 @@ class ESP32ROM(ESPLoader):
     EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = (1 << 7)  # EFUSE_RD_DISABLE_DL_ENCRYPT
 
     DR_REG_SYSCON_BASE = 0x3ff66000
+    APB_CTL_DATE_ADDR = DR_REG_SYSCON_BASE + 0x7C
+    APB_CTL_DATE_V = 0x1
+    APB_CTL_DATE_S = 31
 
     SPI_W0_OFFS = 0x80
 
@@ -1401,7 +1571,17 @@ class ESP32ROM(ESPLoader):
         '2MB': 0x10,
         '4MB': 0x20,
         '8MB': 0x30,
-        '16MB': 0x40
+        '16MB': 0x40,
+        '32MB': 0x50,
+        '64MB': 0x60,
+        '128MB': 0x70
+    }
+
+    FLASH_FREQUENCY = {
+        '80m': 0xf,
+        '40m': 0x0,
+        '26m': 0x1,
+        '20m': 0x2,
     }
 
     BOOTLOADER_FLASH_OFFSET = 0x1000
@@ -1485,28 +1665,37 @@ class ESP32ROM(ESPLoader):
         pkg_version += ((word3 >> 2) & 0x1) << 3
         return pkg_version
 
-    def get_chip_revision(self):
-        word3 = self.read_efuse(3)
-        word5 = self.read_efuse(5)
-        apb_ctl_date = self.read_reg(self.DR_REG_SYSCON_BASE + 0x7C)
+    # Returns new version format based on major and minor versions
+    def get_chip_full_revision(self):
+        return self.get_major_chip_version() * 100 + self.get_minor_chip_version()
 
-        rev_bit0 = (word3 >> 15) & 0x1
-        rev_bit1 = (word5 >> 20) & 0x1
-        rev_bit2 = (apb_ctl_date >> 31) & 0x1
-        if rev_bit0:
-            if rev_bit1:
-                if rev_bit2:
-                    return 3
-                else:
-                    return 2
-            else:
-                return 1
-        return 0
+    # Returns old version format (ECO number). Use the new format get_chip_full_revision().
+    def get_chip_revision(self):
+        return self.get_major_chip_version()
+
+    def get_minor_chip_version(self):
+        return (self.read_efuse(5) >> 24) & 0x3
+
+    def get_major_chip_version(self):
+        rev_bit0 = (self.read_efuse(3) >> 15) & 0x1
+        rev_bit1 = (self.read_efuse(5) >> 20) & 0x1
+        apb_ctl_date = self.read_reg(self.APB_CTL_DATE_ADDR)
+        rev_bit2 = (apb_ctl_date >> self.APB_CTL_DATE_S) & self.APB_CTL_DATE_V
+        combine_value = (rev_bit2 << 2) | (rev_bit1 << 1) | rev_bit0
+
+        revision = {
+            0: 0,
+            1: 1,
+            3: 2,
+            7: 3,
+        }.get(combine_value, 0)
+        return revision
 
     def get_chip_description(self):
         pkg_version = self.get_pkg_version()
-        chip_revision = self.get_chip_revision()
-        rev3 = (chip_revision == 3)
+        major_rev = self.get_major_chip_version()
+        minor_rev = self.get_minor_chip_version()
+        rev3 = major_rev == 3
         single_core = self.read_efuse(3) & (1 << 0)  # CHIP_VER DIS_APP_CPU
 
         chip_name = {
@@ -1516,13 +1705,14 @@ class ESP32ROM(ESPLoader):
             4: "ESP32-U4WDH",
             5: "ESP32-PICO-V3" if rev3 else "ESP32-PICO-D4",
             6: "ESP32-PICO-V3-02",
+            7: "ESP32-D0WDR2-V3",
         }.get(pkg_version, "unknown ESP32")
 
         # ESP32-D0WD-V3, ESP32-D0WDQ6-V3
         if chip_name.startswith("ESP32-D0WD") and rev3:
             chip_name += "-V3"
 
-        return "%s (revision %d)" % (chip_name, chip_revision)
+        return "%s (revision v%d.%d)" % (chip_name, major_rev, minor_rev)
 
     def get_chip_features(self):
         features = ["WiFi"]
@@ -1638,6 +1828,8 @@ class ESP32S2ROM(ESP32ROM):
     CHIP_NAME = "ESP32-S2"
     IMAGE_CHIP_ID = 2
 
+    FPGA_SLOW_BOOT = False
+
     IROM_MAP_START = 0x40080000
     IROM_MAP_END   = 0x40b80000
     DROM_MAP_START = 0x3F000000
@@ -1662,6 +1854,8 @@ class ESP32S2ROM(ESP32ROM):
     # todo: use espefuse APIs to get this info
     EFUSE_BASE = 0x3f41A000
     EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030  # BLOCK0 read base address
+    EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x044
+    EFUSE_BLOCK2_ADDR = EFUSE_BASE + 0x05C
 
     EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34
     EFUSE_PURPOSE_KEY0_SHIFT = 24
@@ -1706,21 +1900,50 @@ class ESP32S2ROM(ESP32ROM):
                   [0x40080000, 0x40800000, "IROM"],
                   [0x50000000, 0x50002000, "RTC_DATA"]]
 
+    # Returns old version format (ECO number). Use the new format get_chip_full_revision().
+    def get_chip_revision(self):
+        return self.get_major_chip_version()
+
     def get_pkg_version(self):
+        num_word = 4
+        return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 0) & 0x0F
+
+    def get_minor_chip_version(self):
+        hi_num_word = 3
+        hi = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * hi_num_word)) >> 20) & 0x01
+        low_num_word = 4
+        low = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * low_num_word)) >> 4) & 0x07
+        return (hi << 3) + low
+
+    def get_major_chip_version(self):
         num_word = 3
-        block1_addr = self.EFUSE_BASE + 0x044
-        word3 = self.read_reg(block1_addr + (4 * num_word))
-        pkg_version = (word3 >> 21) & 0x0F
-        return pkg_version
+        return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 18) & 0x03
+
+    def get_flash_version(self):
+        num_word = 3
+        return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x0F
+
+    def get_psram_version(self):
+        num_word = 3
+        return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 28) & 0x0F
+
+    def get_block2_version(self):
+        # BLK_VERSION_MINOR
+        num_word = 4
+        return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 4) & 0x07
 
     def get_chip_description(self):
         chip_name = {
             0: "ESP32-S2",
-            1: "ESP32-S2FH16",
-            2: "ESP32-S2FH32",
-        }.get(self.get_pkg_version(), "unknown ESP32-S2")
+            1: "ESP32-S2FH2",
+            2: "ESP32-S2FH4",
+            102: "ESP32-S2FNR2",
+            100: "ESP32-S2R2",
+        }.get(self.get_flash_version() + self.get_psram_version() * 100, "unknown ESP32-S2")
 
-        return "%s" % (chip_name)
+        major_rev = self.get_major_chip_version()
+        minor_rev = self.get_minor_chip_version()
+        return "%s (revision v%d.%d)" % (chip_name, major_rev, minor_rev)
 
     def get_chip_features(self):
         features = ["WiFi"]
@@ -1728,22 +1951,27 @@ class ESP32S2ROM(ESP32ROM):
         if self.secure_download_mode:
             features += ["Secure Download Mode Enabled"]
 
-        pkg_version = self.get_pkg_version()
+        flash_version = {
+            0: "No Embedded Flash",
+            1: "Embedded Flash 2MB",
+            2: "Embedded Flash 4MB",
+        }.get(self.get_flash_version(), "Unknown Embedded Flash")
+        features += [flash_version]
 
-        if pkg_version in [1, 2]:
-            if pkg_version == 1:
-                features += ["Embedded 2MB Flash"]
-            elif pkg_version == 2:
-                features += ["Embedded 4MB Flash"]
-            features += ["105C temp rating"]
+        psram_version = {
+            0: "No Embedded PSRAM",
+            1: "Embedded PSRAM 2MB",
+            2: "Embedded PSRAM 4MB",
+        }.get(self.get_psram_version(), "Unknown Embedded PSRAM")
+        features += [psram_version]
 
-        num_word = 4
-        block2_addr = self.EFUSE_BASE + 0x05C
-        word4 = self.read_reg(block2_addr + (4 * num_word))
-        block2_version = (word4 >> 4) & 0x07
+        block2_version = {
+            0: "No calibration in BLK2 of efuse",
+            1: "ADC and temperature sensor calibration in BLK2 of efuse V1",
+            2: "ADC and temperature sensor calibration in BLK2 of efuse V2",
+        }.get(self.get_block2_version(), "Unknown Calibration in BLK2")
+        features += [block2_version]
 
-        if block2_version == 1:
-            features += ["ADC and temperature sensor calibration in BLK2 of efuse"]
         return features
 
     def get_crystal_freq(self):
@@ -1810,16 +2038,17 @@ class ESP32S2ROM(ESP32ROM):
         strap_reg = self.read_reg(self.GPIO_STRAP_REG)
         force_dl_reg = self.read_reg(self.RTC_CNTL_OPTION1_REG)
         if strap_reg & self.GPIO_STRAP_SPI_BOOT_MASK == 0 and force_dl_reg & self.RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK == 0:
-            print("ERROR: {} chip was placed into download mode using GPIO0.\n"
+            print("WARNING: {} chip was placed into download mode using GPIO0.\n"
                   "esptool.py can not exit the download mode over USB. "
                   "To run the app, reset the chip manually.\n"
-                  "To suppress this error, set --after option to 'no_reset'.".format(self.get_chip_description()))
+                  "To suppress this note, set --after option to 'no_reset'.".format(self.get_chip_description()))
             raise SystemExit(1)
 
     def hard_reset(self):
         if self.uses_usb():
             self._check_if_can_reset()
 
+        print('Hard resetting via RTS pin...')
         self._setRTS(True)  # EN->LOW
         if self.uses_usb():
             # Give the chip some time to come out of reset, to be able to handle further DTR/RTS transitions
@@ -1827,12 +2056,21 @@ class ESP32S2ROM(ESP32ROM):
             self._setRTS(False)
             time.sleep(0.2)
         else:
+            time.sleep(0.1)
             self._setRTS(False)
 
 
 class ESP32S3ROM(ESP32ROM):
     CHIP_NAME = "ESP32-S3"
 
+    IMAGE_CHIP_ID = 9
+
+    CHIP_DETECT_MAGIC_VALUE = [0x9]
+
+    BOOTLOADER_FLASH_OFFSET = 0x0
+
+    FPGA_SLOW_BOOT = False
+
     IROM_MAP_START = 0x42000000
     IROM_MAP_END   = 0x44000000
     DROM_MAP_START = 0x3c000000
@@ -1851,9 +2089,10 @@ class ESP32S3ROM(ESP32ROM):
     FLASH_ENCRYPTED_WRITE_ALIGN = 16
 
     # todo: use espefuse APIs to get this info
-    EFUSE_BASE = 0x6001A000  # BLOCK0 read base address
+    EFUSE_BASE = 0x60007000  # BLOCK0 read base address
     MAC_EFUSE_REG = EFUSE_BASE + 0x044
-
+    EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x44
+    EFUSE_BLOCK2_ADDR = EFUSE_BASE + 0x5C
     EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030  # BLOCK0 read base address
 
     EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34
@@ -1876,9 +2115,17 @@ class ESP32S3ROM(ESP32ROM):
     PURPOSE_VAL_XTS_AES256_KEY_2 = 3
     PURPOSE_VAL_XTS_AES128_KEY = 4
 
-    UART_CLKDIV_REG = 0x60000014
+    UARTDEV_BUF_NO = 0x3fcef14c  # Variable in ROM .bss which indicates the port in use
+    UARTDEV_BUF_NO_USB = 3  # Value of the above variable indicating that USB is in use
+
+    USB_RAM_BLOCK = 0x800  # Max block size USB CDC is used
 
     GPIO_STRAP_REG = 0x60004038
+    GPIO_STRAP_SPI_BOOT_MASK = 0x8   # Not download mode
+    RTC_CNTL_OPTION1_REG = 0x6000812C
+    RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK = 0x1  # Is download mode forced over USB?
+
+    UART_CLKDIV_REG = 0x60000014
 
     MEMORY_MAP = [[0x00000000, 0x00010000, "PADDING"],
                   [0x3C000000, 0x3D000000, "DROM"],
@@ -1893,8 +2140,57 @@ class ESP32S3ROM(ESP32ROM):
                   [0x42000000, 0x42800000, "IROM"],
                   [0x50000000, 0x50002000, "RTC_DATA"]]
 
+    # Returns old version format (ECO number). Use the new format get_chip_full_revision().
+    def get_chip_revision(self):
+        return self.get_minor_chip_version()
+
+    def get_pkg_version(self):
+        num_word = 3
+        return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x07
+
+    def is_eco0(self, minor_raw):
+        # Workaround: The major version field was allocated to other purposes
+        # when block version is v1.1.
+        # Luckily only chip v0.0 have this kind of block version and efuse usage.
+        return (
+            (minor_raw & 0x7) == 0 and self.get_blk_version_major() == 1 and self.get_blk_version_minor() == 1
+        )
+
+    def get_minor_chip_version(self):
+        minor_raw = self.get_raw_minor_chip_version()
+        if self.is_eco0(minor_raw):
+            return 0
+        return minor_raw
+
+    def get_raw_minor_chip_version(self):
+        hi_num_word = 5
+        hi = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * hi_num_word)) >> 23) & 0x01
+        low_num_word = 3
+        low = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * low_num_word)) >> 18) & 0x07
+        return (hi << 3) + low
+
+    def get_blk_version_major(self):
+        num_word = 4
+        return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 0) & 0x03
+
+    def get_blk_version_minor(self):
+        num_word = 3
+        return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 24) & 0x07
+
+    def get_major_chip_version(self):
+        minor_raw = self.get_raw_minor_chip_version()
+        if self.is_eco0(minor_raw):
+            return 0
+        return self.get_raw_major_chip_version()
+
+    def get_raw_major_chip_version(self):
+        num_word = 5
+        return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 24) & 0x03
+
     def get_chip_description(self):
-        return "ESP32-S3"
+        major_rev = self.get_major_chip_version()
+        minor_rev = self.get_minor_chip_version()
+        return "%s (revision v%d.%d)" % (self.CHIP_NAME, major_rev, minor_rev)
 
     def get_chip_features(self):
         return ["WiFi", "BLE"]
@@ -1940,6 +2236,50 @@ class ESP32S3ROM(ESP32ROM):
         except TypeError:  # Python 3, bitstring elements are already bytes
             return tuple(bitstring)
 
+    def uses_usb(self, _cache=[]):
+        if self.secure_download_mode:
+            return False  # can't detect native USB in secure download mode
+        if not _cache:
+            buf_no = self.read_reg(self.UARTDEV_BUF_NO) & 0xff
+            _cache.append(buf_no == self.UARTDEV_BUF_NO_USB)
+        return _cache[0]
+
+    def _post_connect(self):
+        if self.uses_usb():
+            self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK
+
+    def _check_if_can_reset(self):
+        """
+        Check the strapping register to see if we can reset out of download mode.
+        """
+        if os.getenv("ESPTOOL_TESTING") is not None:
+            print("ESPTOOL_TESTING is set, ignoring strapping mode check")
+            # Esptool tests over USB CDC run with GPIO0 strapped low, don't complain in this case.
+            return
+        strap_reg = self.read_reg(self.GPIO_STRAP_REG)
+        force_dl_reg = self.read_reg(self.RTC_CNTL_OPTION1_REG)
+        if strap_reg & self.GPIO_STRAP_SPI_BOOT_MASK == 0 and force_dl_reg & self.RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK == 0:
+            print("WARNING: {} chip was placed into download mode using GPIO0.\n"
+                  "esptool.py can not exit the download mode over USB. "
+                  "To run the app, reset the chip manually.\n"
+                  "To suppress this note, set --after option to 'no_reset'.".format(self.get_chip_description()))
+            raise SystemExit(1)
+
+    def hard_reset(self):
+        if self.uses_usb():
+            self._check_if_can_reset()
+
+        print('Hard resetting via RTS pin...')
+        self._setRTS(True)  # EN->LOW
+        if self.uses_usb():
+            # Give the chip some time to come out of reset, to be able to handle further DTR/RTS transitions
+            time.sleep(0.2)
+            self._setRTS(False)
+            time.sleep(0.2)
+        else:
+            time.sleep(0.1)
+            self._setRTS(False)
+
 
 class ESP32S3BETA2ROM(ESP32S3ROM):
     CHIP_NAME = "ESP32-S3(beta2)"
@@ -1947,24 +2287,20 @@ class ESP32S3BETA2ROM(ESP32S3ROM):
 
     CHIP_DETECT_MAGIC_VALUE = [0xeb004136]
 
-    def get_chip_description(self):
-        return "ESP32-S3(beta2)"
-
-
-class ESP32S3BETA3ROM(ESP32S3ROM):
-    CHIP_NAME = "ESP32-S3(beta3)"
-    IMAGE_CHIP_ID = 6
-
-    CHIP_DETECT_MAGIC_VALUE = [0x9]
+    EFUSE_BASE = 0x6001A000  # BLOCK0 read base address
 
     def get_chip_description(self):
-        return "ESP32-S3(beta3)"
+        major_rev = self.get_major_chip_version()
+        minor_rev = self.get_minor_chip_version()
+        return "%s (revision v%d.%d)" % (self.CHIP_NAME, major_rev, minor_rev)
 
 
 class ESP32C3ROM(ESP32ROM):
     CHIP_NAME = "ESP32-C3"
     IMAGE_CHIP_ID = 5
 
+    FPGA_SLOW_BOOT = False
+
     IROM_MAP_START = 0x42000000
     IROM_MAP_END   = 0x42800000
     DROM_MAP_START = 0x3c000000
@@ -1986,6 +2322,7 @@ class ESP32C3ROM(ESP32ROM):
     UART_DATE_REG_ADDR = 0x60000000 + 0x7c
 
     EFUSE_BASE = 0x60008800
+    EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x044
     MAC_EFUSE_REG  = EFUSE_BASE + 0x044
 
     EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030  # BLOCK0 read base address
@@ -2024,27 +2361,32 @@ class ESP32C3ROM(ESP32ROM):
                   [0x50000000, 0x50002000, "RTC_DRAM"],
                   [0x600FE000, 0x60100000, "MEM_INTERNAL2"]]
 
+    # Returns old version format (ECO number). Use the new format get_chip_full_revision().
+    def get_chip_revision(self):
+        return self.get_minor_chip_version()
+
     def get_pkg_version(self):
         num_word = 3
-        block1_addr = self.EFUSE_BASE + 0x044
-        word3 = self.read_reg(block1_addr + (4 * num_word))
-        pkg_version = (word3 >> 21) & 0x0F
-        return pkg_version
+        return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x07
 
-    def get_chip_revision(self):
-        # reads WAFER_VERSION field from EFUSE_RD_MAC_SPI_SYS_3_REG
-        block1_addr = self.EFUSE_BASE + 0x044
-        num_word = 3
-        pos = 18
-        return (self.read_reg(block1_addr + (4 * num_word)) & (0x7 << pos)) >> pos
+    def get_minor_chip_version(self):
+        hi_num_word = 5
+        hi = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * hi_num_word)) >> 23) & 0x01
+        low_num_word = 3
+        low = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * low_num_word)) >> 18) & 0x07
+        return (hi << 3) + low
+
+    def get_major_chip_version(self):
+        num_word = 5
+        return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 24) & 0x03
 
     def get_chip_description(self):
         chip_name = {
             0: "ESP32-C3",
         }.get(self.get_pkg_version(), "unknown ESP32-C3")
-        chip_revision = self.get_chip_revision()
-
-        return "%s (revision %d)" % (chip_name, chip_revision)
+        major_rev = self.get_major_chip_version()
+        minor_rev = self.get_minor_chip_version()
+        return "%s (revision v%d.%d)" % (chip_name, major_rev, minor_rev)
 
     def get_chip_features(self):
         return ["Wi-Fi"]
@@ -2087,21 +2429,226 @@ class ESP32C3ROM(ESP32ROM):
         return any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes)
 
 
+class ESP32H2BETA1ROM(ESP32ROM):
+    CHIP_NAME = "ESP32-H2(beta1)"
+    IMAGE_CHIP_ID = 10
+
+    IROM_MAP_START = 0x42000000
+    IROM_MAP_END   = 0x42800000
+    DROM_MAP_START = 0x3c000000
+    DROM_MAP_END   = 0x3c800000
+
+    SPI_REG_BASE = 0x60002000
+    SPI_USR_OFFS    = 0x18
+    SPI_USR1_OFFS   = 0x1C
+    SPI_USR2_OFFS   = 0x20
+    SPI_MOSI_DLEN_OFFS = 0x24
+    SPI_MISO_DLEN_OFFS = 0x28
+    SPI_W0_OFFS = 0x58
+
+    BOOTLOADER_FLASH_OFFSET = 0x0
+
+    CHIP_DETECT_MAGIC_VALUE = [0xca26cc22]
+
+    UART_DATE_REG_ADDR = 0x60000000 + 0x7c
+
+    EFUSE_BASE = 0x6001A000
+    EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x044
+    MAC_EFUSE_REG  = EFUSE_BASE + 0x044
+
+    EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030  # BLOCK0 read base address
+
+    EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34
+    EFUSE_PURPOSE_KEY0_SHIFT = 24
+    EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34
+    EFUSE_PURPOSE_KEY1_SHIFT = 28
+    EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38
+    EFUSE_PURPOSE_KEY2_SHIFT = 0
+    EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38
+    EFUSE_PURPOSE_KEY3_SHIFT = 4
+    EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38
+    EFUSE_PURPOSE_KEY4_SHIFT = 8
+    EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38
+    EFUSE_PURPOSE_KEY5_SHIFT = 12
+
+    EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE
+    EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20
+
+    PURPOSE_VAL_XTS_AES128_KEY = 4
+
+    GPIO_STRAP_REG = 0x3f404038
+
+    FLASH_ENCRYPTED_WRITE_ALIGN = 16
+
+    MEMORY_MAP = []
+
+    FLASH_FREQUENCY = {
+        '48m': 0xf,
+        '24m': 0x0,
+        '16m': 0x1,
+        '12m': 0x2,
+    }
+
+    # Returns old version format (ECO number). Use the new format get_chip_full_revision().
+    def get_chip_revision(self):
+        return 0
+
+    def get_pkg_version(self):
+        num_word = 4
+        return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 0) & 0x07
+
+    def get_minor_chip_version(self):
+        num_word = 3
+        return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 18) & 0x07
+
+    def get_major_chip_version(self):
+        num_word = 3
+        return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x03
+
+    def get_chip_description(self):
+        chip_name = {
+            0: "ESP32-H2",
+        }.get(self.get_pkg_version(), "unknown ESP32-H2")
+        major_rev = self.get_major_chip_version()
+        minor_rev = self.get_minor_chip_version()
+        return "%s (revision v%d.%d)" % (chip_name, major_rev, minor_rev)
+
+    def get_chip_features(self):
+        return ["BLE/802.15.4"]
+
+    def get_crystal_freq(self):
+        return 32
+
+    def override_vddsdio(self, new_voltage):
+        raise NotImplementedInROMError("VDD_SDIO overrides are not supported for ESP32-H2")
+
+    def read_mac(self):
+        mac0 = self.read_reg(self.MAC_EFUSE_REG)
+        mac1 = self.read_reg(self.MAC_EFUSE_REG + 4)  # only bottom 16 bits are MAC
+        bitstring = struct.pack(">II", mac1, mac0)[2:]
+        try:
+            return tuple(ord(b) for b in bitstring)
+        except TypeError:  # Python 3, bitstring elements are already bytes
+            return tuple(bitstring)
+
+    def get_flash_crypt_config(self):
+        return None  # doesn't exist on ESP32-H2
+
+    def get_key_block_purpose(self, key_block):
+        if key_block < 0 or key_block > 5:
+            raise FatalError("Valid key block numbers must be in range 0-5")
+
+        reg, shift = [(self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT),
+                      (self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT),
+                      (self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT),
+                      (self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT),
+                      (self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT),
+                      (self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT)][key_block]
+        return (self.read_reg(reg) >> shift) & 0xF
+
+    def is_flash_encryption_key_valid(self):
+        # Need to see an AES-128 key
+        purposes = [self.get_key_block_purpose(b) for b in range(6)]
+
+        return any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes)
+
+
+class ESP32H2BETA2ROM(ESP32H2BETA1ROM):
+    CHIP_NAME = "ESP32-H2(beta2)"
+    IMAGE_CHIP_ID = 14
+
+    def get_chip_description(self):
+        major_rev = self.get_major_chip_version()
+        minor_rev = self.get_minor_chip_version()
+        return "%s (revision v%d.%d)" % (self.CHIP_NAME, major_rev, minor_rev)
+
+
+class ESP32C2ROM(ESP32C3ROM):
+    CHIP_NAME = "ESP32-C2"
+    IMAGE_CHIP_ID = 12
+
+    IROM_MAP_START = 0x42000000
+    IROM_MAP_END   = 0x42400000
+    DROM_MAP_START = 0x3c000000
+    DROM_MAP_END   = 0x3c400000
+
+    # Magic value for ESP32C2 ECO0 and ECO1 respectively
+    CHIP_DETECT_MAGIC_VALUE = [0x6F51306F, 0x7c41a06f]
+
+    EFUSE_BASE = 0x60008800
+    EFUSE_BLOCK2_ADDR = EFUSE_BASE + 0x040
+    MAC_EFUSE_REG  = EFUSE_BASE + 0x040
+
+    FLASH_FREQUENCY = {
+        '60m': 0xf,
+        '30m': 0x0,
+        '20m': 0x1,
+        '15m': 0x2,
+    }
+
+    # Returns old version format (ECO number). Use the new format get_chip_full_revision().
+    def get_chip_revision(self):
+        return self.get_major_chip_version()
+
+    def get_pkg_version(self):
+        num_word = 1
+        return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 22) & 0x07
+
+    def get_chip_description(self):
+        chip_name = {
+            0: "ESP32-C2",
+            1: "ESP32-C2",
+        }.get(self.get_pkg_version(), "unknown ESP32-C2")
+        major_rev = self.get_major_chip_version()
+        minor_rev = self.get_minor_chip_version()
+        return "%s (revision v%d.%d)" % (chip_name, major_rev, minor_rev)
+
+    def get_minor_chip_version(self):
+        num_word = 1
+        return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 16) & 0xF
+
+    def get_major_chip_version(self):
+        num_word = 1
+        return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 20) & 0x3
+
+    def _post_connect(self):
+        # ESP32C2 ECO0 is no longer supported by the flasher stub
+        if self.get_chip_revision() == 0:
+            self.stub_is_disabled = True
+            self.IS_STUB = False
+
+
 class ESP32C6BETAROM(ESP32C3ROM):
-    CHIP_NAME = "ESP32-C6 BETA"
+    CHIP_NAME = "ESP32-C6(beta)"
     IMAGE_CHIP_ID = 7
 
     CHIP_DETECT_MAGIC_VALUE = [0x0da1806f]
 
     UART_DATE_REG_ADDR = 0x00000500
 
+    # Returns old version format (ECO number). Use the new format get_chip_full_revision().
+    def get_chip_revision(self):
+        return 0
+
+    def get_pkg_version(self):
+        num_word = 3
+        return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 29) & 0x07
+
+    def get_minor_chip_version(self):
+        num_word = 3
+        return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 18) & 0x0F
+
+    def get_major_chip_version(self):
+        num_word = 3
+        return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 22) & 0x03
+
     def get_chip_description(self):
         chip_name = {
             0: "ESP32-C6",
         }.get(self.get_pkg_version(), "unknown ESP32-C6")
-        chip_revision = self.get_chip_revision()
-
-        return "%s (revision %d)" % (chip_name, chip_revision)
+        major_rev = self.get_major_chip_version()
+        minor_rev = self.get_minor_chip_version()
+        return "%s (revision v%d.%d)" % (chip_name, major_rev, minor_rev)
 
 
 class ESP32StubLoader(ESP32ROM):
@@ -2165,7 +2712,7 @@ class ESP32S3BETA2StubLoader(ESP32S3BETA2ROM):
 ESP32S3BETA2ROM.STUB_CLASS = ESP32S3BETA2StubLoader
 
 
-class ESP32S3BETA3StubLoader(ESP32S3BETA3ROM):
+class ESP32S3StubLoader(ESP32S3ROM):
     """ Access class for ESP32S3 stub loader, runs on top of ROM.
 
     (Basically the same as ESP32StubLoader, but different base class.
@@ -2181,8 +2728,12 @@ class ESP32S3BETA3StubLoader(ESP32S3BETA3ROM):
         self._trace_enabled = rom_loader._trace_enabled
         self.flush_input()  # resets _slip_reader
 
+        if rom_loader.uses_usb():
+            self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK
+            self.FLASH_WRITE_SIZE = self.USB_RAM_BLOCK
 
-ESP32S3BETA3ROM.STUB_CLASS = ESP32S3BETA3StubLoader
+
+ESP32S3ROM.STUB_CLASS = ESP32S3StubLoader
 
 
 class ESP32C3StubLoader(ESP32C3ROM):
@@ -2205,6 +2756,66 @@ class ESP32C3StubLoader(ESP32C3ROM):
 ESP32C3ROM.STUB_CLASS = ESP32C3StubLoader
 
 
+class ESP32H2BETA1StubLoader(ESP32H2BETA1ROM):
+    """ Access class for ESP32H2BETA1 stub loader, runs on top of ROM.
+
+    (Basically the same as ESP32StubLoader, but different base class.
+    Can possibly be made into a mixin.)
+    """
+    FLASH_WRITE_SIZE = 0x4000  # matches MAX_WRITE_BLOCK in stub_loader.c
+    STATUS_BYTES_LENGTH = 2  # same as ESP8266, different to ESP32 ROM
+    IS_STUB = True
+
+    def __init__(self, rom_loader):
+        self.secure_download_mode = rom_loader.secure_download_mode
+        self._port = rom_loader._port
+        self._trace_enabled = rom_loader._trace_enabled
+        self.flush_input()  # resets _slip_reader
+
+
+ESP32H2BETA1ROM.STUB_CLASS = ESP32H2BETA1StubLoader
+
+
+class ESP32H2BETA2StubLoader(ESP32H2BETA2ROM):
+    """ Access class for ESP32H2BETA2 stub loader, runs on top of ROM.
+
+    (Basically the same as ESP32StubLoader, but different base class.
+    Can possibly be made into a mixin.)
+    """
+    FLASH_WRITE_SIZE = 0x4000  # matches MAX_WRITE_BLOCK in stub_loader.c
+    STATUS_BYTES_LENGTH = 2  # same as ESP8266, different to ESP32 ROM
+    IS_STUB = True
+
+    def __init__(self, rom_loader):
+        self.secure_download_mode = rom_loader.secure_download_mode
+        self._port = rom_loader._port
+        self._trace_enabled = rom_loader._trace_enabled
+        self.flush_input()  # resets _slip_reader
+
+
+ESP32H2BETA2ROM.STUB_CLASS = ESP32H2BETA2StubLoader
+
+
+class ESP32C2StubLoader(ESP32C2ROM):
+    """ Access class for ESP32C2 stub loader, runs on top of ROM.
+
+    (Basically the same as ESP32StubLoader, but different base class.
+    Can possibly be made into a mixin.)
+    """
+    FLASH_WRITE_SIZE = 0x4000  # matches MAX_WRITE_BLOCK in stub_loader.c
+    STATUS_BYTES_LENGTH = 2  # same as ESP8266, different to ESP32 ROM
+    IS_STUB = True
+
+    def __init__(self, rom_loader):
+        self.secure_download_mode = rom_loader.secure_download_mode
+        self._port = rom_loader._port
+        self._trace_enabled = rom_loader._trace_enabled
+        self.flush_input()  # resets _slip_reader
+
+
+ESP32C2ROM.STUB_CLASS = ESP32C2StubLoader
+
+
 class ESPBOOTLOADER(object):
     """ These are constants related to software ESP8266 bootloader, working with 'v2' image files """
 
@@ -2223,7 +2834,7 @@ def LoadFirmwareImage(chip, filename):
 
         Returns a BaseFirmwareImage subclass, either ESP8266ROMFirmwareImage (v1) or ESP8266V2FirmwareImage (v2).
     """
-    chip = chip.lower().replace("-", "")
+    chip = re.sub(r"[-()]", "", chip.lower())
     with open(filename, 'rb') as f:
         if chip == 'esp32':
             return ESP32FirmwareImage(f)
@@ -2231,12 +2842,18 @@ def LoadFirmwareImage(chip, filename):
             return ESP32S2FirmwareImage(f)
         elif chip == "esp32s3beta2":
             return ESP32S3BETA2FirmwareImage(f)
-        elif chip == "esp32s3beta3":
-            return ESP32S3BETA3FirmwareImage(f)
+        elif chip == "esp32s3":
+            return ESP32S3FirmwareImage(f)
         elif chip == 'esp32c3':
             return ESP32C3FirmwareImage(f)
         elif chip == 'esp32c6beta':
             return ESP32C6BETAFirmwareImage(f)
+        elif chip == 'esp32h2beta1':
+            return ESP32H2BETA1FirmwareImage(f)
+        elif chip == 'esp32h2beta2':
+            return ESP32H2BETA2FirmwareImage(f)
+        elif chip == 'esp32c2':
+            return ESP32C2FirmwareImage(f)
         else:  # Otherwise, ESP8266 so look at magic to determine the image type
             magic = ord(f.read(1))
             f.seek(0)
@@ -2314,6 +2931,7 @@ class BaseFirmwareImage(object):
         self.entrypoint = 0
         self.elf_sha256 = None
         self.elf_sha256_offset = 0
+        self.pad_to_size = 0
 
     def load_common_header(self, load_file, expected_magic):
         (magic, segments, self.flash_mode, self.flash_size_freq, self.entrypoint) = struct.unpack('<BBBBI', load_file.read(8))
@@ -2361,7 +2979,7 @@ class BaseFirmwareImage(object):
             if segment_data[patch_offset:patch_offset + self.SHA256_DIGEST_LEN] != b'\x00' * self.SHA256_DIGEST_LEN:
                 raise FatalError('Contents of segment at SHA256 digest offset 0x%x are not all zero. Refusing to overwrite.' %
                                  self.elf_sha256_offset)
-            assert(len(self.elf_sha256) == self.SHA256_DIGEST_LEN)
+            assert len(self.elf_sha256) == self.SHA256_DIGEST_LEN
             segment_data = segment_data[0:patch_offset] + self.elf_sha256 + \
                 segment_data[patch_offset + self.SHA256_DIGEST_LEN:]
         return segment_data
@@ -2453,6 +3071,10 @@ class BaseFirmwareImage(object):
         # script will have produced any adjacent sections in linear order in the ELF, anyhow.
         self.segments = segments
 
+    def set_mmu_page_size(self, size):
+        """ If supported, this should be overridden by the chip-specific class. Gets called in elf2image. """
+        print('WARNING: Changing MMU page size is not supported on {}! Defaulting to 64KB.'.format(self.ROM_LOADER.CHIP_NAME))
+
 
 class ESP8266ROMFirmwareImage(BaseFirmwareImage):
     """ 'Version 1' firmware image, segments loaded directly by the ROM bootloader. """
@@ -2586,11 +3208,6 @@ class ESP8266V2FirmwareImage(BaseFirmwareImage):
             f.write(struct.pack(b'<I', crc))
 
 
-# Backwards compatibility for previous API, remove in esptool.py V3
-ESPFirmwareImage = ESP8266ROMFirmwareImage
-OTAFirmwareImage = ESP8266V2FirmwareImage
-
-
 def esp8266_crc32(data):
     """
     CRC32 algorithm used by 8266 SDK bootloader (and gen_appbin.py).
@@ -2617,7 +3234,7 @@ class ESP32FirmwareImage(BaseFirmwareImage):
     # to be set to this value so ROM bootloader will skip it.
     WP_PIN_DISABLED = 0xEE
 
-    EXTENDED_HEADER_STRUCT_FMT = "<BBBBHB" + ("B" * 8) + "B"
+    EXTENDED_HEADER_STRUCT_FMT = "<BBBBHBHH" + ("B" * 4) + "B"
 
     IROM_ALIGN = 65536
 
@@ -2636,6 +3253,8 @@ class ESP32FirmwareImage(BaseFirmwareImage):
         self.hd_drv = 0
         self.wp_drv = 0
         self.min_rev = 0
+        self.min_rev_full = 0
+        self.max_rev_full = 0
 
         self.append_digest = True
 
@@ -2782,6 +3401,12 @@ class ESP32FirmwareImage(BaseFirmwareImage):
                 digest.update(f.read(image_length))
                 f.write(digest.digest())
 
+            if self.pad_to_size:
+                image_length = f.tell()
+                if image_length % self.pad_to_size != 0:
+                    pad_by = self.pad_to_size - (image_length % self.pad_to_size)
+                    f.write(b"\xff" * pad_by)
+
             with open(filename, 'wb') as real_file:
                 real_file.write(f.getvalue())
 
@@ -2813,8 +3438,12 @@ class ESP32FirmwareImage(BaseFirmwareImage):
             print(("Unexpected chip id in image. Expected %d but value was %d. "
                    "Is this image for a different chip model?") % (self.ROM_LOADER.IMAGE_CHIP_ID, chip_id))
 
+        self.min_rev = fields[5]
+        self.min_rev_full = fields[6]
+        self.max_rev_full = fields[7]
+
         # reserved fields in the middle should all be zero
-        if any(f for f in fields[6:-1] if f != 0):
+        if any(f for f in fields[8:-1] if f != 0):
             print("Warning: some reserved header fields have non-zero values. This image may be from a newer esptool.py?")
 
         append_digest = fields[-1]  # last byte is append_digest
@@ -2834,14 +3463,110 @@ class ESP32FirmwareImage(BaseFirmwareImage):
                   join_byte(self.d_drv, self.cs_drv),
                   join_byte(self.hd_drv, self.wp_drv),
                   self.ROM_LOADER.IMAGE_CHIP_ID,
-                  self.min_rev]
-        fields += [0] * 8  # padding
+                  self.min_rev,
+                  self.min_rev_full,
+                  self.max_rev_full]
+        fields += [0] * 4  # padding
         fields += [append_digest]
 
         packed = struct.pack(self.EXTENDED_HEADER_STRUCT_FMT, *fields)
         save_file.write(packed)
 
 
+class ESP8266V3FirmwareImage(ESP32FirmwareImage):
+    """ ESP8266 V3 firmware image is very similar to ESP32 image
+    """
+
+    EXTENDED_HEADER_STRUCT_FMT = "B" * 16
+
+    def is_flash_addr(self, addr):
+        return (addr > ESP8266ROM.IROM_MAP_START)
+
+    def save(self, filename):
+        total_segments = 0
+        with io.BytesIO() as f:  # write file to memory first
+            self.write_common_header(f, self.segments)
+
+            checksum = ESPLoader.ESP_CHECKSUM_MAGIC
+
+            # split segments into flash-mapped vs ram-loaded, and take copies so we can mutate them
+            flash_segments = [copy.deepcopy(s) for s in sorted(self.segments, key=lambda s:s.addr) if self.is_flash_addr(s.addr) and len(s.data)]
+            ram_segments = [copy.deepcopy(s) for s in sorted(self.segments, key=lambda s:s.addr) if not self.is_flash_addr(s.addr) and len(s.data)]
+
+            # check for multiple ELF sections that are mapped in the same flash mapping region.
+            # this is usually a sign of a broken linker script, but if you have a legitimate
+            # use case then let us know
+            if len(flash_segments) > 0:
+                last_addr = flash_segments[0].addr
+                for segment in flash_segments[1:]:
+                    if segment.addr // self.IROM_ALIGN == last_addr // self.IROM_ALIGN:
+                        raise FatalError(("Segment loaded at 0x%08x lands in same 64KB flash mapping as segment loaded at 0x%08x. "
+                                          "Can't generate binary. Suggest changing linker script or ELF to merge sections.") %
+                                         (segment.addr, last_addr))
+                    last_addr = segment.addr
+
+            # try to fit each flash segment on a 64kB aligned boundary
+            # by padding with parts of the non-flash segments...
+            while len(flash_segments) > 0:
+                segment = flash_segments[0]
+                # remove 8 bytes empty data for insert segment header
+                if segment.name == '.flash.rodata':
+                    segment.data = segment.data[8:]
+                # write the flash segment
+                checksum = self.save_segment(f, segment, checksum)
+                flash_segments.pop(0)
+                total_segments += 1
+
+            # flash segments all written, so write any remaining RAM segments
+            for segment in ram_segments:
+                checksum = self.save_segment(f, segment, checksum)
+                total_segments += 1
+
+            # done writing segments
+            self.append_checksum(f, checksum)
+            image_length = f.tell()
+
+            # kinda hacky: go back to the initial header and write the new segment count
+            # that includes padding segments. This header is not checksummed
+            f.seek(1)
+            try:
+                f.write(chr(total_segments))
+            except TypeError:  # Python 3
+                f.write(bytes([total_segments]))
+
+            if self.append_digest:
+                # calculate the SHA256 of the whole file and append it
+                f.seek(0)
+                digest = hashlib.sha256()
+                digest.update(f.read(image_length))
+                f.write(digest.digest())
+
+            with open(filename, 'wb') as real_file:
+                real_file.write(f.getvalue())
+
+    def load_extended_header(self, load_file):
+        def split_byte(n):
+            return (n & 0x0F, (n >> 4) & 0x0F)
+
+        fields = list(struct.unpack(self.EXTENDED_HEADER_STRUCT_FMT, load_file.read(16)))
+
+        self.wp_pin = fields[0]
+
+        # SPI pin drive stengths are two per byte
+        self.clk_drv, self.q_drv = split_byte(fields[1])
+        self.d_drv, self.cs_drv = split_byte(fields[2])
+        self.hd_drv, self.wp_drv = split_byte(fields[3])
+
+        if fields[15] in [0, 1]:
+            self.append_digest = (fields[15] == 1)
+        else:
+            raise RuntimeError("Invalid value for append_digest field (0x%02x). Should be 0 or 1.", fields[15])
+
+        # remaining fields in the middle should all be zero
+        if any(f for f in fields[4:15] if f != 0):
+            print("Warning: some reserved header fields have non-zero values. This image may be from a newer esptool.py?")
+
+
 ESP32ROM.BOOTLOADER_IMAGE = ESP32FirmwareImage
 
 
@@ -2861,12 +3586,12 @@ class ESP32S3BETA2FirmwareImage(ESP32FirmwareImage):
 ESP32S3BETA2ROM.BOOTLOADER_IMAGE = ESP32S3BETA2FirmwareImage
 
 
-class ESP32S3BETA3FirmwareImage(ESP32FirmwareImage):
+class ESP32S3FirmwareImage(ESP32FirmwareImage):
     """ ESP32S3 Firmware Image almost exactly the same as ESP32FirmwareImage """
-    ROM_LOADER = ESP32S3BETA3ROM
+    ROM_LOADER = ESP32S3ROM
 
 
-ESP32S3BETA3ROM.BOOTLOADER_IMAGE = ESP32S3BETA3FirmwareImage
+ESP32S3ROM.BOOTLOADER_IMAGE = ESP32S3FirmwareImage
 
 
 class ESP32C3FirmwareImage(ESP32FirmwareImage):
@@ -2885,9 +3610,42 @@ class ESP32C6BETAFirmwareImage(ESP32FirmwareImage):
 ESP32C6BETAROM.BOOTLOADER_IMAGE = ESP32C6BETAFirmwareImage
 
 
+class ESP32H2BETA1FirmwareImage(ESP32FirmwareImage):
+    """ ESP32H2 Firmware Image almost exactly the same as ESP32FirmwareImage """
+    ROM_LOADER = ESP32H2BETA1ROM
+
+
+ESP32H2BETA1ROM.BOOTLOADER_IMAGE = ESP32H2BETA1FirmwareImage
+
+
+class ESP32H2BETA2FirmwareImage(ESP32FirmwareImage):
+    """ ESP32H2 Firmware Image almost exactly the same as ESP32FirmwareImage """
+    ROM_LOADER = ESP32H2BETA2ROM
+
+
+ESP32H2BETA2ROM.BOOTLOADER_IMAGE = ESP32H2BETA2FirmwareImage
+
+
+class ESP32C2FirmwareImage(ESP32FirmwareImage):
+    """ ESP32C2 Firmware Image almost exactly the same as ESP32FirmwareImage """
+    ROM_LOADER = ESP32C2ROM
+
+    def set_mmu_page_size(self, size):
+        if size not in [16384, 32768, 65536]:
+            raise FatalError("{} is not a valid page size.".format(size))
+        self.IROM_ALIGN = size
+
+
+ESP32C2ROM.BOOTLOADER_IMAGE = ESP32C2FirmwareImage
+
+
 class ELFFile(object):
     SEC_TYPE_PROGBITS = 0x01
     SEC_TYPE_STRTAB = 0x03
+    SEC_TYPE_INITARRAY = 0x0e
+    SEC_TYPE_FINIARRAY = 0x0f
+
+    PROG_SEC_TYPES = (SEC_TYPE_PROGBITS, SEC_TYPE_INITARRAY, SEC_TYPE_FINIARRAY)
 
     LEN_SEC_HEADER = 0x28
 
@@ -2944,7 +3702,7 @@ class ELFFile(object):
             name_offs, sec_type, _flags, lma, sec_offs, size = struct.unpack_from("<LLLLLL", section_header[offs:])
             return (name_offs, sec_type, lma, size, sec_offs)
         all_sections = [read_section_header(offs) for offs in section_header_offsets]
-        prog_sections = [s for s in all_sections if s[1] == ELFFile.SEC_TYPE_PROGBITS]
+        prog_sections = [s for s in all_sections if s[1] in ELFFile.PROG_SEC_TYPES]
 
         # search for the string table section
         if not (shstrndx * self.LEN_SEC_HEADER) in section_header_offsets:
@@ -3012,13 +3770,17 @@ def slip_reader(port, trace_function):
     """
     partial_packet = None
     in_escape = False
+    successful_slip = False
     while True:
         waiting = port.inWaiting()
         read_bytes = port.read(1 if waiting == 0 else waiting)
         if read_bytes == b'':
-            waiting_for = "header" if partial_packet is None else "content"
-            trace_function("Timed out waiting for packet %s", waiting_for)
-            raise FatalError("Timed out waiting for packet %s" % waiting_for)
+            if partial_packet is None:  # fail due to no data
+                msg = "Serial data stream stopped: Possible serial noise or corruption." if successful_slip else "No serial data received."
+            else:  # fail during packet transfer
+                msg = "Packet content transfer stopped (received {} bytes)".format(len(partial_packet))
+            trace_function(msg)
+            raise FatalError(msg)
         trace_function("Read %d bytes: %s", len(read_bytes), HexFormatter(read_bytes))
         for b in read_bytes:
             if type(b) is int:
@@ -3030,7 +3792,7 @@ def slip_reader(port, trace_function):
                 else:
                     trace_function("Read invalid data: %s", HexFormatter(read_bytes))
                     trace_function("Remaining data in serial buffer: %s", HexFormatter(port.read(port.inWaiting())))
-                    raise FatalError('Invalid head of packet (0x%s)' % hexify(b))
+                    raise FatalError('Invalid head of packet (0x%s): Possible serial noise or corruption.' % hexify(b))
             elif in_escape:  # part-way through escape sequence
                 in_escape = False
                 if b == b'\xdc':
@@ -3047,6 +3809,7 @@ def slip_reader(port, trace_function):
                 trace_function("Received full packet: %s", HexFormatter(partial_packet))
                 yield partial_packet
                 partial_packet = None
+                successful_slip = True
             else:  # normal byte in packet
                 partial_packet += b
 
@@ -3055,6 +3818,15 @@ def arg_auto_int(x):
     return int(x, 0)
 
 
+def format_chip_name(c):
+    """ Normalize chip name from user input """
+    c = c.lower().replace('-', '')
+    if c == 'esp8684':  # TODO: Delete alias, ESPTOOL-389
+        print('WARNING: Chip name ESP8684 is deprecated in favor of ESP32-C2 and will be removed in a future release. Using ESP32-C2 instead.')
+        return 'esp32c2'
+    return c
+
+
 def div_roundup(a, b):
     """ Return a/b rounded up to nearest integer,
     equivalent result to int(math.ceil(float(int(a)) / float(int(b))), only
@@ -3135,7 +3907,7 @@ def pad_to(data, alignment, pad_character=b'\xFF'):
 class FatalError(RuntimeError):
     """
     Wrapper class for runtime errors that aren't caused by internal bugs, but by
-    ESP8266 responses or input content.
+    ESP ROM responses or input content.
     """
     def __init__(self, message):
         RuntimeError.__init__(self, message)
@@ -3144,9 +3916,37 @@ class FatalError(RuntimeError):
     def WithResult(message, result):
         """
         Return a fatal error object that appends the hex values of
-        'result' as a string formatted argument.
+        'result' and its meaning as a string formatted argument.
         """
-        message += " (result was %s)" % hexify(result)
+
+        err_defs = {
+            0x101: 'Out of memory',
+            0x102: 'Invalid argument',
+            0x103: 'Invalid state',
+            0x104: 'Invalid size',
+            0x105: 'Requested resource not found',
+            0x106: 'Operation or feature not supported',
+            0x107: 'Operation timed out',
+            0x108: 'Received response was invalid',
+            0x109: 'CRC or checksum was invalid',
+            0x10A: 'Version was invalid',
+            0x10B: 'MAC address was invalid',
+            # Flasher stub error codes
+            0xC000: 'Bad data length',
+            0xC100: 'Bad data checksum',
+            0xC200: 'Bad blocksize',
+            0xC300: 'Invalid command',
+            0xC400: 'Failed SPI operation',
+            0xC500: 'Failed SPI unlock',
+            0xC600: 'Not in flash mode',
+            0xC700: 'Inflate error',
+            0xC800: 'Not enough data',
+            0xC900: 'Too much data',
+            0xFF00: 'Command not implemented',
+        }
+
+        err_code = struct.unpack(">H", result[:2])
+        message += " (result was {}: {})".format(hexify(result), err_defs.get(err_code[0], 'Unknown result'))
         return FatalError(message)
 
 
@@ -3273,7 +4073,7 @@ def _update_image_flash_params(esp, address, args, image):
 
     flash_freq = flash_size_freq & 0x0F
     if args.flash_freq != 'keep':
-        flash_freq = {'40m': 0, '26m': 1, '20m': 2, '80m': 0xf}[args.flash_freq]
+        flash_freq = esp.parse_flash_freq_arg(args.flash_freq)
 
     flash_size = flash_size_freq & 0xF0
     if args.flash_size != 'keep':
@@ -3333,7 +4133,7 @@ def write_flash(esp, args):
             argfile.seek(0, os.SEEK_END)
             if address + argfile.tell() > flash_end:
                 raise FatalError(("File %s (length %d) at offset %d will not fit in %d bytes of flash. "
-                                  "Use --flash-size argument, or change flashing address.")
+                                  "Use --flash_size argument, or change flashing address.")
                                  % (argfile.name, argfile.tell(), address, flash_end))
             argfile.seek(0)
 
@@ -3497,8 +4297,20 @@ def write_flash(esp, args):
 
 
 def image_info(args):
+    if args.chip == "auto":
+        print("WARNING: --chip not specified, defaulting to ESP8266.")
     image = LoadFirmwareImage(args.chip, args.filename)
     print('Image version: %d' % image.version)
+    if args.chip != 'auto' and args.chip != 'esp8266':
+        print(
+            "Minimal chip revision:",
+            "v{}.{},".format(image.min_rev_full // 100, image.min_rev_full % 100),
+            "(legacy min_rev = {})".format(image.min_rev)
+        )
+        print(
+            "Maximal chip revision:",
+            "v{}.{}".format(image.max_rev_full // 100, image.max_rev_full % 100),
+        )
     print('Entry point: %08x' % image.entrypoint if image.entrypoint != 0 else 'Entry point not set')
     print('%d segments' % len(image.segments))
     print()
@@ -3539,9 +4351,10 @@ def make_image(args):
 def elf2image(args):
     e = ELFFile(args.input)
     if args.chip == 'auto':  # Default to ESP8266 for backwards compatibility
-        print("Creating image for ESP8266...")
         args.chip = 'esp8266'
 
+    print("Creating {} image...".format(args.chip))
+
     if args.chip == 'esp32':
         image = ESP32FirmwareImage()
         if args.secure_pad:
@@ -3556,8 +4369,8 @@ def elf2image(args):
         image = ESP32S3BETA2FirmwareImage()
         if args.secure_pad_v2:
             image.secure_pad = '2'
-    elif args.chip == 'esp32s3beta3':
-        image = ESP32S3BETA3FirmwareImage()
+    elif args.chip == 'esp32s3':
+        image = ESP32S3FirmwareImage()
         if args.secure_pad_v2:
             image.secure_pad = '2'
     elif args.chip == 'esp32c3':
@@ -3568,21 +4381,43 @@ def elf2image(args):
         image = ESP32C6BETAFirmwareImage()
         if args.secure_pad_v2:
             image.secure_pad = '2'
+    elif args.chip == 'esp32h2beta1':
+        image = ESP32H2BETA1FirmwareImage()
+        if args.secure_pad_v2:
+            image.secure_pad = '2'
+    elif args.chip == 'esp32h2beta2':
+        image = ESP32H2BETA2FirmwareImage()
+        if args.secure_pad_v2:
+            image.secure_pad = '2'
+    elif args.chip == 'esp32c2':
+        image = ESP32C2FirmwareImage()
+        if args.secure_pad_v2:
+            image.secure_pad = '2'
     elif args.version == '1':  # ESP8266
         image = ESP8266ROMFirmwareImage()
-    else:
+    elif args.version == '2':
         image = ESP8266V2FirmwareImage()
+    else:
+        image = ESP8266V3FirmwareImage()
     image.entrypoint = e.entrypoint
     image.flash_mode = {'qio': 0, 'qout': 1, 'dio': 2, 'dout': 3}[args.flash_mode]
 
     if args.chip != 'esp8266':
-        image.min_rev = int(args.min_rev)
+        image.min_rev = args.min_rev
+        image.min_rev_full = args.min_rev_full
+        image.max_rev_full = args.max_rev_full
+
+    if args.flash_mmu_page_size:
+        image.set_mmu_page_size(flash_size_bytes(args.flash_mmu_page_size))
 
     # ELFSection is a subclass of ImageSegment, so can use interchangeably
     image.segments = e.segments if args.use_segments else e.sections
 
-    image.flash_size_freq = image.ROM_LOADER.FLASH_SIZES[args.flash_size]
-    image.flash_size_freq += {'40m': 0, '26m': 1, '20m': 2, '80m': 0xf}[args.flash_freq]
+    if args.pad_to_size:
+        image.pad_to_size = flash_size_bytes(args.pad_to_size)
+
+    image.flash_size_freq = image.ROM_LOADER.parse_flash_size_arg(args.flash_size)
+    image.flash_size_freq += image.ROM_LOADER.parse_flash_freq_arg(args.flash_freq)
 
     if args.elf_sha256_offset:
         image.elf_sha256 = e.sha256()
@@ -3600,6 +4435,8 @@ def elf2image(args):
         args.output = image.default_output_name(args.input)
     image.save(args.output)
 
+    print("Successfully created {} image.".format(args.chip))
+
 
 def read_mac(esp, args):
     mac = esp.read_mac()
@@ -3716,15 +4553,23 @@ def write_flash_status(esp, args):
 
 
 def get_security_info(esp, args):
-    (flags, flash_crypt_cnt, key_purposes) = esp.get_security_info()
-    # TODO: better display
-    print('Flags: 0x%08x (%s)' % (flags, bin(flags)))
-    print('Flash_Crypt_Cnt: 0x%x' % flash_crypt_cnt)
-    print('Key_Purposes: %s' % (key_purposes,))
+    si = esp.get_security_info()
+    # TODO: better display and tests
+    print('Flags: {:#010x} ({})'.format(si["flags"], bin(si["flags"])))
+    print('Flash_Crypt_Cnt: {:#x}'.format(si["flash_crypt_cnt"]))
+    print('Key_Purposes: {}'.format(si["key_purposes"]))
+    if si["chip_id"] is not None and si["api_version"] is not None:
+        print('Chip_ID: {}'.format(si["chip_id"]))
+        print('Api_Version: {}'.format(si["api_version"]))
 
 
 def merge_bin(args):
-    chip_class = _chip_to_rom_loader(args.chip)
+    try:
+        chip_class = _chip_to_rom_loader(args.chip)
+    except KeyError:
+        msg = "Please specify the chip argument" if args.chip == "auto" else "Invalid chip choice: '{}'".format(args.chip)
+        msg = msg + " (choose from {})".format(', '.join(SUPPORTED_CHIPS))
+        raise FatalError(msg)
 
     # sort the files by offset. The AddrFilenamePairAction has already checked for overlap
     input_files = sorted(args.addr_filename, key=lambda x: x[0])
@@ -3772,12 +4617,12 @@ def main(argv=None, esp=None):
 
     external_esp = esp is not None
 
-    parser = argparse.ArgumentParser(description='esptool.py v%s - ESP8266 ROM Bootloader Utility' % __version__, prog='esptool')
+    parser = argparse.ArgumentParser(description='esptool.py v%s - Espressif chips ROM Bootloader Utility' % __version__, prog='esptool')
 
     parser.add_argument('--chip', '-c',
                         help='Target chip type',
-                        type=lambda c: c.lower().replace('-', ''),  # support ESP32-S2, etc.
-                        choices=['auto', 'esp8266', 'esp32', 'esp32s2', 'esp32s3beta2', 'esp32s3beta3', 'esp32c3', 'esp32c6beta'],
+                        type=format_chip_name,  # support ESP32-S2, etc.
+                        choices=['auto'] + SUPPORTED_CHIPS,
                         default=os.environ.get('ESPTOOL_CHIP', 'auto'))
 
     parser.add_argument(
@@ -3873,12 +4718,12 @@ def main(argv=None, esp=None):
             extra_fs_message = ""
 
         parent.add_argument('--flash_freq', '-ff', help='SPI Flash frequency',
-                            choices=extra_keep_args + ['40m', '26m', '20m', '80m'],
+                            choices=extra_keep_args + ['80m', '60m', '48m', '40m', '30m', '26m', '24m', '20m', '16m', '15m', '12m'],
                             default=os.environ.get('ESPTOOL_FF', 'keep' if allow_keep else '40m'))
         parent.add_argument('--flash_mode', '-fm', help='SPI Flash mode',
                             choices=extra_keep_args + ['qio', 'qout', 'dio', 'dout'],
                             default=os.environ.get('ESPTOOL_FM', 'keep' if allow_keep else 'qio'))
-        parent.add_argument('--flash_size', '-fs', help='SPI Flash size in MegaBytes (1MB, 2MB, 4MB, 8MB, 16M)'
+        parent.add_argument('--flash_size', '-fs', help='SPI Flash size in MegaBytes (1MB, 2MB, 4MB, 8MB, 16MB, 32MB, 64MB, 128MB)'
                             ' plus ESP8266-only (256KB, 512KB, 2MB-c1, 4MB-c1)' + extra_fs_message,
                             action=FlashSizeAction, auto_detect=auto_detect,
                             default=os.environ.get('ESPTOOL_FS', 'keep' if allow_keep else '1MB'))
@@ -3935,8 +4780,36 @@ def main(argv=None, esp=None):
         help='Create an application image from ELF file')
     parser_elf2image.add_argument('input', help='Input ELF file')
     parser_elf2image.add_argument('--output', '-o', help='Output filename prefix (for version 1 image), or filename (for version 2 single image)', type=str)
-    parser_elf2image.add_argument('--version', '-e', help='Output image version', choices=['1', '2'], default='1')
-    parser_elf2image.add_argument('--min-rev', '-r', help='Minimum chip revision', choices=['0', '1', '2', '3'], default='0')
+    parser_elf2image.add_argument('--version', '-e', help='Output image version', choices=['1', '2', '3'], default='1')
+    parser_elf2image.add_argument(
+        # kept for compatibility
+        # Minimum chip revision (deprecated, consider using --min-rev-full)
+        "--min-rev",
+        "-r",
+        # In v3 we do not do help=argparse.SUPPRESS because
+        # it should remain visible.
+        help="Minimal chip revision (ECO version format)",
+        type=int,
+        choices=range(256),
+        metavar="{0, ... 255}",
+        default=0,
+    )
+    parser_elf2image.add_argument(
+        "--min-rev-full",
+        help="Minimal chip revision (in format: major * 100 + minor)",
+        type=int,
+        choices=range(65536),
+        metavar="{0, ... 65535}",
+        default=0,
+    )
+    parser_elf2image.add_argument(
+        "--max-rev-full",
+        help="Maximal chip revision (in format: major * 100 + minor)",
+        type=int,
+        choices=range(65536),
+        metavar="{0, ... 65535}",
+        default=65535,
+    )
     parser_elf2image.add_argument('--secure-pad', action='store_true',
                                   help='Pad image so once signed it will end on a 64KB boundary. For Secure Boot v1 images only.')
     parser_elf2image.add_argument('--secure-pad-v2', action='store_true',
@@ -3946,7 +4819,12 @@ def main(argv=None, esp=None):
                                   type=arg_auto_int, default=None)
     parser_elf2image.add_argument('--use_segments', help='If set, ELF segments will be used instead of ELF sections to genereate the image.',
                                   action='store_true')
-
+    parser_elf2image.add_argument('--flash-mmu-page-size', help="Change flash MMU page size.", choices=['64KB', '32KB', '16KB'])
+    parser_elf2image.add_argument(
+        "--pad-to-size",
+        help="The block size with which the final binary image after padding must be aligned to. Value 0xFF is used for padding, similar to erase_flash",
+        default=None,
+    )
     add_spi_flash_subparsers(parser_elf2image, allow_keep=False, auto_detect=False)
 
     subparsers.add_parser(
@@ -4024,11 +4902,10 @@ def main(argv=None, esp=None):
                                   help='Address followed by binary filename, separated by space',
                                   action=AddrFilenamePairAction)
 
-    subparsers.add_parser(
-        'version', help='Print esptool version')
-
     subparsers.add_parser('get_security_info', help='Get some security-related data')
 
+    subparsers.add_parser('version', help='Print esptool version')
+
     # internal sanity check - every operation matches a module function of the same name
     for operation in subparsers.choices.keys():
         assert operation in globals(), "%s should be a module function" % operation
@@ -4074,6 +4951,7 @@ def main(argv=None, esp=None):
         esp = esp or get_default_connected_device(ser_list, port=args.port, connect_attempts=args.connect_attempts,
                                                   initial_baud=initial_baud, chip=args.chip, trace=args.trace,
                                                   before=args.before)
+
         if esp is None:
             raise FatalError("Could not connect to an Espressif device on any of the %d available serial ports." % len(ser_list))
 
@@ -4089,6 +4967,9 @@ def main(argv=None, esp=None):
             if esp.secure_download_mode:
                 print("WARNING: Stub loader is not supported in Secure Download Mode, setting --no-stub")
                 args.no_stub = True
+            elif not esp.IS_STUB and esp.stub_is_disabled:
+                print("WARNING: Stub loader has been disabled for compatibility, setting --no-stub")
+                args.no_stub = True
             else:
                 esp = esp.run_stub()
 
@@ -4112,11 +4993,84 @@ def main(argv=None, esp=None):
             # ROM loader doesn't enable flash unless we explicitly do it
             esp.flash_spi_attach(0)
 
+        # XMC chip startup sequence
+        XMC_VENDOR_ID = 0x20
+
+        def is_xmc_chip_strict():
+            id = esp.flash_id()
+            rdid = ((id & 0xff) << 16) | ((id >> 16) & 0xff) | (id & 0xff00)
+
+            vendor_id = ((rdid >> 16) & 0xFF)
+            mfid = ((rdid >> 8) & 0xFF)
+            cpid = (rdid & 0xFF)
+
+            if vendor_id != XMC_VENDOR_ID:
+                return False
+
+            matched = False
+            if mfid == 0x40:
+                if cpid >= 0x13 and cpid <= 0x20:
+                    matched = True
+            elif mfid == 0x41:
+                if cpid >= 0x17 and cpid <= 0x20:
+                    matched = True
+            elif mfid == 0x50:
+                if cpid >= 0x15 and cpid <= 0x16:
+                    matched = True
+            return matched
+
+        def flash_xmc_startup():
+            # If the RDID value is a valid XMC one, may skip the flow
+            fast_check = True
+            if fast_check and is_xmc_chip_strict():
+                return  # Successful XMC flash chip boot-up detected by RDID, skipping.
+
+            sfdp_mfid_addr = 0x10
+            mf_id = esp.read_spiflash_sfdp(sfdp_mfid_addr, 8)
+            if mf_id != XMC_VENDOR_ID:  # Non-XMC chip detected by SFDP Read, skipping.
+                return
+
+            print("WARNING: XMC flash chip boot-up failure detected! Running XMC25QHxxC startup flow")
+            esp.run_spiflash_command(0xB9)  # Enter DPD
+            esp.run_spiflash_command(0x79)  # Enter UDPD
+            esp.run_spiflash_command(0xFF)  # Exit UDPD
+            time.sleep(0.002)               # Delay tXUDPD
+            esp.run_spiflash_command(0xAB)  # Release Power-Down
+            time.sleep(0.00002)
+            # Check for success
+            if not is_xmc_chip_strict():
+                print("WARNING: XMC flash boot-up fix failed.")
+            print("XMC flash chip boot-up fix successful!")
+
+        # Check flash chip connection
+        if not esp.secure_download_mode:
+            try:
+                flash_id = esp.flash_id()
+                if flash_id in (0xffffff, 0x000000):
+                    print('WARNING: Failed to communicate with the flash chip, read/write operations will fail. '
+                          'Try checking the chip connections or removing any other hardware connected to IOs.')
+            except Exception as e:
+                esp.trace('Unable to verify flash chip connection ({}).'.format(e))
+
+        # Check if XMC SPI flash chip booted-up successfully, fix if not
+        if not esp.secure_download_mode:
+            try:
+                flash_xmc_startup()
+            except Exception as e:
+                esp.trace('Unable to perform XMC flash chip startup sequence ({}).'.format(e))
+
         if hasattr(args, "flash_size"):
             print("Configuring flash size...")
             detect_flash_size(esp, args)
             if args.flash_size != 'keep':  # TODO: should set this even with 'keep'
                 esp.flash_set_parameters(flash_size_bytes(args.flash_size))
+                # Check if stub supports chosen flash size
+                if esp.IS_STUB and args.flash_size in ('32MB', '64MB', '128MB'):
+                    print("WARNING: Flasher stub doesn't fully support flash size larger than 16MB, in case of failure use --no-stub.")
+
+        if esp.IS_STUB and hasattr(args, "address") and hasattr(args, "size"):
+            if args.address + args.size > 0x1000000:
+                print("WARNING: Flasher stub doesn't fully support flash size larger than 16MB, in case of failure use --no-stub.")
 
         try:
             operation_func(esp, args)
@@ -4281,292 +5235,407 @@ class AddrFilenamePairAction(argparse.Action):
 
 # Binary stub code (see flasher_stub dir for source & details)
 ESP8266ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b"""
-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\
+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\
+5dwuzr3/OyU1/H+n+L8kk1ilxnz4f3giyVw=\
 """)))
 ESP32ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b"""
-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\
+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\
 """)))
 ESP32S2ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b"""
-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\
+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\
++rqe/Hebvj3Y4S8jQkWaGpP//j8geRE3\
 """)))
 ESP32S3BETA2ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b"""
-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\
+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\
 """)))
-ESP32S3BETA3ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b"""
-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\
+ESP32S3ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b"""
+eNqNW3t31MaS/yrjMTa2gb1qzYzUTbLBDsngJHv3AiGGsD5nabWkkBzWx5jJjs2F+9lX9eouPczmD8OM1I/qev6qquefdzfN9ebuw1l19/zaLM+vbXZ+nS3edP9k6osP986vQ/Wo+5rGFCc8ypxft1X313bfww/d\
+wGxGb5zr/rfdg4ImwrM4w9KMONhbGowvS3rp4Xm26R7yyyaj/7NspxtRDJbAUfl5d5QGxnYLmbobkqWdszgf/rqTmaC+pFHwJn2hk+6kY/KOuF33rvW9xwdw2ln30XVndt0GTd49AR6s9B4n6QRw5mY1xZBlOntk\
+RqMPUc1bPPR10/HMwnpLWAeINb0FHQ3rc+QFvnrzQp+zoxlm1mPmbkj2NrtzdkpvcaT/KyPHkrg/i2yfjQQAJxRykDNBThU1jXUr7soHdEodUZfcgDyXv6UP6Qly+OxmUp0+dU9zOMo8A5mCRCa1KjsmehshthvX\
+icN5JeJacS0MyXKDA/WpGu7p+H+ySkv8Dv6RMF5Z21J/Pz6WT6ewF89xy7iacL2KRjpjvfOdHMyie9u9cd3RGk8qjVwu1NGKgfU6Po3VTK/yQ6VtC57ObO+NdKC2Nh0b/kDLTSfl4PgU/CxOCvkrntFR6SptV/kP\
+Q/mrDVBOPlEjmwlX8XMJ4lrz4GU6udiwN+SC8LmnwyfK0DwqNRXX6/TBwJqgw1n2iRaAN8aDL1vDJK1wWvEvNF/d+SYu039+0Z+1IaJrRSiqfIA3ijnMyKEGNlbc0DGxJX6p0puCmIRzwk9ktnAgHTSSP7vf/VOS\
+rzS3zrLDWZcvHid/gEO7+TWQvqJ1wO6AH9adwD+gvm7H0Tt0GwY2Zqdia4gK93lnD1OL3/3vz1ja7XD33sC+/qRBrxev50+/IzVtCgq1VclUj7zyl5jWLMYEYBAr+C/04tiINb7PGjqx6zGpG+LdM2ASsmuHmASj\
+6yGzQmASLb/tk0ZHhsOnY4f6tmPD+wPlScHv5DOWej1x7P7sc1Yemc4Tm+HEOdiBEX64OfgK2oAYNaPz2sUDmjGQjV67/Itrh7+2tvpj660kSjTg2O0+G5hl74df3FIeg6/ix73Yyerowx8xHp4k6m0AZ8x4Y3Hb\
+yVjDl6hlm/4TnLGamKFXXA7fr9XAVYrTKVACTagEwO4F/+GyM46tufLZzXh/gEOZ6ePOsW0mKbSZwNyKPx3sK27upKiNKwm+ItQZAWylMNG0m0smaQeoDKN6MZZgngCF90oB+bnND/hhIPVLkKlJwRVmGy8E/Hib\
+OxVqTgekNMwkXOlHkqxTgc5KQPc5M8WAI5Ew05QRuHU2YiEqujlHYsszghHLYUk7kjpqACAJ8DcQo2AL3x4DN+bRpiiIOruXTjVG3UPHMqU3wD3BaqY4Ujpqp8BYdAk5URrENHIO7fn5FZPcvakrFg/re3we1sRT\
+TatVk3gpPE+jiIkL1M2XKXwoaUieNoDJ8t1U8wheea92edthfTzsXkJ8eqi3Ykx+SZ+OyZiiu6ozti7PiAdUoMe4xQNDu6CdDWQp3gUVBnxYTSp3m/+KNtvwsY2C+fYLnmsi4jZsSnWNdF1oqYHaLfbZxhcc8FCx\
+7+X+yw7L2XQEMitEmqg+SGXLFowDhumvzx/3U4lhdLFTkcbwtpJLtG36wwHLmJ0FcvyUQWyUa4geb77LGRLwJ4cxoKlVnRxQ5dnzWUYmYAeCB0DoYPPga2y+ywkdy8gwUvdMtc0OwepnM05NKoFmK0Kg8Lmyu+x9\
+8CWEzopxXwmQ+D77DFD16h74fY6FqFf/Ib7ymN9wWtTDY9UteCxf0zkio3E5PdNOxwXTzVQKhRCdjxbwNT9rVK5hhqkrgn0EZRVp/F/OM0nIGEfYszpBK53MZiSLgHs8hi+7RweHu0nXOoh3oay6U48aiXu1fLpg\
+aO6AudUrGPv656fn5yccqfHAewIV3nbY0Rc0I1u+vfMbu+B8j0TUT7CvYbXuU0DDz16CenT/VMX/sEAzBi9KANk4k3i6YR1D3/D0dzhpt3G1YJ7VZNIVOyl0VpywoN2DmYQC6gRVPrvsBpSz5x0HGBs5hNEPf9nj\
+fLV9+upEwXPl30DngwNmi8twDMFRu1n06LkA8NUMdOCzpM8YL0G2xvwBhLwhRnRsQV8HhIJzAhUV+xznWhkpzwhUKTd8G09Q+ZYToDDW9MLDOceQ5eGLAxDgw/khJgFLWChkOS/d92OXVFBr/SNG1Kp0dBxh8wV/\
+7WKK1seGbLttVWXIoFnPgK6FpBsjlPh3XIbtrwnkyCz7a1SZPIlO3BUll6S4vlQVkjzZrnwelM8Q8sDpBhl7JcCqThFqKin12R3CRMMggIJup9IZK2qqnKzLAQbiucKJ1M5yXfjDJ+bQIt+h4OnnR4vsa1Hs/PCV\
+O43A5AG6JdQwkf1qOgtK5ddX+gtkeVSzPZ6xYN/GJ5v4qdjZIYmA8zHZbUJlbY5e+pkuZF2ghT4ODxmfcrJDMCNCSAwjoPHKLYVB3hHhuvmCObThaLjJ+Ahj0PDj/BjN+GyXoJtXULtSs8GkvUQeN0GHqkchEyuZ\
+s0dxB4RHeGt0OhRlmJLjOy27t/rLpf5yrb9serV3lYTs+JRMGd/+qgoeq0pO+hSsbfacgRLiAlTr9xKInnDlBcHwI5XIctWlzqahI9np+2Rt6CTtR7KF1Op4gJHjA6cBBhLt9nLT06JSgdUSPclG9Ancuy9U8qKx\
+ukBjAy7zC6rkwmuVYuYYKD/TN6fLjobmy2HgDJCX+Zi3ddZxJfs3FDC6wwADZ0yOaShrE+AVxAT4nOxboYdUsG+tRtj84/zjLyUJKnA9mULm1T5RZtjXElS//JMBSFmSMwEjbsLAeSFaXL9jsJQlV+8kuZR8Ffk5\
+cqFn3SxP4Q8wnwgF3XzxjmiqqiY5fsDqUF2AEnlbPElGG6xWGDnFR4rOsRptqu1aXMQnslFXErwJGWhtZokdpSjwlljmGhKZQ6uGMA+chs3R3pkIN8KpDcUwiUNulZyGz7TTaBKeRntyqQNj4KVfTbcogn/0j9OT\
+HwDQUQL4HZBSQqEKw3Z4w2EuL49rKGrBq4iE7RsgfofHIS8zGGBSY/KFPEtFzzf2mN1GL4cZJDyjZ0tc925sTUkBdtCTslxisb1AdTxeMG0+8RI7LtIPxS9mqfxdm/WJF+4l3uCcSjeAkCAqfl6r0mDFFcN6P8ZH\
+IaoRiPQ0QqUsFZ8gBqB1HN8hrqBkDmTuMg7cMKeWx5hJSX7YqlTRxg28VRvYJQ95F4c6ySoPVDOhTpMKynSwEhdSH/BiDtAufJsMX6fHvZZxKBj7hVKFapwFZpj/NC/BgdmG8tAIirGkZRA9lsnZecul1BqcT70j\
+H4zg+6M7Eb/h8lBKB8liHT2HJLeJqMhQGaHYWXPCBjZZc7mCS4wxk+Eo4NohPlf+tVKfDYP6jCvRqWbgEowMUzDSfAVbPFdpt3k0TgpwL9ZbIRaX9LdUhfF9OV6oN3kUKMAPV6Ne++LpPOHkwwRSjFWxy7qUMmLj\
+MjfJc0kxCxKsSj1XpZWgsh2mr3JT9BlRm+/7RMIrwGjNNLWbxEgXUv80+FkiT5qtvp3YmPLg55y7mtHmVE2f2Nxy4jhK9AxFkJERuXwkgpwwIsQTULWwAExoMul/SEME0ivPlfyMQSZuM1FiAyu1LOyaiy64q3pe\
+qc9NTNtjjdlQcBwdwOJSz/4FZz76DqDj6t6aKw3ZELb/V69+kssyhFOLD+TJvXSa6jhyfjpoQtVDy/qcYJgz06Bz/Pz9RwEItncPBSpnhms4FpPmrHf1gzNyW01vZEeC1kVyO5Ew6MsuzLucciyUbNmjbplyZ356\
+A0/nJ3fmbAEo4YMbOMX8ZA9AwTOCUiaUW5gJChWebGkRI1DPtLLA4oo/0GUNMObL+az9FjhxsNvX1kNWFZXJmDCf445NYhI2Ryvx/neUFwFHTLsSrKeNd14L/wEacp0RGFzNoCQkVwdgsjG6hE7hxC773qiBbC8+\
+XBFwxPULKf9dae51D/Hykyu5/IhloWyb/9r+JgkPd33xlsZ2t5WRnlUfcOtFiyGfh1tpGBX3ekXuJzwTk9+sfclfG/GTxb4ufaKwImkUdtGVdtSBgNpteo6N6IyvRXSL7goBs0GVPZ2zTftUfRZUK241EiYCOX+r\
+CbHFIVHxNw6SIzJtk8hkL5T3V2iZnirO+mcqLpikX8OVXSuvNM2tMFDDWi6W1IOdfSRtKSOy/ogdPdv0392RLuxSD1ooWiBpNPJiqWdnxbXqZJbz1KYBpWnK3kWvHwbDQOFhIQzQqDXY9oLMubxh0wxbcUB+tZFb\
+SPlsDz/vXad/FSqy0VQhtGzBqv6EIWfcEKrX3w8YmpkzQMkrtlmseEuHOeQUbCSNM1Md5vr84uyjFGxK2Z8n5ZgN7n1IUNO3us5/drqIo3iPqb4VwEazWL8U2vc5hVROFZgekOnXuwdYxCxn6c5StTzjaz4NeRqH\
+8fbsI4e2giJxr00Oo2uoYIeTLeV/hsoWF1OtjHIc37CytmBe1xAass5b1VgTUJ1Oyh4meihFyr9tkfqxMYmScFJQ6ooVJbqsdfYrlcK0CcWLTngmGLbakvMyjRwrqD6B140mQ650eMLhtVYn9ZQGq6C8aSkPtjxD\
+XVrATkczPrhTB6/VlQNkY3N+F0L/6gjsMTUfETG7sdSKW6WGdYl6eCUD5TIlqWyYXE0Q3vikUoHshZ1Gna23Ex4jRG3O6bhuoUc9YCRREDNR97EWAaWi4veeJfMrWcjWcs9wVxAwNxQx1rYplKYLwVNrYRC0ScIV\
+52TUn1xeSdYpB8pVUVims5JU9mfwLbv9aI4iUXfBPLnwRcf6sv0SZaH4JB6C266O85e2+f/ICuS/DxaJqtkBrNRIH+A914xqfVvlN1SMe4eIENgCPWAqu1hfJh65Qk0kr7jRxBRb0nGS1Tuk5FcoC+2SN0fN5Iar\
+4xue+naFgy1BU6Ta17TxyuNHvijESB1ijIFYYHGtDMP0XQjigp1WCsLhRc+tILftTL0q0m2BDlle61clu1pszVxy38ZO0W4ST+IZQvFKDAsykjoyia97oO59Psd0o2OSAf/N9zSVHdktXZshnXsuBVBMQg5eAZv/\
+oWpWgXtTTIuIMgVl2J/b3639APEA4CVoSoDGfYW3ckBcmB7UQ2/wmbtlF5dHJI5QfD9IrnDL/VPZ7FvJ/3f7qaGdcJEZwZpUk/MSC5Vf2XLFI19/rYp3yxOC9G7RkfiJ+gmhSB0xj1eJJCQ7/ICFF89KYT6O6XEY\
+ofMYqg8OqZAB8gjxVhRfmsGKOI4DW0Zx6fZ8nd/H7OsegjnMogH5+NmwaBNYr0FHwO8bqYhXfBvUYVRtT5VvzdXd+cSp9Z2eSLSDQT7vvGKPZ3++3pU7azsv4eENnyCo/IL9bjA/0zRRstpGhB4NXBTPMnyq2VhA\
+ZSr0g3Yxk35qz3tY8Bfm77CFPHcIW6BHWn7zn+Q8sKiz1Drh/PcPYis3eY2jD9e//TS/wbIB0NLO/gUTjqjNUUFVEBAIXsCzdO+HpHCDl6WLjUIneCZuzxv//hug8YDbIcVc+0ErpQpcq3zvuNznlFliq4UuQwnd\
+WUlX8ve+5n4MX8//SlLFR4x6AS/4RiWJ4ogkvTLlN1ugckn5HEgQcxdJGDEBItCpEhe4DoGSlZSsl9ys/+Ss5GKcUq2/SeDQ10p4lNmY8nkv2zDl/zJuZs6Q0V9hRKdw3uzwWenaMmUP4CNRY83hdF0L6EJUpj6n\
+RrvcpowNN8TQK/blljxBr89PSf4lmY7NOf/HoDtFgucrRo1VqcXEPQtXpmPXeF5ssXG0T8iGIMUfKQFzizQx1SrmukIiJQq4q1LN2CwrMkvMYqD2YpvTlLxg2az5cr2Fz2OH53nPCD5ISRC5h4FzjteCuHpnOdGA\
+soPNpeMiN4Ty4e1I9tPN6qHcQd1hz22f8PUhR1yj+kPGQQ5suKnkWkLT/6mOp6EGc0DhzIJOFe8NjcDvj1wepAst7iUe5GgBnrnssjGsQB1yFS7Lp4LapchsLvtBY8NkV1O1pgFQueJyufgft+5VS3b4hwO1tH8r\
+vveV33DNGW8qZu7E7ssK0mdEzOJiMNyTn+pQ0JMrZjX3wI1EN/mFCAsWc7lasur71E+XTLnWl1nCONNymMBgN8vzDWsrm3gxNml4wgAHI4E/IUvhzNuTdFvUe5pG/heM2QeOO4yiSLtSX13tE8If5FzxXi6VXeJ8\
+REpebhNFCOFXZY9xOWbccJst40no5UGtW+VeYLu6OOCONAoRksLiRseR8vCXtTIL+BEmLoi49ViplUSdEB6DTf97L3we/gLPvpLhB7tnBBlss8ddbW4Ve04craRD9djDUio6VPGVduOSgIDtoauEjNEYVQRVLhJv\
+cQzWa9QvJWzxb8O1b7u67MStx4yCeyT0uy6KjiHk/fJKVqrEFEHd4hN1HdIdon0evtqfc1O3UJUOfZc7gzuTtB9sHuayYMsxlraTaBgwhLp0Bw2HP1AXB5e3XByv5feLcrait8Q80dRn1t37M/xt8H9/2Pgr+IWw\
+ycpyUbqszLs3zcXm6kYeWrMy8LD2G88/JVad6rv8Ri+U5aUrCvv5/wDWE/uZ\
 """)))
 ESP32C3ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b"""
-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\
+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\
+2JPh/i7GZklhkqv/AdnBtYo=\
 """)))
 ESP32C6BETAROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b"""
-eNrFWmt700YW/itpEpKWvc3Iug0tiU3tOA6XZftAKTymRRpJWWjrXYKB0F3/9533XCQ5iQ3f9oMTezSaOXPOe95zkf5zuKwvl4d3dsrD+aWx80sbPmUWvuNjXp3NL30evg3ml4WbX+Y0ehAGi0fhT/pD+BOHoTT8\
-r3fDHy93x3T3/LKpnme0xnH4Yx6E9QfLMIrLzfxiflmb8CsaluO9sEG+zzKU0Wx+WUXje7PdcK9JirBxFD5hbp4Pw5/B/HC+wA5Y731YIaH1aJbLVmE0bFAHma0LX5pwxQfhyyabH5Jc/70f5lVhfsn3Nk2Wbbig\
-W49YNXTS8KmqjE7K60H4VHQWtcoLn6A6Fz5+gP/fr0SW/BnOOITwk24fE/7nbsQquHlTd7ySrXmHYU8G3an7bf33MACvHva7vjQsFmdjqCfYxw143WDRYCPn7+vOsHe4qQg3++ZlZlmddsB25uk2Pw2bVNOwth0H\
-FFThzlrVEI9xDdbjXcqYTWTMQ+ybHEPYcV9xltd32J/GwpJZOEad8gQCWcqYwG/nGEs6zouoLh6zCbAl/pfZQauye2SUUZDbWV7CJFNMxXEKwAmyMcyxSV7LOjnpdDeczEXjNdWKavQoHrochb82gKpOeDS3rJ4c\
-mzmsb7+CBwBJhrGAaY17doRJ3dJBOs9+qNrN3QlGgnorGWmqZ2x4aAP6tpksaTvZSgP/81YWGzDUIF9TY9GpOk0PGOEWnCMVjzOCOdtN0RM3ttsd9qhoNxKMtgqnqbCVv74EBPudNy6zCWFT/AAz8mQCuDEw/3Hs\
-Z3KyFLOnPSTjVF5VNOg5qNBZhdn40WOQe5gy4vlWJCfdGVxJcSXag8Fl6TwFQPZ4k0KP31/c2fNOHNo+7jaBLoOuG6UNrElMqTZS+Qusomg0Vw6jkKeTb9YB/RYiMTG2bt1J6IuPJAis9AcOy1t9xVcCJtmIRnAA\
-I16lF1my6i9Zi8CVIJPDDUWNGzmIwgy+TjXCUCR5xGfo0w8AB7YQUBYAcLhUOsJs0Ng3Y7mCUOIO2JXt4DljnOKIP/VPwzJGf479LzgrTIM4cpeY93R4IFqs2S2daBd0AJolnzZMJLW/Ep5wYJ/oIYfkKXO6dz5k\
-fYXl5iTzHIw54GO4bDhmAqJIKaFRwmQ/Ol4kNfuNBvJeXH7/rYRlXx//pqETt49Z8E0HocOUQlQ2YYYooymfgqCb7RyxSWr7bT/6TBWbwxv2g/fnjXArvD6abBJiJ3wBE4XQxuSoOYNQY08FI01g1AuIzJks9pi8\
-FYU9/1iybACr7/Maseb5ijkAqQ2Ch5crgEZe3WHUAVf4X1qm9hyTyz2+wLw4QiBxTHWsicdMlTdG5i7or8cSQMpIGIl0ISXzTTbEWDXoZnua3e1rhVX4vvMejUVX4wWRd97Fia1b2m5L5n9ot8y61EMSCz7qbXFa\
-8esykbAP6VwsYX9TxDXIpIzMooAQTxDWiGrevwVx2NU7XHqNnZMDMWY4QlXzQSvJWqpCJMpWAEbBmYEQ05KVkWtmIMJC8KY4YkGZZHYZnMpFNyvq1cHVlU3yplueGKCYjpjKODk57wVb+kBKJ2pmOTbI50S+9dt7\
-u8Geye52YivMnszwdoIvcDJ8M4nvtFUWw9uwUzGeLwJJNenz5gW0+2LWuiqI1Pl3mKRWe33G5Aor5MmjzwmiCLvCsHW1tszu54kaQRiRxjkl3f3RayYvxi+kS58ytyqiNdO50XkIW/bt29W7H0EEL8BGPwNSpVAM\
-xcZd0OABfPMMZ0Fga1BRVSgSBm+YK2qBaCFmVRWvyRH9lWuWEqotUyYgEISPPnP0HOo59xj9yIvmSaWQyrWU2w753c9bymarPzavSYj0d5hUCYb++uSw8QXfQtSr2ZCdHUnNllxNtbIrnkSJhJV0sEIgwj5NHLJN\
-U/q37GXEJBC5SDKN88BqMoJ/JdkjBlfxJeDSqIdVHCpm5/yH14yXUnnVoTZoIpG7vfKnEds9JxkHH6U0ppE1GX78PMD9up885BzsSxWGMObZDg3gYmeIfGGRAnOcXmk+L0cZrSSnglKSmH/UBd9ZuyP/9w7UCDV1\
-dFeK8M2y0iAh4QfOxheci5OsCXhVi5E2c0UBp5mrtVMehEaRZ1Bz4v9Ns4Se/GnSce37b9/ever1WzImKoLXKfnx7SeA4ZP54jnEn70B6xRnZ/dx8f7tB7j4YL54CL5++bDXdCmzZ6PZ87edAZD24ayB+I/ER4Rv\
-CwTSWJL/iGm1MjzHS6D1EmjpWsGyA+mUQkac6aIwAXcBG94seMI2Nyu8pn35MZhsrQbS/ElzGYyVhecV8LHRLRjDTv7JyWHOek4YN21dtl5TfXV+RIDbkTxYa7EkH2oPDd/6QoBujT3RUlZt2bbIbDKh5SQkbkga\
-wo9ei24+n17HRUlLqg7oiBmljnOWfiNummIrkS9Ey9w9QYlEJynYmqLjRduck44CpIO/kd4ASEuDCX/hfshkO3UU1CM5z+7LsdDesNl6eozMOE+aUaOG6JpJlKIEUZcIx/ibV79mD9Ssm9RcSARwpcYoRm/bU8jk\
-3DmfD4GbxuNuPBROXAwvpBQmtrRsJkyGUrgZNl9iKhVZcr0YrM/Hp0q/oRaQwNfED+mn9ttQRUIphZ18zSoprTJuFney+2xfQGabXejwJRdkLVxBIKjOmlI3+rJ6Y4fpb73Y2Khi16tLKMGttycSLpovBwjVmbbK\
-tL9h+TuRfaJnjLtipj1wIR2NykkJqhEArQ7sRT7gkb/674+g8Nec+nmn9mF7zHh43YjJiRjHZ7c9XDVlK9i2TRekAjhwIQDkopuP1pr/Va7E8+WJV7qhxvisCzTUpmg0qFM3fZbqT8fXyuTotRNxzVUp7xJmyLL3\
-/O8gsJgU4SUUZz9A4g5YT+YXqiThk3bN5CZQWeE3jhKi80TyjkoykuTud9KnJPKl48bSNEICmJzIltJJgl08jGWSiefWARFvLeaq8tO9fWVPlCm1cFIpJXrBxqkp+Yb/NKJsRNLqRFRSoT1cWN1cmmFN3m/cRNi/\
-kv1TdUyMjZVstoAYqqJJPvnOWbCkeis/u7hIV2JjGN3YsdQmDZ7CKF6ofiiqk84sJCaCJ9kRsaaR7nXlEd3zBwjOpzjTKYhsjI78eHtczSvU5Q5czb0XKdMtVdYPVWeMLF8peEd0wbWo/k0uwBityGLrhjqcAi8j\
-6QUZsQ/5trNc8bMEz7ZsBC/xTPEpIR11Uz6YMt97OVDGxV7L3U56DNRfyPb3GEym7NrERUwWuCXsQ23PHWqDo89TUVD1TNgtkgbtMwiaV4iIaXfJts0U5/kqoFqQ3YzvwSqZ3FYnmq3EUWgNWZu6/vd0cNiuKk8D\
-6vyKEtIuDwkuM+rKVzmnuGDdT5h6ys/ttDsEARRNTpi9Fkdkpn3Sp4SxhuFZP87wtUqQF127spQq4ApZU6Zes7LruJPQ6qMWy/AwyXgiXWGBGDXtWnQPV4NYIS08TmqoRN24wVAT9pzVrOpwlP5YqktuRX8BIEfo\
-KblqxQ2+plppmVANeajOxd8L8hppEt4s2Ks/AKVqwaCsJQUtBx9vMZNS3ZKNXnd7+IWUYWb1qRv9BD7cuhMC7kcJEO6M92u0Q7fttsJ80DBI2t8fMaFTjARCYKwyacl6LFWXAJuWFPL5HUlO0ru1mx13s50/lWxH\
-o6n3p0JeVZKzgTjl07sLvruu27uVm2qxrC4hXXbKeZAAuJwL3qU+GomltyKP7cpMisWmTS5OxLOsJPWZdIJr6UBWyuTJhJ+jc8CbyggJLMGEimGcq82IbzbcxV19NvdR6EJKZgQKCMDO8XLai8dxh20Sp6le4p4P\
-LFPVnI637fgzPRX5CZCOhyt4o6Qn+izHGSEirxzecKaUN5pb5uLqoSwfsd9W+ozRSnOGnhMPbp1Rz1y6v1xGIHdv1qLCK2nBad8O7wUQ3xKp1/2UePCxryPpyldEWF/rPVKbEcmVamQqIsStmnzouUlQ91Yz+owz\
-R85RcfaxL71/I05fXgGNNY+6nNUpBVLGo5F+HR+iU8qRB/udyFYecrU9CCLCsP9CsiD6j/jd9uXhb+WaItEw8J+kAdaPurza8xuuOKP32NlUwhzVmd6Xgsh4Mu0wWWf8CHNzXr6GUXs9T6fKpJIUgV6YmN0QyoMD\
-XnTvBPiBH+8gs/U/8VsKWjH+m7TERqpdDxqN+G/adXD4qAVLxLdLCqClkdfxLgJLFMU6dMr4epqMcBi/5GxuQ5R60b17wyccUZ71jgqCXeZ9wmCE4+G9ii6YG+J0Kw0l1tOSgvyP3YNO1R8HEP+YELqQ1Nj8ix4d\
-ctpOCye6cJ52TQw12Cbu8OYpOe/6o09RfuP6gswk9BbyYB0aX5+hiuDe4+GIiutvtgtgzBMJpaGqqnvdkuRtT79qILORBl9N1l6Ogsvfv/rOidQ2pJ5nwmmWkPyeYVzK+0UK65IL7kMhEtA+hugBadZ1+W4Ax0l3\
-ktwcbTv+PVTM/ubsny19DN77xBDiEa4isVZu9XWMD/pYVVGTslNv6ZLJazPNETf/WrxRRMW7WmHBBTcJucMfskl9/BB3O5jkVJ7yN5yMXZzeQ8vwjvQLReScHp3sTqNTeXWEIRRQOufEQZNXbeNY/QyEbJH836zI\
-HcSG6Bmd/lByDmzph2JEhHBK+So5MLWBo2cIBpQHUPGkjzgSKftN760HEYoeZ1R4H0Gfd9MbIEqliEl1Kv12PvnfABjpiDh1x4QTDQofNZRKEXHAyidiq7u9JW6sOyVJHUl5rWWm3mm2ZSg+FFDXXG1zchFvdLlc\
-3kOKJG0jtmskBhVbpRimX+ggufbWXPyQtyrTxwfolqeoZFL0y9NZg355enYCl0/vH6CiTh/gMjrm0cu61zEnKjCHf96h1yt/ebcsLvCSpTVZFlubxyZcqRfLi0/t4GAQ5WGwKpaFvo0JXAWHOpTh/irGprEz8ep/\
-TB3fMw==\
+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\
+""")))
+ESP32H2BETA1ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b"""
+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\
+""")))
+ESP32H2BETA2ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b"""
+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\
+""")))
+ESP32C2ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b"""
+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\
 """)))
 
 

From 96232e9def49526623278f024cfa19cb6037d2b8 Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Fri, 6 Sep 2024 15:55:03 +0200
Subject: [PATCH 05/58] #78: reset wifi nvs storage and do not store wifi data
 in nvs any more

---
 lib/gwwifi/GwWifi.cpp | 25 ++++++++++++++++++++++++-
 1 file changed, 24 insertions(+), 1 deletion(-)

diff --git a/lib/gwwifi/GwWifi.cpp b/lib/gwwifi/GwWifi.cpp
index 549f99d..69715d2 100644
--- a/lib/gwwifi/GwWifi.cpp
+++ b/lib/gwwifi/GwWifi.cpp
@@ -1,3 +1,4 @@
+#include <esp_wifi.h>
 #include "GWWifi.h"
 
 
@@ -35,7 +36,29 @@ void GwWifi::setup(){
         LOG_DEBUG(GwLog::ERROR,"unable to set access point mask %s, falling back to %s",
             apMask.c_str(),AP_subnet.toString().c_str());
     }
+    //try to remove any existing config from nvs
+    //this will avoid issues when updating from framework 6.3.2 to 6.8.1 - see #78
+    //we do not need the nvs config any way - so we set persistent to false
+    //unfortunately this will be to late (config from nvs has already been loaded)
+    //if we update from an older version that has config in the nvs
+    //so we need to make a dummy init, erase the flash and deinit
+    wifi_config_t conf_current;
+    wifi_init_config_t conf=WIFI_INIT_CONFIG_DEFAULT();
+    esp_err_t err=esp_wifi_init(&conf);
+    esp_wifi_get_config((wifi_interface_t)WIFI_IF_AP, &conf_current);
+    LOG_DEBUG(GwLog::DEBUG,"Wifi AP old config before reset ssid=%s, pass=%s, channel=%d",conf_current.ap.ssid,conf_current.ap.password,conf_current.ap.channel);
+    if (err){
+        LOG_DEBUG(GwLog::ERROR,"unable to pre-init wifi: %d",(int)err);
+    }
+    err=esp_wifi_restore();
+    if (err){
+        LOG_DEBUG(GwLog::ERROR,"unable to reset wifi: %d",(int)err);
+    }
+    err=esp_wifi_deinit();
+    WiFi.persistent(false);
     WiFi.mode(WIFI_MODE_APSTA); //enable both AP and client
+    esp_wifi_get_config((wifi_interface_t)WIFI_IF_AP, &conf_current);
+    LOG_DEBUG(GwLog::DEBUG,"Wifi AP old config after reset ssid=%s, pass=%s, channel=%d",conf_current.ap.ssid,conf_current.ap.password,conf_current.ap.channel);
     const char *ssid=config->getConfigItem(config->systemName)->asCString();
     if (fixedApPass){
         WiFi.softAP(ssid,AP_password);
@@ -45,7 +68,7 @@ void GwWifi::setup(){
     }
     delay(100);
     WiFi.softAPConfig(AP_local_ip, AP_gateway, AP_subnet);
-    LOG_DEBUG(GwLog::LOG,"WifiAP created: ssid=%s,adress=%s",
+    LOG_DEBUG(GwLog::ERROR,"WifiAP created: ssid=%s,adress=%s",
         ssid,
         WiFi.softAPIP().toString().c_str()
         );

From 20b108ac45932e95d2a1a3d64946b5d87897d479 Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Fri, 6 Sep 2024 15:55:32 +0200
Subject: [PATCH 06/58] update flashtool again

---
 tools/flashtool.pyz | Bin 467869 -> 683141 bytes
 1 file changed, 0 insertions(+), 0 deletions(-)

diff --git a/tools/flashtool.pyz b/tools/flashtool.pyz
index a3f96072c08ddc78470948229227a964052639e1..5dca3bbb793d44a7b8508d25e66754f0627725b8 100644
GIT binary patch
delta 104002
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
Uy16dIbe8D29P6&v;O%hyFWku}@&Et;

delta 184
zcmZp@sX6z!%!YI3%(GL&n=hKTUo>X~VkRJF-hR=XWrh9p#o1iK?U(km12M<;OZz!j
zu4H12*<PE;CBz8QG<{MQS3XFQ>~y(oF2(8Z_p`H3f0oUq#Z)RVy)c_gKEzC~pfWkY
zn~_O`S%d)u7-Q5TCFH^!t$=J0<^~BuK?9?<B0EETd~RZ9UVJ=IXMi^=8%T}=2!Ap%
KF!TYX85jVo8#FKg


From c1e8848f2524e74069e20db475db2a12471836eb Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Fri, 6 Sep 2024 17:20:38 +0200
Subject: [PATCH 07/58] update webinstaller to esptool 0.4.5

---
 webinstall/installUtil.js | 35 ++++++++++++++++++++++++-----------
 1 file changed, 24 insertions(+), 11 deletions(-)

diff --git a/webinstall/installUtil.js b/webinstall/installUtil.js
index 14910d8..330310a 100644
--- a/webinstall/installUtil.js
+++ b/webinstall/installUtil.js
@@ -1,4 +1,4 @@
-import {ESPLoader,Transport} from "https://cdn.jsdelivr.net/npm/esptool-js@0.2.1/bundle.js";
+import {ESPLoader,Transport} from "https://cdn.jsdelivr.net/npm/esptool-js@0.4.5/bundle.js";
 /**
  * write all messages to the console
  */
@@ -102,10 +102,14 @@ class ESPInstaller{
                     await this.consoleReader.cancel();
                     this.consoleReader=undefined;
                 }
-                await this.consoleDevice.close();
             }catch(e){
                 console.log(`error cancel serial read ${e}`);
             }
+            try{
+                await this.consoleDevice.close();
+            }catch(e){
+                console.log('error closing console device', this.consoleDevice,e);
+            }
             this.consoleDevice=undefined;
         }
         if (this.transport){
@@ -126,13 +130,17 @@ class ESPInstaller{
         }
         try {
             this.transport = new Transport(device);
-            this.esploader = new ESPLoader(this.transport, 115200, this.espLoaderTerminal);
-            let foundChip = await this.esploader.main_fn();
+            this.esploader = new ESPLoader({
+                transport:this.transport, 
+                baudrate: 115200, 
+                terminal: this.espLoaderTerminal});
+            //this.esploader.debugLogging=true;
+            let foundChip = await this.esploader.main();
             if (!foundChip) {
                 throw new Error("unable to read chip id");
             }
             this.espLoaderTerminal.writeLine(`chip: ${foundChip}`);
-            await this.esploader.flash_id();
+            //await this.esploader.flashId();
             this.chipFamily = this.esploader.chip.CHIP_NAME;
             this.imageChipId = this.esploader.chip.IMAGE_CHIP_ID;
             this.espLoaderTerminal.writeLine(`chipFamily: ${this.chipFamily}`);
@@ -198,12 +206,17 @@ class ESPInstaller{
     async writeFlash(fileList){
         this.checkConnected();
         this.espLoaderTerminal.writeLine(`Flashing....`);
-        await this.esploader.write_flash(
-            fileList,
-            "keep",
-            "keep",
-            "keep",
-            false
+        await this.esploader.writeFlash({
+            fileArray: fileList,
+            flashSize: "keep",
+            flashMode: "keep",
+            flashFreq: "keep",
+            eraseAll: false,
+            compress: true,
+            reportProgress: (fileIndex, written, total)=>{
+                this.espLoaderTerminal.writeLine(`file ${fileIndex}: ${written}/${total}`); 
+            }
+            }
         )
         await this.resetTransport();
         this.espLoaderTerminal.writeLine(`Done.`);

From c49cda2e57734273df83760c7862e0c94b48809a Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Fri, 6 Sep 2024 17:31:20 +0200
Subject: [PATCH 08/58] remove additional progress from webinstaller

---
 webinstall/installUtil.js | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/webinstall/installUtil.js b/webinstall/installUtil.js
index 330310a..c737d42 100644
--- a/webinstall/installUtil.js
+++ b/webinstall/installUtil.js
@@ -213,9 +213,9 @@ class ESPInstaller{
             flashFreq: "keep",
             eraseAll: false,
             compress: true,
-            reportProgress: (fileIndex, written, total)=>{
+            /*reportProgress: (fileIndex, written, total)=>{
                 this.espLoaderTerminal.writeLine(`file ${fileIndex}: ${written}/${total}`); 
-            }
+            }*/
             }
         )
         await this.resetTransport();

From bd793b8306034d50fb55e4838f06e6f29ab2f46c Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Fri, 6 Sep 2024 19:17:00 +0200
Subject: [PATCH 09/58] wipe nvs partition on factory reset

---
 lib/config/GWConfig.cpp | 26 ++++++++++++++++++++++++--
 1 file changed, 24 insertions(+), 2 deletions(-)

diff --git a/lib/config/GWConfig.cpp b/lib/config/GWConfig.cpp
index e53870a..b372d3d 100644
--- a/lib/config/GWConfig.cpp
+++ b/lib/config/GWConfig.cpp
@@ -4,6 +4,7 @@
 #include <ArduinoJson.h>
 #include <string.h>
 #include <MD5Builder.h>
+#include <esp_partition.h>
 using CfgInit=std::function<void(GwConfigHandler *)>;
 static std::vector<CfgInit> cfgInits;
 #define CFG_INIT(name,value,mode) \
@@ -96,15 +97,36 @@ bool GwConfigHandler::updateValue(String name, String value){
         if (i->asString() == value){
             return false;
         }
-        LOG_DEBUG(GwLog::LOG,"update config %s=>%s",name.c_str(),i->isSecret()?"***":value.c_str());
         prefs->begin(PREF_NAME,false);
         prefs->putString(i->getName().c_str(),value);
+        LOG_DEBUG(GwLog::LOG,"update config %s=>%s, freeEntries=%d",name.c_str(),i->isSecret()?"***":value.c_str(),(int)(prefs->freeEntries()));
         prefs->end();
     }
     return true;
 }
 bool GwConfigHandler::reset(){
-    LOG_DEBUG(GwLog::LOG,"reset config");
+    LOG_DEBUG(GwLog::ERROR,"reset config");
+    //try to find the nvs partition
+    //currently we only support the default
+    bool wiped=false;
+    const esp_partition_t *nvspart=esp_partition_find_first(ESP_PARTITION_TYPE_DATA,ESP_PARTITION_SUBTYPE_DATA_NVS,"nvs");
+    if (nvspart != NULL){
+        LOG_DEBUG(GwLog::ERROR,"wiping nvs partition");
+        esp_err_t err=esp_partition_erase_range(nvspart,0,nvspart->size);
+        if (err != ESP_OK){
+            LOG_DEBUG(GwLog::ERROR,"wiping nvs partition failed: %d",(int)err);
+        }
+        else{
+            wiped=true;
+        }
+    }
+    else{
+        LOG_DEBUG(GwLog::ERROR,"nvs partition not found");
+    }
+    if (wiped){
+        return true;
+    }
+    LOG_DEBUG(GwLog::ERROR,"unable to wipe nvs partition, trying to reset values");  
     prefs->begin(PREF_NAME,false);
     for (int i=0;i<getNumConfig();i++){
         prefs->putString(configs[i]->getName().c_str(),configs[i]->getDefault());

From e29e8eb5914c1ad1e4912cbf071b395c1814549a Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Sat, 7 Sep 2024 11:35:50 +0200
Subject: [PATCH 10/58] add example config for led

---
 lib/ledtask/GwLedTask.cpp  |  2 +-
 lib/ledtask/platformio.ini | 15 +++++++++++++++
 2 files changed, 16 insertions(+), 1 deletion(-)
 create mode 100644 lib/ledtask/platformio.ini

diff --git a/lib/ledtask/GwLedTask.cpp b/lib/ledtask/GwLedTask.cpp
index 5df1e9d..bda304e 100644
--- a/lib/ledtask/GwLedTask.cpp
+++ b/lib/ledtask/GwLedTask.cpp
@@ -43,7 +43,7 @@ void handleLeds(GwApi *api){
     leds[0]=colorFromMode(currentMode);
     FastLED.setBrightness(brightness);
     FastLED.show();
-    LOG_DEBUG(GwLog::LOG,"led task started with mode %d",(int)currentMode);
+    LOG_DEBUG(GwLog::LOG,"led task started with mode %d, brightness=%d",(int)currentMode,(int)brightness);
     int apiResult=0;
     while (true)
     {
diff --git a/lib/ledtask/platformio.ini b/lib/ledtask/platformio.ini
new file mode 100644
index 0000000..c883d3b
--- /dev/null
+++ b/lib/ledtask/platformio.ini
@@ -0,0 +1,15 @@
+[platformio]
+#if you want a pio run to only build
+#your special environments you can set this here
+#by uncommenting the next line
+#default_envs = testboard
+[env:nodemculed]
+board = nodemcu-32s
+lib_deps = ${env.lib_deps}
+build_flags = 
+	-D BOARD_HOMBERGER
+    -D GWLED_CODE=1
+    -D GWLED_PIN=33
+	${env.build_flags}
+upload_port = /dev/esp32
+upload_protocol = esptool

From d63b4d16619d7f938c58c56606ee4fa8f65b7a53 Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Wed, 18 Sep 2024 17:47:43 +0200
Subject: [PATCH 11/58] correctly terminate the current logbuffer when
 partially written

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

diff --git a/lib/channel/GwChannelList.cpp b/lib/channel/GwChannelList.cpp
index a118f13..c736ae2 100644
--- a/lib/channel/GwChannelList.cpp
+++ b/lib/channel/GwChannelList.cpp
@@ -72,7 +72,7 @@ public:
                 if (handled > 0){
                     memmove(logBuffer,logBuffer+handled,wp-handled);
                     wp-=handled;
-                    logBuffer[handled]=0;
+                    logBuffer[wp]=0;
                 }
                 return;
             }

From 55145726c3a936f65494701e421fa57150a04778 Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Thu, 19 Sep 2024 19:51:28 +0200
Subject: [PATCH 12/58] only link FastLED stuff when GWLED_FASTLED is defined

---
 lib/ledtask/GwLedTask.cpp | 74 ++++++++++++++++++++++-----------------
 lib/ledtask/GwLedTask.h   |  7 ++--
 2 files changed, 45 insertions(+), 36 deletions(-)

diff --git a/lib/ledtask/GwLedTask.cpp b/lib/ledtask/GwLedTask.cpp
index bda304e..0a03005 100644
--- a/lib/ledtask/GwLedTask.cpp
+++ b/lib/ledtask/GwLedTask.cpp
@@ -1,8 +1,19 @@
 #include "GwLedTask.h"
 #include "GwHardware.h"
 #include "GwApi.h"
+
+void handleLeds(GwApi *api);
+void initLeds(GwApi *param)
+{
+#ifdef GWLED_FASTLED
+    param->addUserTask(handleLeds, "handleLeds");
+#endif
+}
+
+#ifdef GWLED_FASTLED
 #include "FastLED.h"
-typedef enum {
+typedef enum
+{
     LED_OFF,
     LED_GREEN,
     LED_BLUE,
@@ -10,41 +21,38 @@ typedef enum {
     LED_WHITE
 } GwLedMode;
 
-static CRGB::HTMLColorCode colorFromMode(GwLedMode cmode){
-    switch(cmode){
-        case LED_BLUE:
-            return CRGB::Blue;    
-        case LED_GREEN:
-            return CRGB::Green;
-        case LED_RED:
-            return CRGB::Red;
-        case LED_WHITE:
-            return CRGB::White;        
-        default:
-            return CRGB::Black;    
+static CRGB::HTMLColorCode colorFromMode(GwLedMode cmode)
+{
+    switch (cmode)
+    {
+    case LED_BLUE:
+        return CRGB::Blue;
+    case LED_GREEN:
+        return CRGB::Green;
+    case LED_RED:
+        return CRGB::Red;
+    case LED_WHITE:
+        return CRGB::White;
+    default:
+        return CRGB::Black;
     }
 }
-void handleLeds(GwApi *api){
-    GwLog *logger=api->getLogger();
-    #ifndef GWLED_FASTLED
-        LOG_DEBUG(GwLog::LOG,"currently only fastled handling");
-        delay(50);
-        vTaskDelete(NULL);
-        return;
-    #else
+void handleLeds(GwApi *api)
+{
+    GwLog *logger = api->getLogger();
     CRGB leds[1];
-    #ifdef GWLED_SCHEMA
-    FastLED.addLeds<GWLED_TYPE,GWLED_PIN,(EOrder)GWLED_SCHEMA>(leds,1);
-    #else
-    FastLED.addLeds<GWLED_TYPE,GWLED_PIN>(leds,1);
-    #endif
-    uint8_t brightness=api->getConfig()->getInt(GwConfigDefinitions::ledBrightness,128);
-    GwLedMode currentMode=LED_GREEN;
-    leds[0]=colorFromMode(currentMode);
+#ifdef GWLED_SCHEMA
+    FastLED.addLeds<GWLED_TYPE, GWLED_PIN, (EOrder)GWLED_SCHEMA>(leds, 1);
+#else
+    FastLED.addLeds<GWLED_TYPE, GWLED_PIN>(leds, 1);
+#endif
+    uint8_t brightness = api->getConfig()->getInt(GwConfigDefinitions::ledBrightness, 128);
+    GwLedMode currentMode = LED_GREEN;
+    leds[0] = colorFromMode(currentMode);
     FastLED.setBrightness(brightness);
     FastLED.show();
-    LOG_DEBUG(GwLog::LOG,"led task started with mode %d, brightness=%d",(int)currentMode,(int)brightness);
-    int apiResult=0;
+    LOG_DEBUG(GwLog::LOG, "led task started with mode %d, brightness=%d", (int)currentMode, (int)brightness);
+    int apiResult = 0;
     while (true)
     {
         delay(50);
@@ -77,5 +85,5 @@ void handleLeds(GwApi *api){
         }
     }
     vTaskDelete(NULL);
-    #endif
-}
\ No newline at end of file
+}
+#endif
\ No newline at end of file
diff --git a/lib/ledtask/GwLedTask.h b/lib/ledtask/GwLedTask.h
index 2ae1532..03cb980 100644
--- a/lib/ledtask/GwLedTask.h
+++ b/lib/ledtask/GwLedTask.h
@@ -1,8 +1,9 @@
 #ifndef _GWLEDS_H
 #define _GWLEDS_H
 #include "GwApi.h"
-//task function
-void handleLeds(GwApi *param);
+//task init function
 
-DECLARE_USERTASK(handleLeds);
+void initLeds(GwApi *param);
+
+DECLARE_INITFUNCTION(initLeds);
 #endif
\ No newline at end of file

From eacefd4b09e8562057f2fe84964ae80a91449427 Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Thu, 19 Sep 2024 20:00:04 +0200
Subject: [PATCH 13/58] split dependencies to allow for builds without FastLED

---
 platformio.ini | 27 +++++++++++++++------------
 1 file changed, 15 insertions(+), 12 deletions(-)

diff --git a/platformio.ini b/platformio.ini
index b249ef8..0616d7f 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -16,6 +16,19 @@ default_envs=
 extra_configs=
 	lib/*task*/platformio.ini
 
+[basedeps]
+lib_deps = 
+    ttlappalainen/NMEA2000-library @ 4.18.9
+	ttlappalainen/NMEA0183 @ 1.9.1
+	ArduinoJson @ 6.18.5
+	AsyncTCP-esphome @ 2.0.1
+	ottowinter/ESPAsyncWebServer-esphome@2.0.1
+	FS
+	Preferences
+	ESPmDNS
+	WiFi
+	Update
+
 [env]
 platform = espressif32 @ 6.3.2
 framework = arduino
@@ -23,18 +36,8 @@ framework = arduino
 ;	framework-arduinoespressif32 @ 3.20011.230801
 ;	framework-espidf @ 3.50101.0
 lib_deps = 
-        ttlappalainen/NMEA2000-library @ 4.18.9
-	ttlappalainen/NMEA0183 @ 1.9.1
-	ArduinoJson @ 6.18.5
-	AsyncTCP-esphome @ 2.0.1
-	ottowinter/ESPAsyncWebServer-esphome@2.0.1
-    fastled/FastLED @ 3.6.0
-	FS
-	Preferences
-	ESPmDNS
-	WiFi
-	Update
-	
+	${basedeps.lib_deps}
+	fastled/FastLED @ 3.6.0
 
 
 board_build.embed_files = 

From 4ede06e33d85f15d8f8e4ed73b7e2f6ef016db31 Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Sun, 29 Sep 2024 16:27:20 +0200
Subject: [PATCH 14/58] untested: move to nmea2000 library 4.22.0, nmea0183
 1.10.1

---
 lib/nmea2kto0183/N2kDataToNMEA0183.cpp | 28 +++++++++++++++++---------
 platformio.ini                         |  4 ++--
 2 files changed, 20 insertions(+), 12 deletions(-)

diff --git a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp
index 26c6811..35ab4b7 100644
--- a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp
+++ b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp
@@ -657,12 +657,14 @@ private:
         double _Heading=N2kDoubleNA;
         double _ROT=N2kDoubleNA;
         tN2kAISNavStatus _NavStatus;
+        tN2kAISTransceiverInformation _AISTransceiverInformation; 
+        uint8_t _SID;
 
         uint8_t _MessageType = 1;
         tNMEA0183AISMsg NMEA0183AISMsg;
 
         if (ParseN2kPGN129038(N2kMsg, SID, _Repeat, _UserID, _Latitude, _Longitude, _Accuracy, _RAIM, _Seconds,
-                              _COG, _SOG, _Heading, _ROT, _NavStatus))
+                              _COG, _SOG, _Heading, _ROT, _NavStatus,_AISTransceiverInformation,_SID))
         {
 
 // Debug
@@ -746,12 +748,13 @@ private:
         tN2kGNSStype _GNSStype;
         tN2kAISTransceiverInformation _AISinfo;
         tN2kAISDTE _DTE;
+        uint8_t _SID;
 
         tNMEA0183AISMsg NMEA0183AISMsg;
 
-        if (ParseN2kPGN129794(N2kMsg, _MessageID, _Repeat, _UserID, _IMONumber, _Callsign, _Name, _VesselType,
-                              _Length, _Beam, _PosRefStbd, _PosRefBow, _ETAdate, _ETAtime, _Draught, _Destination,
-                              _AISversion, _GNSStype, _DTE, _AISinfo))
+        if (ParseN2kPGN129794(N2kMsg, _MessageID, _Repeat, _UserID, _IMONumber, _Callsign, 8, _Name,21, _VesselType,
+                              _Length, _Beam, _PosRefStbd, _PosRefBow, _ETAdate, _ETAtime, _Draught, _Destination,21,
+                              _AISversion, _GNSStype, _DTE, _AISinfo,_SID))
         {
 
 #ifdef SERIAL_PRINT_AIS_FIELDS
@@ -855,9 +858,10 @@ private:
         bool _Display, _DSC, _Band, _Msg22, _State;
         tN2kAISMode _Mode;
         tN2kAISTransceiverInformation  _AISTranceiverInformation;
+        uint8_t _SID;
 
         if (ParseN2kPGN129039(N2kMsg, _MessageID, _Repeat, _UserID, _Latitude, _Longitude, _Accuracy, _RAIM,
-                              _Seconds, _COG, _SOG, _AISTranceiverInformation, _Heading, _Unit, _Display, _DSC, _Band, _Msg22, _Mode, _State))
+                              _Seconds, _COG, _SOG, _AISTranceiverInformation, _Heading, _Unit, _Display, _DSC, _Band, _Msg22, _Mode, _State,_SID))
         {
 
             tNMEA0183AISMsg NMEA0183AISMsg;
@@ -896,8 +900,10 @@ private:
         tN2kAISRepeat _Repeat;
         uint32_t _UserID; // MMSI
         char _Name[21];
+        tN2kAISTransceiverInformation _AISInfo;
+        uint8_t _SID;
 
-        if (ParseN2kPGN129809(N2kMsg, _MessageID, _Repeat, _UserID, _Name))
+        if (ParseN2kPGN129809(N2kMsg, _MessageID, _Repeat, _UserID, _Name,21,_AISInfo,_SID))
         {
 
             tNMEA0183AISMsg NMEA0183AISMsg;
@@ -923,9 +929,11 @@ private:
         double _Beam=N2kDoubleNA;
         double _PosRefStbd=N2kDoubleNA;
         double _PosRefBow=N2kDoubleNA;
+        tN2kAISTransceiverInformation _AISInfo;
+        uint8_t _SID;
 
-        if (ParseN2kPGN129810(N2kMsg, _MessageID, _Repeat, _UserID, _VesselType, _Vendor, _Callsign,
-                              _Length, _Beam, _PosRefStbd, _PosRefBow, _MothershipID))
+        if (ParseN2kPGN129810(N2kMsg, _MessageID, _Repeat, _UserID, _VesselType, _Vendor,4, _Callsign,8,
+                              _Length, _Beam, _PosRefStbd, _PosRefBow, _MothershipID,_AISInfo,_SID))
         {
 
 //
@@ -1121,8 +1129,8 @@ private:
         int16_t ETADate=0;
         double BearingOriginToDestinationWaypoint=N2kDoubleNA;
         double BearingPositionToDestinationWaypoint=N2kDoubleNA;
-        uint8_t OriginWaypointNumber; 
-        uint8_t DestinationWaypointNumber;
+        uint32_t OriginWaypointNumber; 
+        uint32_t DestinationWaypointNumber;
         double DestinationLatitude=N2kDoubleNA;
         double DestinationLongitude=N2kDoubleNA;
         double WaypointClosingVelocity=N2kDoubleNA;
diff --git a/platformio.ini b/platformio.ini
index 0616d7f..2ed6e04 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -18,8 +18,8 @@ extra_configs=
 
 [basedeps]
 lib_deps = 
-    ttlappalainen/NMEA2000-library @ 4.18.9
-	ttlappalainen/NMEA0183 @ 1.9.1
+    ttlappalainen/NMEA2000-library @ 4.22.0
+	ttlappalainen/NMEA0183 @ 1.10.1
 	ArduinoJson @ 6.18.5
 	AsyncTCP-esphome @ 2.0.1
 	ottowinter/ESPAsyncWebServer-esphome@2.0.1

From c266bddea3001c526744bddccbe88915a032f7e2 Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Sun, 29 Sep 2024 19:56:39 +0200
Subject: [PATCH 15/58] make data store times configurable

---
 lib/boatData/GwBoatData.cpp       |  63 ++++++++++++++--
 lib/boatData/GwBoatData.h         | 115 +++++++++++++++++-------------
 lib/xdrmappings/GwXDRMappings.cpp |   8 ++-
 lib/xdrmappings/GwXDRMappings.h   |  10 ++-
 src/main.cpp                      |   2 +-
 web/config.json                   |  40 +++++++++++
 6 files changed, 176 insertions(+), 62 deletions(-)

diff --git a/lib/boatData/GwBoatData.cpp b/lib/boatData/GwBoatData.cpp
index df61ddf..f7be617 100644
--- a/lib/boatData/GwBoatData.cpp
+++ b/lib/boatData/GwBoatData.cpp
@@ -1,6 +1,7 @@
 #include "GwBoatData.h"
 #include <GwJsonDocument.h>
 #include <ArduinoJson/Json/TextFormatter.hpp>
+#include "GWConfig.h"
 #define GWTYPE_DOUBLE 1
 #define GWTYPE_UINT32 2
 #define GWTYPE_UINT16 3
@@ -35,6 +36,23 @@ GwBoatItemBase::GwBoatItemBase(String name, String format, unsigned long invalid
     this->format = format;
     this->type = 0;
     this->lastUpdateSource = -1;
+    this->toType=TOType::user;
+}
+GwBoatItemBase::GwBoatItemBase(String name, String format, GwBoatItemBase::TOType toType)
+{
+    lastSet = 0;
+    this->invalidTime = INVALID_TIME;
+    this->toType=toType;
+    this->name = name;
+    this->format = format;
+    this->type = 0;
+    this->lastUpdateSource = -1;
+    this->toType=TOType::user;
+}
+void GwBoatItemBase::setInvalidTime(unsigned long it, bool force){
+    if (toType != TOType::user || force ){
+        invalidTime=it;
+    }
 }
 size_t GwBoatItemBase::getJsonSize() { return JSON_OBJECT_SIZE(10); }
 #define STRING_SIZE 40
@@ -113,6 +131,16 @@ GwBoatItem<T>::GwBoatItem(String name, String formatInfo, unsigned long invalidT
         (*map)[name] = this;
     }
 }
+template <class T>
+GwBoatItem<T>::GwBoatItem(String name, String formatInfo, GwBoatItemBase::TOType toType, GwBoatItemMap *map) : GwBoatItemBase(name, formatInfo, toType)
+{
+    T dummy;
+    this->type = GwBoatItemTypes::getType(dummy);
+    if (map)
+    {
+        (*map)[name] = this;
+    }
+}
 
 template <class T>
 bool GwBoatItem<T>::update(T nv, int source)
@@ -246,14 +274,13 @@ void GwSatInfoList::houseKeeping(unsigned long ts)
                    sats.end(),
                    [ts, this](const GwSatInfo &info)
                    {
-                       return (info.timestamp + lifeTime) < ts;
+                       return info.validTill < ts;
                    }),
                sats.end());
 }
-void GwSatInfoList::update(GwSatInfo entry)
+void GwSatInfoList::update(GwSatInfo entry, unsigned long validTill)
 {
-    unsigned long now = millis();
-    entry.timestamp = now;
+    entry.validTill = validTill;
     for (auto it = sats.begin(); it != sats.end(); it++)
     {
         if (it->PRN == entry.PRN)
@@ -267,7 +294,7 @@ void GwSatInfoList::update(GwSatInfo entry)
     sats.push_back(entry);
 }
 
-GwBoatDataSatList::GwBoatDataSatList(String name, String formatInfo, unsigned long invalidTime, GwBoatItemMap *map) : GwBoatItem<GwSatInfoList>(name, formatInfo, invalidTime, map) {}
+GwBoatDataSatList::GwBoatDataSatList(String name, String formatInfo, GwBoatItemBase::TOType toType, GwBoatItemMap *map) : GwBoatItem<GwSatInfoList>(name, formatInfo, toType, map) {}
 
 bool GwBoatDataSatList::update(GwSatInfo info, int source)
 {
@@ -284,7 +311,7 @@ bool GwBoatDataSatList::update(GwSatInfo info, int source)
     }
     lastUpdateSource = source;
     uls(now);
-    data.update(info);
+    data.update(info,now+invalidTime);
     return true;
 }
 void GwBoatDataSatList::toJsonDoc(GwJsonDocument *doc, unsigned long minTime)
@@ -293,9 +320,31 @@ void GwBoatDataSatList::toJsonDoc(GwJsonDocument *doc, unsigned long minTime)
     GwBoatItem<GwSatInfoList>::toJsonDoc(doc, minTime);
 }
 
-GwBoatData::GwBoatData(GwLog *logger)
+GwBoatData::GwBoatData(GwLog *logger, GwConfigHandler *cfg)
 {
     this->logger = logger;
+    for (auto &&it : values){
+        unsigned long timeout=GwBoatItemBase::INVALID_TIME;
+        switch(it.second->getToType()){
+            case GwBoatItemBase::TOType::ais:
+                timeout=cfg->getInt(GwConfigDefinitions::timoAis);
+                break;
+            case GwBoatItemBase::TOType::def:
+                timeout=cfg->getInt(GwConfigDefinitions::timoDefault);
+                break;
+            case GwBoatItemBase::TOType::lng:
+                timeout=cfg->getInt(GwConfigDefinitions::timoLong);
+                break;
+            case GwBoatItemBase::TOType::sensor:
+                timeout=cfg->getInt(GwConfigDefinitions::timoSensor);
+                break;
+            case GwBoatItemBase::TOType::keep:
+                timeout=0;
+                break;
+        }
+        it.second->setInvalidTime(timeout);
+    }
+
 }
 GwBoatData::~GwBoatData()
 {
diff --git a/lib/boatData/GwBoatData.h b/lib/boatData/GwBoatData.h
index f5af8e8..1c4394c 100644
--- a/lib/boatData/GwBoatData.h
+++ b/lib/boatData/GwBoatData.h
@@ -2,6 +2,7 @@
 #define _GWBOATDATA_H
 
 #include "GwLog.h"
+#include "GWConfig.h"
 #include <Arduino.h>
 #include <map>
 #include <vector>
@@ -15,6 +16,14 @@
 class GwJsonDocument;
 class GwBoatItemBase{
     public:
+        using TOType=enum{
+            def=1,
+            ais=2,
+            sensor=3,
+            lng=4,
+            user=5,
+            keep=6
+        };
         class StringWriter{
             uint8_t *buffer =NULL;
             uint8_t *wp=NULL;
@@ -31,7 +40,7 @@ class GwBoatItemBase{
                 bool baseFilled();
                 void reset();
         };
-        static const unsigned long INVALID_TIME=60000;
+        static const long INVALID_TIME=60000;
         //the formatter names that must be known in js
         GWSC(formatCourse);
         GWSC(formatKnots);
@@ -51,10 +60,11 @@ class GwBoatItemBase{
     protected:
         int type;
         unsigned long lastSet=0;
-        unsigned long invalidTime=INVALID_TIME;
+        long invalidTime=INVALID_TIME;
         String name;
         String format;
         StringWriter writer;
+        TOType toType=TOType::def;
         void uls(unsigned long ts=0){
             if (ts) lastSet=ts;
             else lastSet=millis();
@@ -65,7 +75,8 @@ class GwBoatItemBase{
         int getCurrentType(){return type;}
         unsigned long getLastSet() const {return lastSet;}
         bool isValid(unsigned long now=0) const ;
-        GwBoatItemBase(String name,String format,unsigned long invalidTime=INVALID_TIME);
+        GwBoatItemBase(String name,String format,TOType toType);
+        GwBoatItemBase(String name,String format,unsigned long invalidTime);
         virtual ~GwBoatItemBase(){}
         void invalidate(){
             lastSet=0;
@@ -82,6 +93,8 @@ class GwBoatItemBase{
         virtual double getDoubleValue()=0;
         String getName(){return name;}
         const String & getFormat() const{return format;}
+        virtual void setInvalidTime(unsigned long it, bool force=true);
+        TOType getToType(){return toType;}
 };
 class GwBoatData;
 template<class T> class GwBoatItem : public GwBoatItemBase{
@@ -90,6 +103,7 @@ template<class T> class GwBoatItem : public GwBoatItemBase{
         bool lastStringValid=false;
     public:
         GwBoatItem(String name,String formatInfo,unsigned long invalidTime=INVALID_TIME,GwBoatItemMap *map=NULL);
+        GwBoatItem(String name,String formatInfo,TOType toType,GwBoatItemMap *map=NULL);
         virtual ~GwBoatItem(){}
         bool update(T nv, int source=-1);
         bool updateMax(T nv,int sourceId=-1);
@@ -118,14 +132,14 @@ class GwSatInfo{
         uint32_t Elevation;
         uint32_t Azimut;
         uint32_t SNR;
-        unsigned long timestamp;
+        unsigned long validTill;
 };
 class GwSatInfoList{
     public:
-        static const unsigned long lifeTime=32000;
+        static const GwBoatItemBase::TOType toType=GwBoatItemBase::TOType::lng;
         std::vector<GwSatInfo> sats;
         void houseKeeping(unsigned long ts=0);
-        void update(GwSatInfo entry);
+        void update(GwSatInfo entry, unsigned long validTill);
         int getNumSats() const{
             return sats.size();
         }
@@ -139,7 +153,7 @@ class GwSatInfoList{
 class GwBoatDataSatList : public GwBoatItem<GwSatInfoList>
 {
 public:
-    GwBoatDataSatList(String name, String formatInfo, unsigned long invalidTime = INVALID_TIME, GwBoatItemMap *map = NULL);
+    GwBoatDataSatList(String name, String formatInfo, GwBoatItemBase::TOType toType, GwBoatItemMap *map = NULL);
     bool update(GwSatInfo info, int source);
     virtual void toJsonDoc(GwJsonDocument *doc, unsigned long minTime);
     GwSatInfo *getAt(int idx){
@@ -164,56 +178,59 @@ public:
     virtual unsigned long getInvalidTime(){ return GwBoatItemBase::INVALID_TIME;}
     virtual ~GwBoatItemNameProvider() {}
 };
-#define GWBOATDATA(type,name,time,fmt)  \
+#define GWBOATDATAT(type,name,toType,fmt)  \
     static constexpr const char* _##name=#name; \
-    GwBoatItem<type> *name=new GwBoatItem<type>(#name,GwBoatItemBase::fmt,time,&values) ;
-#define GWSPECBOATDATA(clazz,name,time,fmt)  \
-    clazz *name=new clazz(#name,GwBoatItemBase::fmt,time,&values) ;
+    GwBoatItem<type> *name=new GwBoatItem<type>(#name,GwBoatItemBase::fmt,toType,&values) ;
+#define GWBOATDATA(type,name,fmt) GWBOATDATAT(type,name,GwBoatItemBase::TOType::def,fmt) 
+#define GWSPECBOATDATA(clazz,name,toType,fmt)  \
+    clazz *name=new clazz(#name,GwBoatItemBase::fmt,toType,&values) ;
 class GwBoatData{
+    static const unsigned long DEF_TIME=4000;
     private:
         GwLog *logger;
         GwBoatItemBase::GwBoatItemMap values;
     public:
 
-    GWBOATDATA(double,COG,4000,formatCourse) // course over ground
-    GWBOATDATA(double,SOG,4000,formatKnots) // speed over ground
-    GWBOATDATA(double,HDT,4000,formatCourse) // true heading
-    GWBOATDATA(double,HDM,4000,formatCourse) // magnetic heading
-    GWBOATDATA(double,STW,4000,formatKnots) // water speed
-    GWBOATDATA(double,VAR,4000,formatWind) // variation
-    GWBOATDATA(double,DEV,4000,formatWind) // deviation
-    GWBOATDATA(double,AWA,4000,formatWind) // apparent wind ANGLE
-    GWBOATDATA(double,AWS,4000,formatKnots) // apparent wind speed
-    GWBOATDATA(double,MaxAws,0,formatKnots)
-    GWBOATDATA(double,TWD,4000,formatCourse) // true wind DIRECTION
-    GWBOATDATA(double,TWA,4000,formatWind) // true wind ANGLE
-    GWBOATDATA(double,TWS,4000,formatKnots) // true wind speed
-    GWBOATDATA(double,MaxTws,0,formatKnots)
-    GWBOATDATA(double,ROT,4000,formatRot) // rate of turn
-    GWBOATDATA(double,RPOS,4000,formatWind) // rudder position
-    GWBOATDATA(double,PRPOS,4000,formatWind) // secondary rudder position
-    GWBOATDATA(double,LAT,4000,formatLatitude) 
-    GWBOATDATA(double,LON,4000,formatLongitude)
-    GWBOATDATA(double,ALT,4000,formatFixed0) //altitude
-    GWBOATDATA(double,HDOP,4000,formatDop)
-    GWBOATDATA(double,PDOP,4000,formatDop)
-    GWBOATDATA(double,VDOP,4000,formatDop)
-    GWBOATDATA(double,DBS,4000,formatDepth) //waterDepth (below surface)
-    GWBOATDATA(double,DBT,4000,formatDepth) //DepthTransducer
-    GWBOATDATA(double,GPST,4000,formatTime) // GPS time (seconds of day)
-    GWBOATDATA(uint32_t,GPSD,4000,formatDate) // GPS date (days since 1979-01-01)
-    GWBOATDATA(int16_t,TZ,8000,formatFixed0)
-    GWBOATDATA(double,WTemp,4000,kelvinToC)
-    GWBOATDATA(uint32_t,Log,16000,mtr2nm)
-    GWBOATDATA(uint32_t,TripLog,16000,mtr2nm)
-    GWBOATDATA(double,DTW,4000,mtr2nm) // distance to waypoint
-    GWBOATDATA(double,BTW,4000,formatCourse) // bearing to waypoint
-    GWBOATDATA(double,XTE,4000,formatXte) // cross track error
-    GWBOATDATA(double,WPLat,4000,formatLatitude) // waypoint latitude
-    GWBOATDATA(double,WPLon,4000,formatLongitude) // waypoint longitude
-    GWSPECBOATDATA(GwBoatDataSatList,SatInfo,GwSatInfoList::lifeTime,formatFixed0);
+    GWBOATDATA(double,COG,formatCourse) // course over ground
+    GWBOATDATA(double,SOG,formatKnots) // speed over ground
+    GWBOATDATA(double,HDT,formatCourse) // true heading
+    GWBOATDATA(double,HDM,formatCourse) // magnetic heading
+    GWBOATDATA(double,STW,formatKnots) // water speed
+    GWBOATDATA(double,VAR,formatWind) // variation
+    GWBOATDATA(double,DEV,formatWind) // deviation
+    GWBOATDATA(double,AWA,formatWind) // apparent wind ANGLE
+    GWBOATDATA(double,AWS,formatKnots) // apparent wind speed
+    GWBOATDATAT(double,MaxAws,GwBoatItemBase::TOType::keep,formatKnots)
+    GWBOATDATA(double,TWD,formatCourse) // true wind DIRECTION
+    GWBOATDATA(double,TWA,formatWind) // true wind ANGLE
+    GWBOATDATA(double,TWS,formatKnots) // true wind speed
+
+    GWBOATDATAT(double,MaxTws,GwBoatItemBase::TOType::keep,formatKnots)
+    GWBOATDATA(double,ROT,formatRot) // rate of turn
+    GWBOATDATA(double,RPOS,formatWind) // rudder position
+    GWBOATDATA(double,PRPOS,formatWind) // secondary rudder position
+    GWBOATDATA(double,LAT,formatLatitude) 
+    GWBOATDATA(double,LON,formatLongitude)
+    GWBOATDATA(double,ALT,formatFixed0) //altitude
+    GWBOATDATA(double,HDOP,formatDop)
+    GWBOATDATA(double,PDOP,formatDop)
+    GWBOATDATA(double,VDOP,formatDop)
+    GWBOATDATA(double,DBS,formatDepth) //waterDepth (below surface)
+    GWBOATDATA(double,DBT,formatDepth) //DepthTransducer
+    GWBOATDATA(double,GPST,formatTime) // GPS time (seconds of day)
+    GWBOATDATA(uint32_t,GPSD,formatDate) // GPS date (days since 1979-01-01)
+    GWBOATDATAT(int16_t,TZ,GwBoatItemBase::TOType::lng,formatFixed0)
+    GWBOATDATA(double,WTemp,kelvinToC)
+    GWBOATDATAT(uint32_t,Log,GwBoatItemBase::TOType::lng,mtr2nm)
+    GWBOATDATAT(uint32_t,TripLog,GwBoatItemBase::TOType::lng,mtr2nm)
+    GWBOATDATA(double,DTW,mtr2nm) // distance to waypoint
+    GWBOATDATA(double,BTW,formatCourse) // bearing to waypoint
+    GWBOATDATA(double,XTE,formatXte) // cross track error
+    GWBOATDATA(double,WPLat,formatLatitude) // waypoint latitude
+    GWBOATDATA(double,WPLon,formatLongitude) // waypoint longitude
+    GWSPECBOATDATA(GwBoatDataSatList,SatInfo,GwSatInfoList::toType,formatFixed0);
     public:
-        GwBoatData(GwLog *logger);
+        GwBoatData(GwLog *logger, GwConfigHandler *cfg);
         ~GwBoatData();
         template<class T> GwBoatItem<T> *getOrCreate(T initial,GwBoatItemNameProvider *provider);
         template<class T> bool update(T value,int source,GwBoatItemNameProvider *provider);
diff --git a/lib/xdrmappings/GwXDRMappings.cpp b/lib/xdrmappings/GwXDRMappings.cpp
index b2eb0c2..ad26bc8 100644
--- a/lib/xdrmappings/GwXDRMappings.cpp
+++ b/lib/xdrmappings/GwXDRMappings.cpp
@@ -355,6 +355,7 @@ void GwXDRMappings::begin()
 GwXDRFoundMapping GwXDRMappings::selectMapping(GwXDRMapping::MappingList *list, int instance, const char *key)
 {
     GwXDRMapping *candidate = NULL;
+    unsigned long invalidTime=config->getInt(GwConfigDefinitions::timoSensor);
     for (auto mit = list->begin(); mit != list->end(); mit++)
     {
         GwXDRMappingDef *def = (*mit)->definition;
@@ -369,7 +370,7 @@ GwXDRFoundMapping GwXDRMappings::selectMapping(GwXDRMapping::MappingList *list,
             {
                 LOG_DEBUG(GwLog::DEBUG + 1, "selected mapping %s for %s, i=%d",
                           def->toString().c_str(), key, instance);
-                return GwXDRFoundMapping(*mit, instance);
+                return GwXDRFoundMapping(*mit,invalidTime, instance);
             }
             if (instance < 0)
             {
@@ -393,7 +394,7 @@ GwXDRFoundMapping GwXDRMappings::selectMapping(GwXDRMapping::MappingList *list,
     {
         LOG_DEBUG(GwLog::DEBUG + 1, "selected mapping %s for %s, i=%d",
                   candidate->definition->toString().c_str(), key, instance);
-        return GwXDRFoundMapping(candidate, instance>=0?instance:candidate->definition->instanceId);
+        return GwXDRFoundMapping(candidate, invalidTime,instance>=0?instance:candidate->definition->instanceId);
     }
     LOG_DEBUG(GwLog::DEBUG + 1, "no instance mapping found for key=%s, i=%d", key, instance);
     return GwXDRFoundMapping();
@@ -472,8 +473,9 @@ String GwXDRMappings::getXdrEntry(String mapping, double value,int instance){
     }
     GwXDRType *type = findType(code, &typeIndex);
     bool first=true;
+    unsigned long invalidTime=config->getInt(GwConfigDefinitions::timoSensor);
     while (type){
-        GwXDRFoundMapping found(def,type);
+        GwXDRFoundMapping found(def,type,invalidTime);
         found.instanceId=instance;
         if (first) first=false;
         else rt+=",";
diff --git a/lib/xdrmappings/GwXDRMappings.h b/lib/xdrmappings/GwXDRMappings.h
index 246ade5..7643bee 100644
--- a/lib/xdrmappings/GwXDRMappings.h
+++ b/lib/xdrmappings/GwXDRMappings.h
@@ -167,15 +167,18 @@ class GwXDRFoundMapping : public GwBoatItemNameProvider{
         GwXDRType *type=NULL;
         int instanceId=-1;
         bool empty=true;
-        GwXDRFoundMapping(GwXDRMappingDef *definition,GwXDRType *type){
+        unsigned long timeout=0;
+        GwXDRFoundMapping(GwXDRMappingDef *definition,GwXDRType *type, unsigned long timeout){
             this->definition=definition;
             this->type=type;
+            this->timeout=timeout;
             empty=false;
         }
-        GwXDRFoundMapping(GwXDRMapping* mapping,int instance=0){
+        GwXDRFoundMapping(GwXDRMapping* mapping,unsigned long timeout,int instance){
             this->definition=mapping->definition;
             this->type=mapping->type;
             this->instanceId=instance;
+            this->timeout=timeout;
             empty=false;
         }
         GwXDRFoundMapping(){}
@@ -195,6 +198,9 @@ class GwXDRFoundMapping : public GwBoatItemNameProvider{
             return "formatXdr:"+type->xdrtype+":"+type->boatDataUnit; 
         };
         virtual ~GwXDRFoundMapping(){}
+        virtual unsigned long getInvalidTime() override{
+            return timeout;
+        }
 };
 
 //the class GwXDRMappings is not intended to be deleted
diff --git a/src/main.cpp b/src/main.cpp
index 599b340..17d2fa1 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -138,7 +138,7 @@ bool fixedApPass=true;
 #endif
 GwWifi gwWifi(&config,&logger,fixedApPass);
 GwChannelList channels(&logger,&config);
-GwBoatData boatData(&logger);
+GwBoatData boatData(&logger,&config);
 GwXDRMappings xdrMappings(&logger,&config);
 bool sendOutN2k=true;
 
diff --git a/web/config.json b/web/config.json
index ebc5a27..7dad6ca 100644
--- a/web/config.json
+++ b/web/config.json
@@ -250,6 +250,46 @@
         "description": "the n2k instance to be used as port rudder 0...253, -1 to disable",
         "category": "converter"
     },
+    {
+        "name": "timeouts",
+        "type": "array",
+        "replace":[
+            {
+                "n":"Default",
+                "d":"4000",
+                "l": "default",
+                "t": "NMEA"
+            },
+            {
+                "n":"Sensor",
+                "d":"60000",
+                "l": "sensor",
+                "t": "sensor"
+            },
+            {
+                "n":"Long",
+                "d":"32000",
+                "l": "long",
+                "t": "special NMEA"
+            },
+            {
+                "n":"Ais",
+                "d":"120000",
+                "l": "ais",
+                "t": "ais"
+            }
+        ],
+        "children":[
+            {
+            "name":"timo$n",
+            "label":"timeout $l",
+            "default": "$d",
+            "type": "number",
+            "description": "data timeouts(ms) for $t data",
+            "category": "converter"
+            }
+        ]
+    },
     {
         "name": "usbActisense",
         "label": "USB mode",

From 3c664b1480c341cf0bf2d00df92674e1848a6480 Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Sun, 6 Oct 2024 20:06:15 +0200
Subject: [PATCH 16/58] #75: untested/intermediate - add some mappings for the
 pgn 130306 wind reference

---
 lib/config/GwConverterConfig.h          | 71 +++++++++++++++++++++++-
 lib/nmea0183ton2k/NMEA0183DataToN2K.cpp | 32 +++++++----
 lib/nmea2kto0183/N2kDataToNMEA0183.cpp  | 73 +++++++++++++++++--------
 src/main.cpp                            |  2 +-
 web/config.json                         | 52 ++++++++++++++++++
 5 files changed, 195 insertions(+), 35 deletions(-)

diff --git a/lib/config/GwConverterConfig.h b/lib/config/GwConverterConfig.h
index a3e9c81..546d885 100644
--- a/lib/config/GwConverterConfig.h
+++ b/lib/config/GwConverterConfig.h
@@ -16,16 +16,62 @@
 #define _GWCONVERTERCONFIG_H
 
 #include "GWConfig.h"
+#include "N2kTypes.h"
+#include <map>
+
+//list of configs for the PGN 130306 wind references
+static std::map<tN2kWindReference,String> windConfigs={
+  {N2kWind_True_water,GwConfigDefinitions::windmtra},
+  {N2kWind_Apparent,GwConfigDefinitions::windmawa},
+  {N2kWind_True_boat,GwConfigDefinitions::windmgna},
+  {N2kWind_Magnetic,GwConfigDefinitions::windmmgd},
+  {N2kWind_True_North,GwConfigDefinitions::windmtng},
+};
 
 class GwConverterConfig{
     public:
+    class WindMapping{
+      public:
+      using Wind0183Type=enum{
+        AWA_AWS,
+        TWA_TWS,
+        TWD_TWS,
+        GWA_GWS,
+        GWD_GWS
+      };
+      tN2kWindReference n2kType;  
+      Wind0183Type nmea0183Type;
+      bool valid=false;
+      WindMapping(){}
+      WindMapping(const tN2kWindReference &n2k,const Wind0183Type &n183):
+        n2kType(n2k),nmea0183Type(n183),valid(true){}
+      WindMapping(const tN2kWindReference &n2k,const String &n183):
+        n2kType(n2k){
+          if (n183 == "twa_tws"){
+            nmea0183Type=TWA_TWS;
+            valid=true;
+            return;
+          }
+          if (n183 == "awa_aws"){
+            nmea0183Type=AWA_AWS;
+            valid=true;
+            return;
+          }
+          if (n183 == "twd_tws"){
+            nmea0183Type=TWD_TWS;
+            valid=true;
+            return;
+          }
+        }  
+    };
       int minXdrInterval=100;
       int starboardRudderInstance=0; 
       int portRudderInstance=-1; //ignore
       int min2KInterval=50;
       int rmcInterval=1000;
       int rmcCheckTime=4000;
-      void init(GwConfigHandler *config){
+      std::vector<WindMapping> windMappings;
+      void init(GwConfigHandler *config, GwLog*logger){
         minXdrInterval=config->getInt(GwConfigDefinitions::minXdrInterval,100);
         starboardRudderInstance=config->getInt(GwConfigDefinitions::stbRudderI,0);
         portRudderInstance=config->getInt(GwConfigDefinitions::portRudderI,-1);
@@ -36,6 +82,29 @@ class GwConverterConfig{
         rmcInterval=config->getInt(GwConfigDefinitions::sendRMCi,1000);
         if (rmcInterval < 0) rmcInterval=0;
         if (rmcInterval > 0 && rmcInterval <100) rmcInterval=100;
+        for (auto && it:windConfigs){
+          String cfg=config->getString(it.second);
+          WindMapping mapping(it.first,cfg);
+          if (mapping.valid){
+            LOG_DEBUG(GwLog::ERROR,"add wind mapping n2k=%d,nmea0183=%01d(%s)",
+              (int)(mapping.n2kType),(int)(mapping.nmea0183Type),cfg.c_str());
+            windMappings.push_back(mapping);
+          }
+        }
       }
+      const WindMapping findWindMapping(const tN2kWindReference &n2k) const{
+        for (const auto & it:windMappings){
+          if (it.n2kType == n2k) return it;
+        }
+        return WindMapping();
+      }
+      const WindMapping findWindMapping(const WindMapping::Wind0183Type &n183) const{
+        for (const auto & it:windMappings){
+          if (it.nmea0183Type == n183) return it;
+        }
+        return WindMapping();
+      }
+
+
   };
 #endif
\ No newline at end of file
diff --git a/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp b/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp
index 7868eaf..8bb9237 100644
--- a/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp
+++ b/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp
@@ -399,28 +399,29 @@ private:
             return;
         }
         tN2kMsg n2kMsg;
-        tN2kWindReference n2kRef; 
         bool shouldSend=false;
         WindAngle=formatDegToRad(WindAngle);
+        GwConverterConfig::WindMapping mapping;
         switch(Reference){
             case NMEA0183Wind_Apparent:
-                n2kRef=N2kWind_Apparent;
                 shouldSend=updateDouble(boatData->AWA,WindAngle,msg.sourceId) && 
                            updateDouble(boatData->AWS,WindSpeed,msg.sourceId);
                 if (WindSpeed != NMEA0183DoubleNA) boatData->MaxAws->updateMax(WindSpeed);    
+                mapping=config.findWindMapping(GwConverterConfig::WindMapping::AWA_AWS);
                 break;
             case NMEA0183Wind_True:
-                n2kRef=N2kWind_True_water;
                 shouldSend=updateDouble(boatData->TWA,WindAngle,msg.sourceId) && 
                            updateDouble(boatData->TWS,WindSpeed,msg.sourceId);
                 if (WindSpeed != NMEA0183DoubleNA) boatData->MaxTws->updateMax(WindSpeed);    
+                mapping=config.findWindMapping(GwConverterConfig::WindMapping::TWA_TWS);
                 break;      
             default:
                 LOG_DEBUG(GwLog::DEBUG,"unknown wind reference %d in %s",(int)Reference,msg.line);
         }
-        if (shouldSend){
-            SetN2kWindSpeed(n2kMsg,1,WindSpeed,WindAngle,n2kRef);  
-            send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((int)n2kRef));  
+        //TODO: try to compute TWD and get mapping for this one
+        if (shouldSend && mapping.valid){
+            SetN2kWindSpeed(n2kMsg,1,WindSpeed,WindAngle,mapping.n2kType);  
+            send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((int)mapping.n2kType));  
         }
     }
     void convertVWR(const SNMEA0183Msg &msg)
@@ -460,8 +461,11 @@ private:
         if (WindSpeed != NMEA0183DoubleNA) boatData->MaxAws->updateMax(WindSpeed);             
         if (shouldSend)
         {
-            SetN2kWindSpeed(n2kMsg, 1, WindSpeed, WindAngle, N2kWind_Apparent);
-            send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((int)N2kWind_Apparent));
+            const GwConverterConfig::WindMapping mapping=config.findWindMapping(GwConverterConfig::WindMapping::AWA_AWS);
+            if (mapping.valid){
+                SetN2kWindSpeed(n2kMsg, 1, WindSpeed, WindAngle, mapping.n2kType);
+                send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((int)mapping.n2kType));
+            }
         }
     }
 
@@ -504,8 +508,16 @@ private:
                 double twa = WindDirection-boatData->HDT->getData();
                 if(twa<0) { twa+=2*M_PI; }
                 updateDouble(boatData->TWA, twa, msg.sourceId);
-                SetN2kWindSpeed(n2kMsg, 1, WindSpeed, twa, N2kWind_True_water);
-                send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((int)N2kWind_True_water));
+                const GwConverterConfig::WindMapping mapping=config.findWindMapping(GwConverterConfig::WindMapping::TWA_TWS);
+                if (mapping.valid){
+                    SetN2kWindSpeed(n2kMsg, 1, WindSpeed, twa, mapping.n2kType);
+                    send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((int)mapping.n2kType));
+                }
+                const GwConverterConfig::WindMapping mapping2=config.findWindMapping(GwConverterConfig::WindMapping::TWD_TWS);
+                if (mapping2.valid){
+                    SetN2kWindSpeed(n2kMsg, 1, WindSpeed, WindDirection, mapping2.n2kType);
+                    send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((int)mapping2.n2kType));
+                }
             }
         }
     }
diff --git a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp
index 659a5c3..03a4a3c 100644
--- a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp
+++ b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp
@@ -469,38 +469,65 @@ private:
         unsigned char SID;
         tN2kWindReference WindReference;
         double WindAngle=N2kDoubleNA, WindSpeed=N2kDoubleNA;
-
+        tNMEA0183WindReference NMEA0183Reference;
         if (ParseN2kWindSpeed(N2kMsg, SID, WindSpeed, WindAngle, WindReference)) {
             tNMEA0183Msg NMEA0183Msg;
-            tNMEA0183WindReference NMEA0183Reference;
+            GwConverterConfig::WindMapping mapping=config.findWindMapping(WindReference);
             bool shouldSend = false;
 
             // MWV sentence contains apparent/true ANGLE and SPEED
             // https://gpsd.gitlab.io/gpsd/NMEA.html#_mwv_wind_speed_and_angle
             // https://docs.vaisala.com/r/M211109EN-L/en-US/GUID-7402DEF8-5E82-446F-B63E-998F49F3D743/GUID-C77934C7-2A72-466E-BC52-CE6B8CC7ACB6
-
-            if (WindReference == N2kWind_Apparent) {
-                NMEA0183Reference = NMEA0183Wind_Apparent;
-                updateDouble(boatData->AWA, WindAngle);
-                updateDouble(boatData->AWS, WindSpeed);
-                setMax(boatData->MaxAws, boatData->AWS);
-                shouldSend = true;
-            } 
-            if (WindReference == N2kWind_True_water) {
-                NMEA0183Reference = NMEA0183Wind_True;
-                updateDouble(boatData->TWA, WindAngle);
-                updateDouble(boatData->TWS, WindSpeed);
-                setMax(boatData->MaxTws, boatData->TWS);
-                shouldSend = true;
-                if (boatData->HDT->isValid()) {
-                    double twd = WindAngle+boatData->HDT->getData();
-                    if (twd>2*M_PI) { twd-=2*M_PI; }
-                    updateDouble(boatData->TWD, twd);
+            if (mapping.valid)
+            {
+                if (mapping.nmea0183Type == GwConverterConfig::WindMapping::AWA_AWS)
+                {
+                    NMEA0183Reference = NMEA0183Wind_Apparent;
+                    updateDouble(boatData->AWA, WindAngle);
+                    updateDouble(boatData->AWS, WindSpeed);
+                    setMax(boatData->MaxAws, boatData->AWS);
+                    shouldSend = true;
+                }
+                if (mapping.nmea0183Type == GwConverterConfig::WindMapping::TWA_TWS)
+                {
+                    NMEA0183Reference = NMEA0183Wind_True;
+                    updateDouble(boatData->TWA, WindAngle);
+                    updateDouble(boatData->TWS, WindSpeed);
+                    setMax(boatData->MaxTws, boatData->TWS);
+                    shouldSend = true;
+                    if (boatData->HDT->isValid())
+                    {
+                        double twd = WindAngle + boatData->HDT->getData();
+                        if (twd > 2 * M_PI)
+                        {
+                            twd -= 2 * M_PI;
+                        }
+                        updateDouble(boatData->TWD, twd);
+                    }
+                }
+                if (mapping.nmea0183Type == GwConverterConfig::WindMapping::TWD_TWS)
+                {
+                    NMEA0183Reference = NMEA0183Wind_True;
+                    updateDouble(boatData->TWD, WindAngle);
+                    updateDouble(boatData->TWS, WindSpeed);
+                    setMax(boatData->MaxTws, boatData->TWS);
+                    if (boatData->HDT->isValid())
+                    {
+                        shouldSend = true;
+                        double twa = WindAngle - boatData->HDT->getData();
+                        if (twa > 2 * M_PI)
+                        {
+                            twa -= 2 * M_PI;
+                        }
+                        updateDouble(boatData->TWA, twa);
+                        WindAngle=twa;
+                    }
                 }
-            }
 
-            if (shouldSend && NMEA0183SetMWV(NMEA0183Msg, formatCourse(WindAngle), NMEA0183Reference, WindSpeed, talkerId)) {
-                SendMessage(NMEA0183Msg);
+                if (shouldSend && NMEA0183SetMWV(NMEA0183Msg, formatCourse(WindAngle), NMEA0183Reference, WindSpeed, talkerId))
+                {
+                    SendMessage(NMEA0183Msg);
+                }
             }
 
             /* if (WindReference == N2kWind_Apparent && boatData->SOG->isValid())
diff --git a/src/main.cpp b/src/main.cpp
index 17d2fa1..714c0a5 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -850,7 +850,7 @@ void setup() {
   xdrMappings.begin();
   logger.flush();
   GwConverterConfig converterConfig;
-  converterConfig.init(&config);
+  converterConfig.init(&config,&logger);
   nmea0183Converter= N2kDataToNMEA0183::create(&logger, &boatData, 
     [](const tNMEA0183Msg &msg, int sourceId){
       SendNMEA0183Message(msg,sourceId,false);
diff --git a/web/config.json b/web/config.json
index 7dad6ca..906920a 100644
--- a/web/config.json
+++ b/web/config.json
@@ -250,6 +250,58 @@
         "description": "the n2k instance to be used as port rudder 0...253, -1 to disable",
         "category": "converter"
     },
+    {
+        "name": "windmappings",
+        "type": "array",
+        "replace":[
+            {
+                "n": "tng",
+                "l": "true north ground",
+                "t": "True_North=0",
+                "d": "twa_tws"
+            },
+            {
+                "n": "mgd",
+                "l": "magnetic ground dir",
+                "t": "Magnetic=1",
+                "d":""
+            },
+            {
+                "n": "awa",
+                "l": "apparent angle",
+                "t": "Apparent=2",
+                "d":"awa_aws"
+            },
+            {
+                "n": "gna",
+                "l": "ground angle",
+                "t": "True_boat=3",
+                "d": ""
+            },
+            {
+                "n": "tra",
+                "l": "true angle",
+                "t": "True_water=4",
+                "d":""
+            }
+
+        ],
+        "children":[
+            {
+                "name":"windm$n",
+                "type":"list",
+                "description": "mapping of the PGN 130306 wind reference $t",
+                "label":"wind $l",
+                "list":[
+                    {"l": "-unset-","v":""},
+                    {"l": "TWA/TWS","v":"twa_tws"},
+                    {"l": "AWA/AWS", "v":"awa_aws"},
+                    {"l": "TWD/TWS","v":"twd_tws"}
+                ]
+
+            }
+        ]
+    },
     {
         "name": "timeouts",
         "type": "array",

From db3d8323eaa4f6be1c2614967bef385b38df2b6e Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Mon, 7 Oct 2024 19:57:19 +0200
Subject: [PATCH 17/58] config for wind mappings working

---
 web/config.json | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/web/config.json b/web/config.json
index 906920a..69df271 100644
--- a/web/config.json
+++ b/web/config.json
@@ -297,7 +297,9 @@
                     {"l": "TWA/TWS","v":"twa_tws"},
                     {"l": "AWA/AWS", "v":"awa_aws"},
                     {"l": "TWD/TWS","v":"twd_tws"}
-                ]
+                ],
+                "category":"converter",
+                "default":"$d"
 
             }
         ]

From 465c66a514fab87222ff6e5cf8da8e8b6718df8d Mon Sep 17 00:00:00 2001
From: free-x <oroitburd@gmail.com>
Date: Fri, 11 Oct 2024 10:54:00 +0200
Subject: [PATCH 18/58] Issue #83: Converting MTW from PGN 130311

---
 lib/nmea2kto0183/N2kDataToNMEA0183.cpp | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp
index 03a4a3c..a64e498 100644
--- a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp
+++ b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp
@@ -1332,6 +1332,21 @@ private:
             return;
         }
         int i=0;
+        if (TempSource == N2kts_SeaTemperature) {
+          updateDouble(boatData->WTemp, Temperature);
+          tNMEA0183Msg NMEA0183Msg;
+
+          if (!NMEA0183Msg.Init("MTW", talkerId))
+              return;
+          if (!NMEA0183Msg.AddDoubleField(KelvinToC(Temperature)))
+              return;
+          if (!NMEA0183Msg.AddStrField("C"))
+              return;
+
+            SendMessage(NMEA0183Msg);
+            i++;
+        }
+
         GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRTEMP,TempSource,0,0);
         if (updateDouble(&mapping,Temperature)){
             LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str());

From c1ac1baa1b67ca7bf42564b20c4ee2263be4560f Mon Sep 17 00:00:00 2001
From: free-x <oroitburd@gmail.com>
Date: Fri, 11 Oct 2024 12:00:39 +0200
Subject: [PATCH 19/58] fix ci for ubuntu-latest

---
 .github/workflows/ci.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2562875..0a9999c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -18,7 +18,7 @@ jobs:
         run: |
           #apt-get install -y python3-pip
           python3 -m pip install --upgrade pip
-          pip install -U platformio
+          pip install -U platformio --break-system-packages
       - name: Build
         run: |
           pio run

From 61e2c93b7336b2237a41b566f35da3bcb8b515b3 Mon Sep 17 00:00:00 2001
From: free-x <oroitburd@gmail.com>
Date: Fri, 11 Oct 2024 13:00:41 +0200
Subject: [PATCH 20/58] fix ci part 2 for ubuntu-latest

---
 .github/workflows/ci.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0a9999c..ac78ae5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -17,7 +17,7 @@ jobs:
       - name: Install deps
         run: |
           #apt-get install -y python3-pip
-          python3 -m pip install --upgrade pip
+          python3 -m pip install --upgrade pip --break-system-packages
           pip install -U platformio --break-system-packages
       - name: Build
         run: |

From 5cb8bdd8c74161e53bcb35e6a57a510ac260dd52 Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Fri, 11 Oct 2024 15:51:05 +0200
Subject: [PATCH 21/58] only set --break-system-packages as environment to be
 compatible with older versions

---
 .github/workflows/ci.yml      | 6 ++++--
 .github/workflows/release.yml | 3 +++
 2 files changed, 7 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ac78ae5..050f76f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -10,6 +10,8 @@ jobs:
         os: [ubuntu-latest]
     
     runs-on: ${{ matrix.os }}
+    env:
+      PIP_BREAK_SYSTEM_PACKAGES:1
     steps:
       - uses: actions/checkout@v2
         with: 
@@ -17,8 +19,8 @@ jobs:
       - name: Install deps
         run: |
           #apt-get install -y python3-pip
-          python3 -m pip install --upgrade pip --break-system-packages
-          pip install -U platformio --break-system-packages
+          python3 -m pip install --upgrade pip
+          pip install -U platformio
       - name: Build
         run: |
           pio run
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index f7a26bc..52b2fd4 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -18,6 +18,9 @@ jobs:
     # The type of runner that the job will run on
     runs-on: ubuntu-latest
 
+    env:
+      PIP_BREAK_SYSTEM_PACKAGES:1
+
     # Steps represent a sequence of tasks that will be executed as part of the job
     steps:
       # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it

From 0cd552d590de0a77175207628b2f4f11aa7400ec Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Fri, 11 Oct 2024 15:52:28 +0200
Subject: [PATCH 22/58] correct workflow yaml errors

---
 .github/workflows/ci.yml      | 2 +-
 .github/workflows/release.yml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 050f76f..c373801 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,7 +11,7 @@ jobs:
     
     runs-on: ${{ matrix.os }}
     env:
-      PIP_BREAK_SYSTEM_PACKAGES:1
+      PIP_BREAK_SYSTEM_PACKAGES: 1
     steps:
       - uses: actions/checkout@v2
         with: 
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 52b2fd4..2aeae17 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -19,7 +19,7 @@ jobs:
     runs-on: ubuntu-latest
 
     env:
-      PIP_BREAK_SYSTEM_PACKAGES:1
+      PIP_BREAK_SYSTEM_PACKAGES: 1
 
     # Steps represent a sequence of tasks that will be executed as part of the job
     steps:

From 09b583ebd68b0f8b4aba871e40ae45050dc93173 Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Fri, 11 Oct 2024 16:14:22 +0200
Subject: [PATCH 23/58] also set water temperature (MTW) from PGN 130312 - with
 condition

---
 lib/config/GwConverterConfig.h         |  2 +
 lib/nmea2kto0183/N2kDataToNMEA0183.cpp | 16 ++++-
 web/config.json                        | 99 ++++++++++++++------------
 3 files changed, 72 insertions(+), 45 deletions(-)

diff --git a/lib/config/GwConverterConfig.h b/lib/config/GwConverterConfig.h
index 546d885..d0dbc6a 100644
--- a/lib/config/GwConverterConfig.h
+++ b/lib/config/GwConverterConfig.h
@@ -70,6 +70,7 @@ class GwConverterConfig{
       int min2KInterval=50;
       int rmcInterval=1000;
       int rmcCheckTime=4000;
+      int winst312=256;
       std::vector<WindMapping> windMappings;
       void init(GwConfigHandler *config, GwLog*logger){
         minXdrInterval=config->getInt(GwConfigDefinitions::minXdrInterval,100);
@@ -82,6 +83,7 @@ class GwConverterConfig{
         rmcInterval=config->getInt(GwConfigDefinitions::sendRMCi,1000);
         if (rmcInterval < 0) rmcInterval=0;
         if (rmcInterval > 0 && rmcInterval <100) rmcInterval=100;
+        winst312=config->getInt(GwConfigDefinitions::winst312,256);
         for (auto && it:windConfigs){
           String cfg=config->getString(it.second);
           WindMapping mapping(it.first,cfg);
diff --git a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp
index a64e498..1056c87 100644
--- a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp
+++ b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp
@@ -1344,7 +1344,6 @@ private:
               return;
 
             SendMessage(NMEA0183Msg);
-            i++;
         }
 
         GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRTEMP,TempSource,0,0);
@@ -1379,6 +1378,21 @@ private:
            LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN);
            return;
         }
+        if (TemperatureSource == N2kts_SeaTemperature && 
+            (config.winst312 == TemperatureInstance || config.winst312 == 256)) {
+          updateDouble(boatData->WTemp, Temperature);
+          tNMEA0183Msg NMEA0183Msg;
+
+          if (!NMEA0183Msg.Init("MTW", talkerId))
+              return;
+          if (!NMEA0183Msg.AddDoubleField(KelvinToC(Temperature)))
+              return;
+          if (!NMEA0183Msg.AddStrField("C"))
+              return;
+
+            SendMessage(NMEA0183Msg);
+        }
+
         GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRTEMP,(int)TemperatureSource,0,TemperatureInstance);
         if (updateDouble(&mapping,Temperature)){
             LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str());
diff --git a/web/config.json b/web/config.json
index 69df271..ca155ec 100644
--- a/web/config.json
+++ b/web/config.json
@@ -219,16 +219,56 @@
         "category":"converter"
      },
      {
-        "name":"checkRMCt",
+        "name": "checkRMCt",
         "label": "check RMC time",
         "type": "number",
         "description": "start sending RMC if we did not see an external RMC after this much ms",
-        "default":"4000",
+        "default": "4000",
         "min": 1000,
-        "check":"checkMinMax",
-        "category":"converter"
-     },
-     {
+        "check": "checkMinMax",
+        "category": "converter"
+    },
+    {
+        "name": "timeouts",
+        "type": "array",
+        "replace": [
+            {
+                "n": "Default",
+                "d": "4000",
+                "l": "default",
+                "t": "NMEA"
+            },
+            {
+                "n": "Sensor",
+                "d": "60000",
+                "l": "sensor",
+                "t": "sensor"
+            },
+            {
+                "n": "Long",
+                "d": "32000",
+                "l": "long",
+                "t": "special NMEA"
+            },
+            {
+                "n": "Ais",
+                "d": "120000",
+                "l": "ais",
+                "t": "ais"
+            }
+        ],
+        "children": [
+            {
+                "name": "timo$n",
+                "label": "timeout $l",
+                "default": "$d",
+                "type": "number",
+                "description": "data timeouts(ms) for $t data",
+                "category": "converter"
+            }
+        ]
+    },
+    {
         "name": "stbRudderI",
         "label":"stb rudder instance",
         "type": "number",
@@ -305,44 +345,15 @@
         ]
     },
     {
-        "name": "timeouts",
-        "type": "array",
-        "replace":[
-            {
-                "n":"Default",
-                "d":"4000",
-                "l": "default",
-                "t": "NMEA"
-            },
-            {
-                "n":"Sensor",
-                "d":"60000",
-                "l": "sensor",
-                "t": "sensor"
-            },
-            {
-                "n":"Long",
-                "d":"32000",
-                "l": "long",
-                "t": "special NMEA"
-            },
-            {
-                "n":"Ais",
-                "d":"120000",
-                "l": "ais",
-                "t": "ais"
-            }
-        ],
-        "children":[
-            {
-            "name":"timo$n",
-            "label":"timeout $l",
-            "default": "$d",
-            "type": "number",
-            "description": "data timeouts(ms) for $t data",
-            "category": "converter"
-            }
-        ]
+        "name": "winst312",
+        "label": "130312 WTemp iid",
+        "type": "number",
+        "check": "checkMinMax",
+        "min": -1,
+        "max": 256,
+        "description": "the temp instance of PGN 130312 used for water temperature, use -1 for none, 256 for any",
+        "default": "256",
+        "category":"converter"
     },
     {
         "name": "usbActisense",

From f6e3fa369f73134a077e1ae504eaed10b45109de Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Fri, 11 Oct 2024 19:23:19 +0200
Subject: [PATCH 24/58] intermediate: prepare for js api

---
 doc/Conversions.odt |  Bin 17203 -> 26103 bytes
 doc/Conversions.pdf |  Bin 38012 -> 38259 bytes
 extra_script.py     |   16 +-
 web/config.json     |    2 +-
 web/index.css       |    3 +
 web/index.js        | 3859 ++++++++++++++++++++++---------------------
 6 files changed, 1976 insertions(+), 1904 deletions(-)

diff --git a/doc/Conversions.odt b/doc/Conversions.odt
index 975520d39cfa3f817e319c8b3ccfb907fedc8985..9ca2adbc44e5f7c038bb02cadeb73559c7fe439b 100644
GIT binary patch
literal 26103
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
M@MY6+)%{ufAD$UL@c;k-

literal 17203
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

diff --git a/doc/Conversions.pdf b/doc/Conversions.pdf
index 75b2bd240efac709d17fd418b357f06432366c93..3de736be458ce876b404e60dad7c020d55feb8b1 100644
GIT binary patch
delta 32931
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

delta 32701
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

diff --git a/extra_script.py b/extra_script.py
index 7713451..b8b5c97 100644
--- a/extra_script.py
+++ b/extra_script.py
@@ -18,6 +18,8 @@ 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'
@@ -66,6 +68,7 @@ def isCurrent(infile,outfile):
 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)
@@ -372,6 +375,16 @@ def getLibs():
             rt.append(e)
     return rt
 
+def joinFiles(target,pattern,dirlist):
+    with gzip.open(target,"wb") as oh:
+        for dir in dirlist:
+            fn=os.path.join(dir,pattern)
+            if os.path.exists(fn):
+                print("adding %s to %s"%(fn,target))
+                with open(fn,"rb") as rh:
+                    shutil.copyfileobj(rh,oh)
+    
+
 OWNLIBS=getLibs()+["FS","WiFi"]
 GLOBAL_INCLUDES=[]
 
@@ -440,6 +453,8 @@ def prebuild(env):
     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:
@@ -453,7 +468,6 @@ def prebuild(env):
         filedefs.append((pureName,usname,ct))
         inFile=os.path.join(basePath(),"web",pureName)
         if os.path.exists(inFile):
-            print("compressing %s"%inFile)
             compressFile(inFile,ef)
         else:
             print("#WARNING: infile %s for %s not found"%(inFile,ef))
diff --git a/web/config.json b/web/config.json
index ca155ec..5c21eb5 100644
--- a/web/config.json
+++ b/web/config.json
@@ -351,7 +351,7 @@
         "check": "checkMinMax",
         "min": -1,
         "max": 256,
-        "description": "the temp instance of PGN 130312 used for water temperature, use -1 for none, 256 for any",
+        "description": "the temp instance of PGN 130312 used for water temperature (0...255), use -1 for none, 256 for any",
         "default": "256",
         "category":"converter"
     },
diff --git a/web/index.css b/web/index.css
index c3ea7ee..aada09f 100644
--- a/web/index.css
+++ b/web/index.css
@@ -340,4 +340,7 @@ body {
 }
 .error{
   color: red;
+}
+.changed input.error{
+  color: red;
 }
\ No newline at end of file
diff --git a/web/index.js b/web/index.js
index 032b087..d61556b 100644
--- a/web/index.js
+++ b/web/index.js
@@ -1,1933 +1,1988 @@
-let self = this;
-let lastUpdate = (new Date()).getTime();
-let reloadConfig = false;
-let needAdminPass=true;
-let lastSalt="";
-let channelList={};
-let minUser=200;
-function addEl(type, clazz, parent, text) {
-    let el = document.createElement(type);
-    if (clazz) {
-        if (!(clazz instanceof Array)) {
-            clazz = clazz.split(/  */);
-        }
-        clazz.forEach(function (ce) {
-            el.classList.add(ce);
-        });
-    }
-    if (text) el.textContent = text;
-    if (parent) parent.appendChild(el);
-    return el;
-}
-function forEl(query, callback,base) {
-    if (! base) base=document;
-    let all = base.querySelectorAll(query);
-    for (let i = 0; i < all.length; i++) {
-        callback(all[i]);
-    }
-}
-function closestParent(element,clazz){
-    while (true){
-        let parent=element.parentElement;
-        if (! parent) return;
-        if (parent.classList.contains(clazz)) return parent;
-        element=parent;
-    }
-}
-function alertRestart() {
-    reloadConfig = true;
-    alert("Board reset triggered, reconnect WLAN if necessary");
-}
-function getJson(url) {
-    return fetch(url)
-        .then(function (r) { return r.json() });
-}
-function getText(url){
-    return fetch(url)
-        .then(function (r) { return r.text() });
-}
-function reset() {
-    ensurePass()
-        .then(function (hash) {
-            fetch('/api/reset?_hash='+encodeURIComponent(hash));
-            alertRestart();
-        })
-        .catch(function (e) { });
-}
-function update() {
-    let now = (new Date()).getTime();
-    let ce = document.getElementById('connected');
-    if (ce) {
-        if ((lastUpdate + 3000) > now) {
-            ce.classList.add('ok');
-        }
-        else {
-            ce.classList.remove('ok');
-        }
-    }
-    getJson('/api/status')
-        .then(function (jsonData) {
-            let statusPage=document.getElementById('statusPageContent');
-            let even=true; //first counter
-            for (let k in jsonData) {
-                if (k == "salt"){
-                    lastSalt=jsonData[k];
-                    continue;
-                }
-                if (k == "minUser"){
-                    minUser=parseInt(jsonData[k]);
-                    continue;
-                }
-                if (! statusPage) continue;
-                if (typeof (jsonData[k]) === 'object') {
-                    if (k.indexOf('count') == 0) {
-                        createCounterDisplay(statusPage, k.replace("count", "").replace(/in$/," in").replace(/out$/," out"), k, even);
-                        even = !even;
-                        for (let sk in jsonData[k]) {
-                            let key = k + "." + sk;
-                            if (typeof (jsonData[k][sk]) === 'object') {
-                                //msg details
-                                updateMsgDetails(key, jsonData[k][sk]);
-                            }
-                            else {
-                                let el = document.getElementById(key);
-                                if (el) el.textContent = jsonData[k][sk];
-                            }
-                        }
-                    }
-                    if (k.indexOf("ch")==0){
-                        //channel def
-                        let name=k.substring(2);
-                        channelList[name]=jsonData[k];
-                    }
-                }
-                else {
-                    let el = document.getElementById(k);
-                    if (el) el.textContent = jsonData[k];
-                    forEl('.status-'+k,function(el){
-                        el.textContent=jsonData[k];
-                    });
-                }
+(function () {
+    let self = this;
+    let lastUpdate = (new Date()).getTime();
+    let reloadConfig = false;
+    let needAdminPass = true;
+    let lastSalt = "";
+    let channelList = {};
+    let minUser = 200;
+    let listeners = [];
+    function addEl(type, clazz, parent, text) {
+        let el = document.createElement(type);
+        if (clazz) {
+            if (!(clazz instanceof Array)) {
+                clazz = clazz.split(/  */);
             }
-            lastUpdate = (new Date()).getTime();
-            if (reloadConfig) {
-                reloadConfig = false;
-                resetForm();
-            }
-        })
-}
-function resetForm(ev) {
-    getJson("/api/config")
-        .then(function (jsonData) {
-            for (let k in jsonData) {
-                if (k == "useAdminPass"){
-                    needAdminPass=jsonData[k] != 'false';
-                }
-                let el = document.querySelector("[name='" + k + "']");
-                if (el) {
-                    let v = jsonData[k];
-                    let def=getConfigDefition(k);
-                    if (def.check == 'checkMinMax'){
-                        //simple migration if the current value is outside the range
-                        //we even "hide" this from the user
-                        v=parseFloat(v);
-                        if (! isNaN(v)){
-                            if (def.min !== undefined){
-                                if (v < parseFloat(def.min)) v=parseFloat(def.min);
-                            }
-                            if (def.max !== undefined){
-                                if (v > parseFloat(def.max)) v=parseFloat(def.max);
-                            }
-                        }
-                    }
-                    if (el.tagName === 'SELECT') {
-                        setSelect(el,v);
-                    }
-                    else{
-                        el.value = v;
-                    }
-                    el.setAttribute('data-loaded', v);
-                    let changeEvent = new Event('change');
-                    el.dispatchEvent(changeEvent);
-                }
-            }
-            let name=jsonData.systemName;
-            if (name){
-                let el=document.getElementById('headline');
-                if (el) el.textContent=name;
-                document.title=name;
-            }
-        });
-}
-function checkMinMax(v,allValues,def){
-    let parsed=parseFloat(v);
-    if (isNaN(parsed)) return "must be a number";
-    if (def.min !== undefined){
-        if (parsed < parseFloat(def.min)) return "must be >= "+def.min;
-    }
-    if (def.max !== undefined){
-        if (parsed > parseFloat(def.max)) return "must be <= "+def.max;
-    }
-}
-
-function checkSystemName(v) {
-    //2...32 characters for ssid
-    let allowed = v.replace(/[^a-zA-Z0-9]*/g, '');
-    if (allowed != v) return "contains invalid characters, only a-z, A-Z, 0-9";
-    if (v.length < 2 || v.length > 32) return "invalid length (2...32)";
-}
-function checkApPass(v) {
-    //min 8 characters
-    if (v.length < 8) {
-        return "password must be at least 8 characters";
-    }
-}
-function checkAdminPass(v){
-    return checkApPass(v);
-}
-
-function checkApIp(v,allValues){
-    if (! v) return "cannot be empty";
-    let err1="must be in the form 192.168.x.x";
-    if (! v.match(/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/))return err1;
-    let parts=v.split(".");
-    if (parts.length != 4) return err1;
-    for (let idx=0;idx < 4;idx++){
-        let iv=parseInt(parts[idx]);
-        if (iv < 0 || iv > 255) return err1;
-    }
-}
-function checkNetMask(v,allValues){
-    return checkApIp(v,allValues);
-}
-
-function checkIpAddress(v,allValues,def){
-    if (allValues.tclEnabled != "true") return;
-    if (! v) return "cannot be empty";
-    if (! v.match(/[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*/)
-      && ! v.match(/.*\.local/))
-      return "must be either in the form 192.168.1.1 or xxx.local";
-}
-
-function checkXDR(v,allValues){
-    if (! v) return;
-    let parts=v.split(',');
-    if (parseInt(parts[1])  == 0) return;
-    if (parseInt(parts[1]) != 0 && ! parts[6]) return "missing transducer name";
-    for (let k in allValues){
-        if (! k.match(/^XDR/)) continue;
-        let cmp=allValues[k];
-        if (cmp == v){
-            return "same mapping already defined in "+k;
-        }
-        let cmpParts=cmp.split(',');
-        if (parseInt(cmpParts[1]) != 0 && parts[6] == cmpParts[6]){
-            return "transducer "+parts[6]+" already defined in "+k;
-        }
-        //check similar mappings
-        if (parts[0]==cmpParts[0] && parts[2] == cmpParts[2] && parts[3] == cmpParts[3]){
-            if (parts[4] == cmpParts[4] && parts[5] == cmpParts[5]){
-                return "mapping for the same entity already defined in "+k;
-            }
-            if ((parseInt(parts[4]) == 0 && parseInt(cmpParts[4]) == 1)||
-            (parseInt(parts[4]) == 1 && parseInt(cmpParts[4]) == 0)
-            ){
-                //ignore and single for the same mapping
-                return "mapping for the same entity already defined in "+k;
-            }
-        }
-    }
-}
-function getAllConfigs(omitPass) {
-    let values = document.querySelectorAll('.configForm select , .configForm input');
-    let allValues = {};
-    for (let i = 0; i < values.length; i++) {
-        let v = values[i];
-        let name = v.getAttribute('name');
-        if (!name) continue;
-        if (name.indexOf("_") >= 0) continue;
-        if (v.getAttribute('disabled')) continue;
-        let def = getConfigDefition(name);
-        if (def.type === 'password' && ( v.value == '' || omitPass)) {
-            continue;
-        }
-        let check = v.getAttribute('data-check');
-        if (check) {
-            if (typeof (self[check]) === 'function') {
-                let res = self[check](v.value, allValues, getConfigDefition(name));
-                if (res) {
-                    let value = v.value;
-                    if (v.type === 'password') value = "******";
-                    alert("invalid config for " + v.getAttribute('name') + "(" + value + "):\n" + res);
-                    return;
-                }
-            }
-        }
-        allValues[name] = v.value;
-    }
-    return allValues;
-}
-function changeConfig() {
-    ensurePass()
-        .then(function (pass) {
-            let newAdminPass;
-            let url = "/api/setConfig"
-            let body="_hash="+encodeURIComponent(pass)+"&";
-            let allValues=getAllConfigs();
-            if (!allValues) return;
-            for (let name in allValues){
-                if (name == 'adminPassword'){
-                    newAdminPass=allValues[name];
-                }
-                body += encodeURIComponent(name) + "=" + encodeURIComponent(allValues[name]) + "&";
-            }
-            fetch(url,{
-                method:'POST',
-                headers:{
-                    'Content-Type': 'application/octet-stream' //we must lie here
-                },
-                body: body
-            })
-            .then((rs)=>rs.json())
-                .then(function (status) {
-                    if (status.status == 'OK') {
-                        if (newAdminPass !== undefined) {
-                            forEl('#adminPassInput', function (el) {
-                                el.valu = newAdminPass;
-                            });
-                            saveAdminPass(newAdminPass,true);
-                        }
-                        alertRestart();
-                    }
-                    else {
-                        alert("unable to set config: " + status.status);
-                    }
-                })
-        })
-        .catch(function (e) { alert(e); })
-}
-function factoryReset() {
-    ensurePass()
-        .then(function (hash) {
-            if (!confirm("Really delete all configuration?\n" +
-                "This will reset all your Wifi settings and disconnect you.")) {
-                return;
-            }
-            getJson("/api/resetConfig?_hash="+encodeURIComponent(hash))
-                .then(function (status) {
-                    alertRestart();
-                })
-        })
-        .catch(function (e) { });
-}
-function createCounterDisplay(parent,label,key,isEven){
-    if (parent.querySelector("#"+key)){
-        return;
-    }
-    let clazz="row icon-row counter-row";
-    if (isEven) clazz+=" even";
-    let row=addEl('div',clazz,parent);
-    row.setAttribute("id",key);
-    let icon=addEl('span','icon icon-more',row);
-    addEl('span','label',row,label);
-    let value=addEl('span','value',row,'---');
-    value.setAttribute('id',key+".sumOk");
-    let display=addEl('div',clazz+" msgDetails hidden",parent);
-    display.setAttribute('id',key+".ok");
-    row.addEventListener('click',function(ev){
-        let rs=display.classList.toggle('hidden');
-        if (rs){
-            icon.classList.add('icon-more');
-            icon.classList.remove('icon-less');
-        }
-        else{
-            icon.classList.remove('icon-more');
-            icon.classList.add('icon-less');
-        }
-    });
-}
-function validKey(key){
-    if (! key) return;
-    return key.replace(/[^a-z_:A-Z0-9-]/g,'');
-}
-function updateMsgDetails(key, details) {
-    forEl('.msgDetails', function (frame) {
-        if (frame.getAttribute('id') !== key) return;
-        for (let k in details) {
-            k=validKey(k);
-            let el = frame.querySelector("[data-id=\"" + k + "\"] ");
-            if (!el) {
-                el = addEl('div', 'row', frame);
-                let cv = addEl('span', 'label', el, k);
-                cv = addEl('span', 'value', el, details[k]);
-                cv.setAttribute('data-id', k);
-            }
-            else {
-                el.textContent = details[k];
-            }
-        }
-        forEl('.value',function(el){
-            let k=el.getAttribute('data-id');
-            if (k && ! details[k]){
-                el.parentElement.remove();
-            }
-        },frame);
-    });
-}
-
-function showOverlay(text, isHtml,buttons) {
-    let el = document.getElementById('overlayContent');
-    if (text instanceof Object){
-        el.textContent='';
-        el.appendChild(text);
-    }
-    else {
-        if (isHtml) {
-            el.innerHTML = text;
-            el.classList.remove("text");
-        }
-        else {
-            el.textContent = text;
-            el.classList.add("text");
-        }
-    }
-    buttons=(buttons?buttons:[]).concat([{label:"Close",click:hideOverlay}]);
-    let container = document.getElementById('overlayContainer');
-    let btframe=container.querySelector('.overlayButtons');
-    btframe.textContent='';
-    buttons.forEach((btconfig)=>{
-        let bt=addEl('button','',btframe,btconfig.label);
-        bt.addEventListener("click",btconfig.click);
-    });
-    container.classList.remove('hidden');
-}
-function hideOverlay() {
-    let container = document.getElementById('overlayContainer');
-    container.classList.add('hidden');
-    let el = document.getElementById('overlayContent');
-    el.textContent='';
-}
-function checkChange(el, row,name) {
-    let loaded = el.getAttribute('data-loaded');
-    if (loaded !== undefined) {
-        if (loaded != el.value) {
-            row.classList.add('changed');
-        }
-        else {
-            row.classList.remove("changed");
-        }
-    }
-    let dependend=conditionRelations[name];
-    if (dependend){
-        for (let el in dependend){
-            checkCondition(dependend[el]);
-        }
-    }
-}
-let configDefinitions={};
-let xdrConfig={};
-//a map between the name of a config item and a list of dependend items
-let conditionRelations={};
-function getConfigDefition(name){
-    if (! name) return {};
-    let def;
-    for (let k in configDefinitions){
-        if (configDefinitions[k].name === name){
-            def=configDefinitions[k];
-            break;
-        }
-    }
-    if (! def) return {};
-    return def;
-}
-function getConditions(name){
-    let def=getConfigDefition(name);
-    if (! def) return;
-    let condition=def.condition;
-    if (! condition) return;
-    if (! (condition instanceof Array)) condition=[condition];
-    return condition;
-}
-function checkCondition(element){
-    let name=element.getAttribute('name');
-    let condition=getConditions(name);
-    if (! condition) return;
-    let visible=false;
-    if (! condition instanceof Array) condition=[condition];
-    condition.forEach(function(cel){
-        let lvis=true;
-        for (let k in cel){
-            let item=document.querySelector('[name='+k+']');
-            if (item){
-                let compare=cel[k];
-                if (compare instanceof Array){
-                    if (compare.indexOf(item.value) < 0) lvis=false;
-                }
-                else{
-                    if (item.value != cel[k]) lvis=false;
-                }
-            }
-        }
-        if (lvis) visible=true;
-    });
-    let row=closestParent(element,'row');
-    if (!row) return;
-    if (visible) row.classList.remove('hidden');
-    else row.classList.add('hidden');
-}
-let caliv=0;
-function createCalSetInput(configItem,frame,clazz){
-    let el = addEl('input',clazz,frame);
-    let cb = addEl('button','',frame,'C');
-    //el.disabled=true;
-    cb.addEventListener('click',(ev)=>{
-        let cs=document.getElementById("calset").cloneNode(true);
-        cs.classList.remove("hidden");
-        cs.querySelector(".heading").textContent=configItem.label||configItem.name;
-        let vel=cs.querySelector(".val");
-        if (caliv != 0) window.clearInterval(caliv);
-        caliv=window.setInterval(()=>{
-            if (document.body.contains(cs)){
-                fetch("/api/calibrate?name="+encodeURIComponent(configItem.name))
-                .then((r)=>r.text())
-                .then((txt)=>{
-                    if (txt != vel.textContent){
-                        vel.textContent=txt;
-                    }
-                })
-                .catch((e)=>{
-                    alert(e);
-                    hideOverlay();
-                    window.clearInterval(caliv);
-                })
-            }
-            else{
-                window.clearInterval(caliv);
-            }
-        },200);        
-        showOverlay(cs,false,[{label:'Set',click:()=>{
-            el.value=vel.textContent;
-            let cev=new Event('change');
-            el.dispatchEvent(cev);
-        }}]);
-    })
-    el.setAttribute('name', configItem.name)
-    return el;    
-}
-function createCalValInput(configItem,frame,clazz){
-    let el = addEl('input',clazz,frame);
-    let cb = addEl('button','',frame,'C');
-    //el.disabled=true;
-    cb.addEventListener('click',(ev)=>{
-        const sv=function(val,cfg){
-            if (configItem.eval){
-                let v=parseFloat(val);
-                let c=parseFloat(cfg);
-                return(eval(configItem.eval));
-            }
-            return v;
-        };
-        let cs=document.getElementById("calval").cloneNode(true);
-        cs.classList.remove("hidden");
-        cs.querySelector(".heading").textContent=configItem.label||configItem.name;
-        let vel=cs.querySelector(".val");
-        let vinp=cs.querySelector("input");
-        vinp.value=el.value;
-        if (caliv != 0) window.clearInterval(caliv);
-        caliv=window.setInterval(()=>{
-            if (document.body.contains(cs)){
-                fetch("/api/calibrate?name="+encodeURIComponent(configItem.name))
-                .then((r)=>r.text())
-                .then((txt)=>{
-                    txt=sv(txt,vinp.value);
-                    if (txt != vel.textContent){
-                        vel.textContent=txt;
-                    }
-                })
-                .catch((e)=>{
-                    alert(e);
-                    hideOverlay();
-                    window.clearInterval(caliv);
-                })
-            }
-            else{
-                window.clearInterval(caliv);
-            }
-        },200);        
-        showOverlay(cs,false,[{label:'Set',click:()=>{
-            el.value=vinp.value;
-            let cev=new Event('change');
-            el.dispatchEvent(cev);
-        }}]);
-    })
-    el.setAttribute('name', configItem.name)
-    return el;    
-}
-function createInput(configItem, frame,clazz) {
-    let el;
-    if (configItem.type === 'boolean' || configItem.type === 'list' || configItem.type == 'boatData') {
-        el=addEl('select',clazz,frame);
-        if (configItem.readOnly) el.setAttribute('disabled',true);
-        el.setAttribute('name', configItem.name)
-        let slist = [];
-        if (configItem.list) {
-            configItem.list.forEach(function (v) {
-                if (v instanceof Object){
-                    slist.push({l:v.l,v:v.v});
-                }
-                else{
-                    slist.push({ l: v, v: v });
-                }
-            })
-        }
-        else if (configItem.type != 'boatData') {
-            slist.push({ l: 'on', v: 'true' })
-            slist.push({ l: 'off', v: 'false' })
-        }
-        slist.forEach(function (sitem) {
-            let sitemEl = addEl('option','',el,sitem.l);
-            sitemEl.setAttribute('value', sitem.v);
-        })
-        if (configItem.type == 'boatData'){
-            el.classList.add('boatDataSelect');
+            clazz.forEach(function (ce) {
+                el.classList.add(ce);
+            });
         }
+        if (text) el.textContent = text;
+        if (parent) parent.appendChild(el);
         return el;
     }
-    if (configItem.type === 'filter') {
-        return createFilterInput(configItem,frame,clazz);    
+    function forEl(query, callback, base) {
+        if (!base) base = document;
+        let all = base.querySelectorAll(query);
+        for (let i = 0; i < all.length; i++) {
+            callback(all[i]);
+        }
     }
-    if (configItem.type === 'xdr'){
-        return createXdrInput(configItem,frame,clazz);
+    function closestParent(element, clazz) {
+        while (true) {
+            let parent = element.parentElement;
+            if (!parent) return;
+            if (parent.classList.contains(clazz)) return parent;
+            element = parent;
+        }
     }
-    if (configItem.type === "calset"){
-        return createCalSetInput(configItem,frame,clazz);
+    function alertRestart() {
+        reloadConfig = true;
+        alert("Board reset triggered, reconnect WLAN if necessary");
     }
-    if (configItem.type === "calval"){
-        return createCalValInput(configItem,frame,clazz);
+    function getJson(url) {
+        return fetch(url)
+            .then(function (r) { return r.json() });
     }
-    el = addEl('input',clazz,frame);
-    if (configItem.readOnly) el.setAttribute('disabled',true);
-    el.setAttribute('name', configItem.name)
-    if (configItem.type === 'password') {
-        el.setAttribute('type', 'password');
-        let vis = addEl('span', 'icon-eye icon', frame);
-        vis.addEventListener('click', function (v) {
-            if (vis.classList.toggle('active')) {
-                el.setAttribute('type', 'text');
+    function getText(url) {
+        return fetch(url)
+            .then(function (r) { return r.text() });
+    }
+    function reset() {
+        ensurePass()
+            .then(function (hash) {
+                fetch('/api/reset?_hash=' + encodeURIComponent(hash));
+                alertRestart();
+            })
+            .catch(function (e) { });
+    }
+    function update() {
+        let now = (new Date()).getTime();
+        let ce = document.getElementById('connected');
+        if (ce) {
+            if ((lastUpdate + 3000) > now) {
+                ce.classList.add('ok');
             }
             else {
-                el.setAttribute('type', 'password');
+                ce.classList.remove('ok');
             }
-        });
-    }
-    else if (configItem.type === 'number') {
-        el.setAttribute('type', 'number');
-    }
-    else {
-        el.setAttribute('type', 'text');
-    }
-    return el;
-}
-
-function setSelect(item,value){
-    if (!item) return;
-    item.value=value;
-    if (item.value !== value){
-        //missing option with his value
-        let o=addEl('option',undefined,item,value);
-        o.setAttribute('value',value);
-        item.value=value;
-    }
-}
-
-function updateSelectList(item,slist,opt_keepValue){
-    let idx=0;
-    let value;
-    if (opt_keepValue) value=item.value;
-    item.innerHTML='';
-    slist.forEach(function (sitem) {
-        let sitemEl = addEl('option','',item,sitem.l);
-        sitemEl.setAttribute('value', sitem.v !== undefined?sitem.v:idx);
-        idx++;
-    })
-    if (value !== undefined){
-        setSelect(item,value);
-    }
-}
-function getXdrCategories(){
-    let rt=[];
-    for (let c in xdrConfig){
-        if (xdrConfig[c].enabled !== false){
-            rt.push({l:c,v:xdrConfig[c].id});
         }
-    }
-    return rt;
-}
-function getXdrCategoryName(cid){
-    category=parseInt(cid);
-    for (let c in xdrConfig){
-        let base=xdrConfig[c];
-        if (parseInt(base.id) == category){
-            return c;
-        }
-    }
-}
-function getXdrCategory(cid){
-    category=parseInt(cid);
-    for (let c in xdrConfig){
-        let base=xdrConfig[c];
-        if (parseInt(base.id) == category){
-            return base;
-        }
-    }
-    return {};
-}
-function getXdrSelectors(category){
-    let base=getXdrCategory(category);
-    return base.selector || [];
-}
-function getXdrFields(category){
-    let base=getXdrCategory(category);
-    if (!base.fields) return [];
-    let rt=[];
-    base.fields.forEach(function(f){
-        if (f.t === undefined) return;
-        if (parseInt(f.t) == 99) return; //unknown type
-        rt.push(f);
-    });
-    return rt;
-}
-
-function createXdrLine(parent,label){
-    let d=addEl('div','xdrline',parent);
-    addEl('span','xdrlabel',d,label);
-    return d;
-}
-function showHideXdr(el,show,useParent){
-    if (useParent) el=el.parentElement;
-    if (show) el.classList.remove('xdrunused');
-    else el.classList.add('xdrunused');
-}
-
-function createXdrInput(configItem,frame){
-    let configCategory=configItem.category;
-    let el = addEl('div','xdrinput',frame);
-    let d=createXdrLine(el,'Direction');
-    let direction=createInput({
-        type:'list',
-        name: configItem.name+"_dir",
-        list: [
-            //GwXDRMappingDef::Direction
-            {l:'off',v:0},
-            {l:'bidir',v:1},
-            {l:'to2K',v:2},
-            {l:'from2K',v:3}
-        ],
-        readOnly: configItem.readOnly
-    },d,'xdrdir');
-    d=createXdrLine(el,'Category');
-    let category=createInput({
-        type: 'list',
-        name: configItem.name+"_cat",
-        list:getXdrCategories(),
-        readOnly: configItem.readOnly
-    },d,'xdrcat');
-    d=createXdrLine(el,'Source');
-    let selector=createInput({
-        type: 'list',
-        name: configItem.name+"_sel",
-        list:[],
-        readOnly: configItem.readOnly
-    },d,'xdrsel');
-    d=createXdrLine(el,'Field');
-    let field=createInput({
-        type:'list',
-        name: configItem.name+'_field',
-        list: [],
-        readOnly: configItem.readOnly
-    },d,'xdrfield');
-    d=createXdrLine(el,'Instance');
-    let imode=createInput({
-        type:'list',
-        name: configItem.name+"_imode",
-        list:[
-            //GwXDRMappingDef::InstanceMode
-            {l:'single',v:0},
-            {l:'ignore',v:1},
-            {l:'auto',v:2}
-        ],
-        readOnly: configItem.readOnly
-    },d,'xdrimode');
-    let instance=createInput({
-        type:'number',
-        name: configItem.name+"_instance",
-        readOnly: configItem.readOnly
-    },d,'xdrinstance');
-    d=createXdrLine(el,'Transducer');
-    let xdrName=createInput({
-        type:'text',
-        name: configItem.name+"_xdr",
-        readOnly: configItem.readOnly
-    },d,'xdrname');
-    d=createXdrLine(el,'Example');
-    let example=addEl('div','xdrexample',d,'');
-    let data = addEl('input','xdrvalue',el);
-    data.setAttribute('type', 'hidden');
-    data.setAttribute('name', configItem.name);
-    if (configItem.readOnly) data.setAttribute('disabled',true);
-    let changeFunction = function () {
-        let parts=data.value.split(',');
-        direction.value=parts[1] || 0;
-        category.value=parts[0] || 0;
-        let selectors=getXdrSelectors(category.value);
-        updateSelectList(selector,selectors);
-        showHideXdr(selector,selectors.length>0);
-        let fields=getXdrFields(category.value);
-        updateSelectList(field,fields);
-        showHideXdr(field,fields.length>0);
-        selector.value=parts[2]||0;
-        field.value=parts[3]||0;
-        imode.value=parts[4]||0;
-        instance.value=parts[5]||0;
-        showHideXdr(instance,imode.value == 0);
-        xdrName.value=parts[6]||'';
-        let used=isXdrUsed(data);
-        let modified=data.value != data.getAttribute('data-loaded');
-        forEl('[data-category='+configCategory+']',function(el){
-            if (used) {
-                el.classList.add('xdrcused');
-                el.classList.remove('xdrcunused');
-                forEl('.categoryAdd',function(add){
-                    add.textContent=xdrName.value;
-                },el);
-            }
-            else {
-                el.classList.remove('xdrcused');
-                el.classList.add('xdrcunused');
-                forEl('.categoryAdd',function(add){
-                    add.textContent='';
-                },el);
-            }
-            if (modified){
-                el.classList.add('changed');
-            }
-            else{
-                el.classList.remove('changed');
-            }
-        });
-        if (used){
-            getText('/api/xdrExample?mapping='+encodeURIComponent(data.value)+'&value=2.1')
-                .then(function(txt){
-                    example.textContent=txt;
-                })
-        }
-        else{
-            example.textContent='';
-        }
-    }
-    let updateFunction = function (evt) {
-        if (evt.target == category){
-            selector.value=0;
-            field.value=0;
-            instance.value=0;
-        }
-        let txt=category.value+","+direction.value+","+
-            selector.value+","+field.value+","+imode.value;
-        let instanceValue=parseInt(instance.value||0);
-        if (isNaN(instanceValue)) instanceValue=0;
-        if (instanceValue<0) instanceValue=0;
-        if (instanceValue>255) instanceValue=255;
-        txt+=","+instanceValue;
-        let xdr=xdrName.value.replace(/[^a-zA-Z0-9]/g,'');
-        xdr=xdr.substr(0,12);
-        txt+=","+xdr;    
-        data.value=txt;
-        let ev=new Event('change');
-        data.dispatchEvent(ev);
-    }
-    category.addEventListener('change',updateFunction);
-    direction.addEventListener('change',updateFunction);
-    selector.addEventListener('change',updateFunction);
-    field.addEventListener('change',updateFunction);
-    imode.addEventListener('change',updateFunction);
-    instance.addEventListener('change',updateFunction);
-    xdrName.addEventListener('change',updateFunction);
-    data.addEventListener('change',changeFunction);
-    return data;
-}
-
-function isXdrUsed(element){
-    let parts=element.value.split(',');
-    if (! parts[1]) return false;
-    if (! parseInt(parts[1])) return false;
-    if (! parts[6]) return false;
-    return true;
-}
-
-function createFilterInput(configItem, frame) {
-    let el = addEl('div','filter',frame);
-    let ais = createInput({
-        type: 'list',
-        name: configItem.name + "_ais",
-        list: ['aison', 'aisoff'],
-        readOnly: configItem.readOnly
-    }, el);
-    let mode = createInput({
-        type: 'list',
-        name: configItem.name + "_mode",
-        list: ['whitelist', 'blacklist'],
-        readOnly: configItem.readOnly
-    }, el);
-    let sentences = createInput({
-        type: 'text',
-        name: configItem.name + "_sentences",
-        readOnly: configItem.readOnly
-    }, el);
-    let data = addEl('input',undefined,el);
-    data.setAttribute('type', 'hidden');
-    let changeFunction = function () {
-        let cv = data.value || "";
-        let parts = cv.split(":");
-        ais.value = (parts[0] == '0') ? "aisoff" : "aison";
-        mode.value = (parts[1] == '0') ? "whitelist" : "blacklist";
-        sentences.value = parts[2] || "";
-    }
-    let updateFunction = function () {
-        let nv = (ais.value == 'aison') ? "1" : "0";
-        nv += ":";
-        nv += (mode.value == 'blacklist') ? "1" : "0";
-        nv += ":";
-        nv += sentences.value;
-        data.value = nv;
-        let chev = new Event('change');
-        data.dispatchEvent(chev);
-    }
-    mode.addEventListener('change', updateFunction);
-    ais.addEventListener("change", updateFunction);
-    sentences.addEventListener("change", updateFunction);
-    data.addEventListener('change', function (ev) {
-        changeFunction();
-    });
-    data.setAttribute('name', configItem.name);
-    if (configItem.readOnly) data.setAttribute('disabled',true);
-    return data;
-}
-let moreicons=['icon-more','icon-less'];
-
-function collapseCategories(parent,expand){
-    let doExpand=expand;
-    forEl('.category',function(cat){
-        if (typeof(expand) === 'function') doExpand=expand(cat);
-        forEl('.content',function(content){
-            if (doExpand){
-                content.classList.remove('hidden');
-            }
-            else{
-                content.classList.add('hidden');
-            }
-        },cat);
-        forEl('.title .icon',function(icon){
-            toggleClass(icon,doExpand?1:0,moreicons);    
-        },cat);
-    },parent);
-}
-
-function findFreeXdr(data){
-    let page=document.getElementById('xdrPage');
-    let el=undefined;
-    collapseCategories(page,function(cat){
-        if (el) return false;
-        let vEl=cat.querySelector('.xdrvalue');
-        if (!vEl) return false;
-        if (isXdrUsed(vEl)) return false;
-        el=vEl;
-        if (data){
-            el.value=data;
-            let ev=new Event('change');
-            el.dispatchEvent(ev);
-            window.setTimeout(function(){
-                cat.scrollIntoView();
-            },50);
-        }
-        return true;
-    });
-}
-
-function convertUnassigned(value){
-    let rt={};
-    value=parseInt(value);
-    if (isNaN(value)) return;
-    //see GwXDRMappings::addUnknown
-    let instance=value & 0x1ff;
-    value = value >> 9;
-    let field=value & 0x7f;
-    value = value >> 7;
-    let selector=value & 0x7f;
-    value = value >> 7;
-    let cid=value & 0x7f;
-    let category=getXdrCategory(cid);
-    let cname=getXdrCategoryName(cid);
-    if (! cname) return rt;
-    let fieldName="";
-    let idx=0;
-    (category.fields || []).forEach(function(f){
-        if (f.v === undefined){
-            if (idx == field) fieldName=f.l;
-        }
-        else{
-            if (parseInt(f.v) == field) fieldName=f.l;
-        }
-        idx++;
-    });
-    let selectorName=selector+"";
-    (category.selector ||[]).forEach(function(s){
-        if (parseInt(s.v) == selector) selectorName=s.l;
-    });
-    rt.l=cname+","+selectorName+","+fieldName+","+instance;
-    rt.v=cid+",1,"+selector+","+field+",0,"+instance+",";
-    return rt;
-}
-
-function unassignedAdd(ev) {
-    let dv = ev.target.getAttribute('data-value');
-    if (dv) {
-        findFreeXdr(dv);
-        hideOverlay();
-    }
-}
-function loadUnassigned(){
-    getText("/api/xdrUnmapped")
-        .then(function(txt){
-            let ot="";
-            txt.split('\n').forEach(function(line){
-                let cv=convertUnassigned(line);
-                if (!cv || !cv.l) return;
-                ot+='<div class="xdrunassigned"><span>'+
-                    cv.l+'</span>'+
-                    '<button class="addunassigned" data-value="'+
-                    cv.v+
-                    '">+</button></div>';
-            })
-            showOverlay(ot,true);
-            forEl('.addunassigned',function(bt){
-                bt.onclick=unassignedAdd;
-            });
-        })
-}
-function showXdrHelp(){
-    let helpContent=document.getElementById('xdrHelp');
-    if (helpContent){
-        showOverlay(helpContent.innerHTML,true);
-    }
-}
-function formatDateForFilename(usePrefix,d){
-    let rt="";
-    if (usePrefix){
-        let fwt=document.querySelector('.status-fwtype');
-        if (fwt) rt=fwt.textContent;
-        rt+="_";
-    }
-    if (! d) d=new Date();
-    [d.getFullYear(),d.getMonth(),d.getDate(),d.getHours(),d.getMinutes(),d.getSeconds()]
-        .forEach(function(v){
-            if (v < 10) rt+="0"+v;
-            else rt+=""+v;
-        })
-    return rt;
-}
-function downloadData(data,name){
-    let url="data:application/octet-stream,"+encodeURIComponent(JSON.stringify(data,undefined,2));
-    let target=document.getElementById('downloadXdr');
-    if (! target) return;
-    target.setAttribute('href',url);
-    target.setAttribute('download',name);
-    target.click();
-}
-function exportConfig(){
-    let data=getAllConfigs(true);
-    if (! data) return;
-    downloadData(data,formatDateForFilename(true)+".json");
-}
-function exportXdr(){
-    let data={};
-    forEl('.xdrvalue',function(el) {
-        let name=el.getAttribute('name');
-        let value=el.value;
-        let err=checkXDR(value,data);
-        if (err){
-            alert("error in "+name+": "+value+"\n"+err);
-            return;
-        }
-        data[name]=value;
-    })
-    downloadData(data,"xdr"+formatDateForFilename(true)+".json");
-}
-function importJson(opt_keyPattern){
-    let clazz='importJson';
-    forEl('.'+clazz,function(ul){
-        ul.remove();
-    });
-    let ip=addEl('input',clazz,document.body);
-    ip.setAttribute('type','file');
-    ip.addEventListener('change',function(ev){
-        if (ip.files.length > 0){
-            let f=ip.files[0];
-            let reader=new FileReader();
-            reader.onloadend=function(status){
-                try{
-                    let idata=JSON.parse(reader.result);
-                    let hasOverwrites=false;
-                    for (let k in idata){
-                        if (opt_keyPattern && ! k.match(opt_keyPattern)){
-                            alert("file contains invalid key "+k);
-                            return;
-                        }
-                        let del=document.querySelector('[name='+k+']');
-                        if (del){
-                            hasOverwrites=true;
-                        }        
+        getJson('/api/status')
+            .then(function (jsonData) {
+                let statusPage = document.getElementById('statusPageContent');
+                let even = true; //first counter
+                for (let k in jsonData) {
+                    if (k == "salt") {
+                        lastSalt = jsonData[k];
+                        continue;
                     }
-                    if (hasOverwrites){
-                        if (!confirm("overwrite existing data?")) return;
+                    if (k == "minUser") {
+                        minUser = parseInt(jsonData[k]);
+                        continue;
                     }
-                    for (let k in idata){
-                        let del=document.querySelector('[name='+k+']');
-                        if (del){
-                            if (del.tagName === 'SELECT'){
-                                setSelect(del,idata[k]);
-                            }
-                            else{
-                                del.value=idata[k];
-                            }
-                            let ev=new Event('change');
-                            del.dispatchEvent(ev);
-                        }
-                    }
-                }catch (error){
-                    alert("unable to parse upload: "+error);
-                    return;
-                }
-            };
-            reader.readAsBinaryString(f);
-        }
-        ip.remove();
-    });
-    ip.click(); 
-}
-function importXdr(){
-    importJson(new RegExp(/^XDR[0-9][0-9]*/));
-}
-function importConfig(){
-    importJson();
-}
-function toggleClass(el,id,classList){
-    let nc=classList[id];
-    let rt=false;
-    if (nc && !el.classList.contains(nc)) rt=true;
-    for (let i in classList){
-        if (i == id) continue;
-        el.classList.remove(classList[i])
-    }
-    if (nc) el.classList.add(nc);
-    return rt;
-}
-
-function createConfigDefinitions(parent, capabilities, defs,includeXdr) {
-    let categories={};
-    let frame = parent.querySelector('.configFormRows');
-    if (!frame) throw Error("no config form");
-    frame.innerHTML = '';
-    configDefinitions = defs;
-    let currentCategoryPopulated=true;
-    defs.forEach(function (item) {
-        if (!item.type) return;
-        if (item.category.match(/^xdr/)){
-            if (! includeXdr) return;
-        }
-        else{
-            if(includeXdr) return;
-        }
-        let catEntry;
-        if (categories[item.category] === undefined){
-            catEntry={
-                populated:false,
-                frame: undefined,
-                element: undefined
-            }
-            categories[item.category]=catEntry
-            catEntry.frame = addEl('div', 'category', frame);
-            catEntry.frame.setAttribute('data-category',item.category)
-            let categoryTitle = addEl('div', 'title', catEntry.frame);
-            let categoryButton = addEl('span', 'icon icon-more', categoryTitle);
-            addEl('span', 'label', categoryTitle, item.category);
-            addEl('span','categoryAdd',categoryTitle);
-            catEntry.element = addEl('div', 'content', catEntry.frame);
-            catEntry.element.classList.add('hidden');
-            categoryTitle.addEventListener('click', function (ev) {
-                let rs = catEntry.element.classList.toggle('hidden');
-                if (rs) {
-                    toggleClass(categoryButton,0,moreicons);
-                }
-                else {
-                    toggleClass(categoryButton,1,moreicons);
-                }
-            })
-        }
-        else{
-            catEntry=categories[item.category];
-        }
-        let showItem=true;
-        let itemCapabilities=item.capabilities||{};
-        itemCapabilities['HIDE'+item.name]=null;
-        for (let capability in itemCapabilities) {
-            let values = itemCapabilities[capability];
-            let found = false;
-            if (! (values instanceof Array)) values=[values];
-            values.forEach(function (v) {
-                if ( v === null){
-                    if (capabilities[capability] === undefined) found=true;
-                }
-                else{
-                    if (capabilities[capability] == v) found = true;
-                }
-            });
-            if (!found) showItem=false;
-        }
-        let readOnly=false;
-        let mode=capabilities['CFGMODE'+item.name];
-        if (mode == 1) {
-            //hide
-            showItem=false;
-        }
-        if (mode == 2){
-            readOnly=true;
-        }
-        if (showItem) {
-            item.readOnly=readOnly;
-            catEntry.populated=true;
-            let row = addEl('div', 'row', catEntry.element);
-            let label = item.label || item.name;
-            addEl('span', 'label', row, label);
-            let valueFrame = addEl('div', 'value', row);
-            let valueEl = createInput(item, valueFrame);
-            if (!valueEl) return;
-            valueEl.setAttribute('data-default', item.default);
-            if (! readOnly) valueEl.addEventListener('change', function (ev) {
-                let el = ev.target;
-                checkChange(el, row, item.name);
-            })
-            let condition = getConditions(item.name);
-            if (condition) {
-                condition.forEach(function (cel) {
-                    for (let c in cel) {
-                        if (!conditionRelations[c]) {
-                            conditionRelations[c] = [];
-                        }
-                        conditionRelations[c].push(valueEl);
-                    }
-                })
-            }
-            if (item.check) valueEl.setAttribute('data-check', item.check);
-            let btContainer = addEl('div', 'buttonContainer', row);
-            if (!readOnly) {
-                let bt = addEl('button', 'defaultButton', btContainer, 'X');
-                bt.setAttribute('data-default', item.default);
-                bt.addEventListener('click', function (ev) {
-                    valueEl.value = valueEl.getAttribute('data-default');
-                    let changeEvent = new Event('change');
-                    valueEl.dispatchEvent(changeEvent);
-                })
-            }
-            bt = addEl('button', 'infoButton', btContainer, '?');
-            bt.addEventListener('click', function (ev) {
-                if (item.description) {
-                    showOverlay(item.description);
-                }
-                else {
-                    if (item.category.match(/^xdr/)) {
-                        showXdrHelp();
-                    }
-                }
-            });
-        }
-    });
-    for (let cat in categories){
-        let catEntry=categories[cat];
-        if (! catEntry.populated){
-            catEntry.frame.remove();
-        }
-    }
-}
-function loadConfigDefinitions() {
-    getJson("api/capabilities")
-        .then(function (capabilities) {
-            if (capabilities.HELP_URL){
-                let el=document.getElementById('helpButton');
-                if (el) el.setAttribute('data-url',capabilities.HELP_URL);
-            }
-            getJson("config.json")
-                .then(function (defs) {
-                    getJson("xdrconfig.json")
-                        .then(function(xdr){
-                            xdrConfig=xdr;
-                            configDefinitions=defs;
-                            let normalConfig=document.getElementById('configPage');
-                            let xdrParent=document.getElementById('xdrPage');
-                            if (normalConfig) createConfigDefinitions(normalConfig,capabilities,defs,false);
-                            if (xdrParent) createConfigDefinitions(xdrParent,capabilities,defs,true);
-                            resetForm();
-                            getText('api/boatDataString').then(function (data) {
-                                updateDashboard(data.split('\n'));
-                            });
-                        })
-                })
-        })
-        .catch(function (err) { alert("unable to load config: " + err) })
-}
-function verifyPass(pass){
-    return new Promise(function(resolve,reject){
-        let hash=lastSalt+pass;
-        let md5hash=MD5(hash);
-        getJson('api/checkPass?hash='+encodeURIComponent(md5hash))
-            .then(function(jsonData){
-                if (jsonData.status == 'OK') resolve(md5hash);
-                else reject(jsonData.status);
-                return;
-            })
-            .catch(function(error){reject(error);})
-    });
-}
-
-
-function adminPassCancel(){
-    forEl('#adminPassOverlay',function(el){el.classList.add('hidden')});
-    forEl('#adminPassInput',function(el){el.value=''});
-}
-function saveAdminPass(pass,forceIfSet){
-    forEl('#adminPassKeep',function(el){
-        try{
-            let old=localStorage.getItem('adminPass');
-            if (el.value == 'true' || (forceIfSet && old !== undefined)){
-                localStorage.setItem('adminPass',pass);
-            }
-            else{
-                localStorage.removeItem('adminPass');
-            }
-        }catch (e){}
-    });
-}
-function forgetPass(){
-    localStorage.removeItem('adminPass');
-    forEl('#adminPassInput',function(el){
-        el.value='';
-    });
-}
-function ensurePass(){
-    return new Promise(function(resolve,reject){
-        if (! needAdminPass) {
-            resolve('');
-            return;
-        }
-        let pe=document.getElementById('adminPassInput');
-        let hint=document.getElementById('adminPassError');
-        if (!pe) {
-            reject('no input');
-            return;
-        }
-        if (pe.value == ''){
-            let ok=document.getElementById('adminPassOk');
-            if (!ok) {
-                reject('no button');
-                return;
-            }
-            ok.onclick=function(){
-                verifyPass(pe.value)
-                    .then(function(pass){
-                        forEl('#adminPassOverlay',function(el){el.classList.add('hidden')});
-                        saveAdminPass(pe.value);
-                        resolve(pass);
-                    })
-                    .catch(function(err){
-                        if (hint){
-                            hint.textContent="invalid password";
-                        }
-                    });
-            };
-            if (hint) hint.textContent='';
-            forEl('#adminPassOverlay',function(el){el.classList.remove('hidden')});
-        }
-        else{
-            verifyPass(pe.value)
-                .then(function(np){resolve(np);})
-                .catch(function(err){
-                    pe.value='';
-                    ensurePass()
-                        .then(function(p){resolve(p);})
-                        .catch(function(e){reject(e);});
-                });
-            return;    
-        }
-    });
-}
-function converterInfo() {
-    getJson("api/converterInfo").then(function (json) {
-        let text = "<h3>Converted entities</h3>";
-        text += "<p><b>NMEA0183 to NMEA2000:</b><br/>";
-        text += "   " + (json.nmea0183 || "").replace(/,/g, ", ");
-        text += "</p>";
-        text += "<p><b>NMEA2000 to NMEA0183:</b><br/>";
-        text += "   " + (json.nmea2000 || "").replace(/,/g, ", ");
-        text += "</p>";
-        showOverlay(text, true);
-    });
-}
-function handleTab(el) {
-    let activeName = el.getAttribute('data-page');
-    if (!activeName) {
-        let extUrl= el.getAttribute('data-url');
-        if (! extUrl) return;
-        window.open(extUrl,el.getAttribute('data-window')||'_');
-    }
-    let activeTab = document.getElementById(activeName);
-    if (!activeTab) return;
-    let all = document.querySelectorAll('.tabPage');
-    for (let i = 0; i < all.length; i++) {
-        all[i].classList.add('hidden');
-    }
-    let tabs = document.querySelectorAll('.tab');
-    for (let i = 0; i < all.length; i++) {
-        tabs[i].classList.remove('active');
-    }
-    el.classList.add('active');
-    activeTab.classList.remove('hidden');
-}
-/**
- *
- * @param {number} coordinate
- * @param axis
- * @returns {string}
- */
- function formatLonLatsDecimal(coordinate,axis){
-    coordinate = (coordinate+540)%360 - 180; // normalize for sphere being round
-
-    let abscoordinate = Math.abs(coordinate);
-    let coordinatedegrees = Math.floor(abscoordinate);
-
-    let coordinateminutes = (abscoordinate - coordinatedegrees)/(1/60);
-    let numdecimal=2;
-    //correctly handle the toFixed(x) - will do math rounding
-    if (coordinateminutes.toFixed(numdecimal) == 60){
-        coordinatedegrees+=1;
-        coordinateminutes=0;
-    }
-    if( coordinatedegrees < 10 ) {
-        coordinatedegrees = "0" + coordinatedegrees;
-    }
-    if (coordinatedegrees < 100 && axis == 'lon'){
-        coordinatedegrees = "0" + coordinatedegrees;
-    }
-    let str = coordinatedegrees + "\u00B0";
-
-    if( coordinateminutes < 10 ) {
-        str +="0";
-    }
-    str += coordinateminutes.toFixed(numdecimal) + "'";
-    if (axis == "lon") {
-        str += coordinate < 0 ? "W" :"E";
-    } else {
-        str += coordinate < 0 ? "S" :"N";
-    }
-    return str;
-};
-function formatFixed(v,dig,fract){
-    v=parseFloat(v);
-    if (dig === undefined) return v.toFixed(fract);
-    let s=v<0?"-":"";
-    v=Math.abs(v);
-    let rv=v.toFixed(fract);
-    let parts=rv.split('.');
-    parts[0]="0000000000"+parts[0];
-    if (dig >= 10) dig=10;
-    if (fract > 0){
-        return s+parts[0].substr(parts[0].length-dig)+"."+parts[1];
-    }
-    return s+parts[0].substr(parts[0].length-dig);
-}
-let valueFormatters = {
-    formatCourse: {
-        f: function (v) {
-            let x = parseFloat(v);
-            let rt = x * 180.0 / Math.PI;
-            if (rt > 360) rt -= 360;
-            if (rt < 0) rt += 360;
-            return rt.toFixed(0);
-        },
-        u: '°'
-    },
-    formatKnots: {
-        f: function (v) {
-            let x = parseFloat(v);
-            x = x * 3600.0 / 1852.0;
-            return x.toFixed(2);
-        },
-        u: 'kn'
-    },
-    formatWind: {
-        f: function (v) {
-            let x = parseFloat(v);
-            x = x * 180.0 / Math.PI;
-            if (x > 180) x = -1 * (360 - x);
-            return x.toFixed(0);
-        },
-        u: '°'
-    },
-    mtr2nm: {
-        f: function (v) {
-            let x = parseFloat(v);
-            x = x / 1852.0;
-            return x.toFixed(2);
-        },
-        u: 'nm'
-    },
-    kelvinToC: {
-        f: function (v) {
-            let x = parseFloat(v);
-            x = x - 273.15;
-            return x.toFixed(0);
-        },
-        u: '°'
-    },
-    formatFixed0: {
-        f: function (v) {
-            let x = parseFloat(v);
-            return x.toFixed(0);
-        },
-        u: ''
-    },
-    formatDepth: {
-        f: function (v) {
-            let x = parseFloat(v);
-            return x.toFixed(1);
-        },
-        u: 'm'
-    },
-    formatLatitude: {
-        f: function (v) {
-            let x = parseFloat(v);
-            if (isNaN(x)) return '-----';
-            return formatLonLatsDecimal(x,'lat');
-        },
-        u: '°'
-    },
-    formatLongitude: {
-        f: function (v) {
-            let x = parseFloat(v);
-            if (isNaN(x)) return '-----';
-            return formatLonLatsDecimal(x,'lon');
-        },
-        u: ''
-    },
-    formatRot:{
-        f: function(v){
-            let x=parseFloat(v);
-            if (isNaN(x)) return '---';
-            x = x * 180.0 / Math.PI;
-            return x.toFixed(2);
-        },
-        u:'°/s'
-    },
-    formatXte:{
-        f: function(v){
-            let x=parseFloat(v);
-            if (isNaN(x)) return '---';
-            return x.toFixed(0);
-        },
-        u:'m'
-    },
-    formatDate:{
-        f: function(v){
-            v=parseFloat(v);
-            if (isNaN(v)) return "----/--/--";
-            //strange day offset from NMEA0183 lib
-            let d=new Date("2010/01/01");
-            let days=14610-d.getTime()/1000/86400;
-            let tbase=(v-days)*1000*86400;
-            let od=new Date(tbase);
-            return formatFixed(od.getFullYear(),4,0)+
-                "/"+formatFixed(od.getMonth()+1,2,0)+
-                "/"+formatFixed(od.getDate(),2,0);
-        },
-        u:''
-    },
-    formatTime:{
-        f:function(v){
-            v=parseFloat(v);
-            if (isNaN(v)) return "--:--:--";
-            let hr=Math.floor(v/3600.0);
-            let min=Math.floor((v-hr*3600.0)/60);
-            let s=Math.floor((v-hr*3600.0-min*60.0));
-            return formatFixed(hr,2,0)+':'+
-                formatFixed(min,2,0)+':'+
-                formatFixed(s,2,0);
-        },
-        u:''
-    }
-
-
-}
-function resizeFont(el,reset,maxIt){
-    if (maxIt === undefined) maxIt=10;
-    if (! el) return;
-    if (reset) el.style.fontSize='';
-    while (el.scrollWidth > el.clientWidth && maxIt){
-        let next=parseFloat(window.getComputedStyle(el).fontSize)*0.9;
-        el.style.fontSize=next+"px";
-    }
-}
-function createDashboardItem(name, def, parent) {
-    if (! def.name) return;
-    let frame = addEl('div', 'dash', parent);
-    let title = addEl('span', 'dashTitle', frame, name);
-    let value = addEl('span', 'dashValue', frame);
-    value.setAttribute('id', 'data_' + name);
-    let fmt=valueFormatters[def.format];
-    if (def.format) value.classList.add(def.format);
-    let footer = addEl('div','footer',frame);
-    let src= addEl('span','source',footer);
-    src.setAttribute('id','source_'+name);
-    let u=fmt?fmt.u:' ';
-    if (! fmt && def.format && def.format.match(/formatXdr/)){
-        u=def.format.replace(/formatXdr:[^:]*:/,'');
-    }
-    addEl('span','unit',footer,u);
-    return value;
-}
-function parseBoatDataLine(line){
-    let rt={};
-    let parts=line.split(',');
-    rt.name=parts[0];
-    rt.valid=parts[2] === '1';
-    rt.update=parseInt(parts[3]);
-    rt.source=parseInt(parts[4]);
-    rt.format=parts[1];
-    rt.value=parts[5];
-    return rt;
-}
-function createDashboard() {
-    let frame = document.getElementById('dashboardPage');
-    if (!frame) return;
-    frame.innerHTML = '';
-}
-function sourceName(v){
-    if (v == 0) return "N2K";
-    for (let n in channelList){
-        if (v == channelList[n].id) return n;
-        if (v >= channelList[n].id && v <= channelList[n].max){
-            return n;
-        }
-    }
-    if (v < minUser) return "---";
-    return "USER["+v+"]";
-}
-let lastSelectList=[];
-function updateDashboard(data) {
-    let frame = document.getElementById('dashboardPage');
-    let showInvalid=true;
-    forEl('select[name=showInvalidData]',function(el){
-        if (el.value == 'false') showInvalid=false;
-    })
-    let names={};
-    for (let n in data) {
-        let current=parseBoatDataLine(data[n]);
-        if (! current.name) continue;
-        names[current.name]=true;
-        let de = document.getElementById('data_' + current.name);
-        let isValid=current.valid;
-        if (! de && frame && (isValid || showInvalid)){
-            de=createDashboardItem(current.name,current,frame);   
-        }
-        if (de && (!isValid && !showInvalid)){
-            de.parentElement.remove();
-            continue;
-        }
-        if (de) {
-            let newContent='----';
-            if (current.valid) {
-                let formatter;
-                if (current.format && current.format != "NULL") {
-                    let key = current.format.replace(/^\&/, '');
-                    formatter = valueFormatters[key];
-                }
-                if (formatter) {
-                    newContent = formatter.f(current.value);
-                }
-                else {
-                    let v = parseFloat(current.value);
-                    if (!isNaN(v)) {
-                        v = v.toFixed(3)
-                        newContent = v;
-                    }
-                    else {
-                        newContent = current.value;
-                    }
-                }
-            }
-            else newContent = "---";
-            if (newContent !== de.textContent){
-                de.textContent=newContent;
-                resizeFont(de,true);
-            }
-        }
-        let src=document.getElementById('source_'+current.name);
-        if (src){
-            src.textContent=sourceName(current.source);
-        }
-    }
-    console.log("update");
-    forEl('.dashValue',function(el){
-        let id=el.getAttribute('id');
-        if (id){
-            if (! names[id.replace(/^data_/,'')]){
-                el.parentElement.remove();
-            }
-        }
-    });
-    let selectList=[];
-    for (let n in names){
-        selectList.push({l:n,v:n});
-    }
-    let selectChanged=false;
-    if (lastSelectList.length == selectList.length){
-        for (let i=0;i<lastSelectList.length;i++){
-            if (selectList[i] != lastSelectList[i]){
-                selectChanged=true;
-                break;
-            }
-        }
-    }
-    else{
-        selectChanged=true;
-    }
-    if (selectChanged){
-        forEl('.boatDataSelect',function(el){
-            updateSelectList(el,selectList,true);
-        });
-    }
-}
-function uploadBin(ev){
-    let el=document.getElementById("uploadFile");
-    let progressEl=document.getElementById("uploadDone");
-    if (! el) return;
-    if ( el.files.length < 1) return;
-    ev.target.disabled=true;
-    let file=el.files[0];
-    checkImageFile(file)
-        .then(function (result) {
-            let currentType;
-            let currentVersion;
-            let chipid;
-            forEl('.status-version', function (el) { currentVersion = el.textContent });
-            forEl('.status-fwtype', function (el) { currentType = el.textContent });
-            forEl('.status-chipid', function (el) { chipid = el.textContent });
-            let confirmText = 'Ready to update firmware?\n';
-            if (result.chipId != chipid){
-                confirmText += "WARNING: the chipid in the image ("+result.chipId;
-                confirmText +=") does not match the current chip id ("+chipid+").\n";
-            }
-            if (currentType != result.fwtype) {
-                confirmText += "WARNING: image has different type: " + result.fwtype + "\n";
-                confirmText += "** Really update anyway? - device can become unusable **";
-            }
-            else {
-                if (currentVersion == result.version) {
-                    confirmText += "WARNING: image has the same version as current " + result.version;
-                }
-                else {
-                    confirmText += "version in image: " + result.version;
-                }
-            }
-            if (!confirm(confirmText)) {
-                ev.target.disabled=false;
-                return;
-            }
-            ensurePass()
-                .then(function (hash) {
-                    let len = file.size;
-                    let req = new XMLHttpRequest();
-                    req.onloadend = function () {
-                        ev.target.disabled=false;
-                        let result = "unknown error";
-                        try {
-                            let jresult = JSON.parse(req.responseText);
-                            if (jresult.status == 'OK') {
-                                result = '';
-                            }
-                            else {
-                                if (jresult.status) {
-                                    result = jresult.status;
+                    if (!statusPage) continue;
+                    if (typeof (jsonData[k]) === 'object') {
+                        if (k.indexOf('count') == 0) {
+                            createCounterDisplay(statusPage, k.replace("count", "").replace(/in$/, " in").replace(/out$/, " out"), k, even);
+                            even = !even;
+                            for (let sk in jsonData[k]) {
+                                let key = k + "." + sk;
+                                if (typeof (jsonData[k][sk]) === 'object') {
+                                    //msg details
+                                    updateMsgDetails(key, jsonData[k][sk]);
+                                }
+                                else {
+                                    let el = document.getElementById(key);
+                                    if (el) el.textContent = jsonData[k][sk];
                                 }
                             }
-                        } catch (e) {
-                            result = "Error " + req.status;
                         }
-                        if (progressEl) {
-                            progressEl.style.width = 0;
+                        if (k.indexOf("ch") == 0) {
+                            //channel def
+                            let name = k.substring(2);
+                            channelList[name] = jsonData[k];
                         }
-                        if (!result) {
+                    }
+                    else {
+                        let el = document.getElementById(k);
+                        if (el) el.textContent = jsonData[k];
+                        forEl('.status-' + k, function (el) {
+                            el.textContent = jsonData[k];
+                        });
+                    }
+                }
+                lastUpdate = (new Date()).getTime();
+                if (reloadConfig) {
+                    reloadConfig = false;
+                    resetForm();
+                }
+            })
+    }
+    function resetForm(ev) {
+        getJson("/api/config")
+            .then(function (jsonData) {
+                for (let k in jsonData) {
+                    if (k == "useAdminPass") {
+                        needAdminPass = jsonData[k] != 'false';
+                    }
+                    let el = document.querySelector("[name='" + k + "']");
+                    if (el) {
+                        let v = jsonData[k];
+                        let def = getConfigDefition(k);
+                        if (def.check == 'checkMinMax') {
+                            //simple migration if the current value is outside the range
+                            //we even "hide" this from the user
+                            v = parseFloat(v);
+                            if (!isNaN(v)) {
+                                if (def.min !== undefined) {
+                                    if (v < parseFloat(def.min)) v = parseFloat(def.min);
+                                }
+                                if (def.max !== undefined) {
+                                    if (v > parseFloat(def.max)) v = parseFloat(def.max);
+                                }
+                            }
+                        }
+                        if (el.tagName === 'SELECT') {
+                            setSelect(el, v);
+                        }
+                        else {
+                            el.value = v;
+                        }
+                        el.setAttribute('data-loaded', v);
+                        let changeEvent = new Event('change');
+                        el.dispatchEvent(changeEvent);
+                    }
+                }
+                let name = jsonData.systemName;
+                if (name) {
+                    let el = document.getElementById('headline');
+                    if (el) el.textContent = name;
+                    document.title = name;
+                }
+            });
+    }
+    function checkMinMax(v, allValues, def) {
+        let parsed = parseFloat(v);
+        if (isNaN(parsed)) return "must be a number";
+        if (def.min !== undefined) {
+            if (parsed < parseFloat(def.min)) return "must be >= " + def.min;
+        }
+        if (def.max !== undefined) {
+            if (parsed > parseFloat(def.max)) return "must be <= " + def.max;
+        }
+    }
+
+    function checkSystemName(v) {
+        //2...32 characters for ssid
+        let allowed = v.replace(/[^a-zA-Z0-9]*/g, '');
+        if (allowed != v) return "contains invalid characters, only a-z, A-Z, 0-9";
+        if (v.length < 2 || v.length > 32) return "invalid length (2...32)";
+    }
+    function checkApPass(v) {
+        //min 8 characters
+        if (v.length < 8) {
+            return "password must be at least 8 characters";
+        }
+    }
+    function checkAdminPass(v) {
+        return checkApPass(v);
+    }
+
+    function checkApIp(v, allValues) {
+        if (!v) return "cannot be empty";
+        let err1 = "must be in the form 192.168.x.x";
+        if (!v.match(/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/)) return err1;
+        let parts = v.split(".");
+        if (parts.length != 4) return err1;
+        for (let idx = 0; idx < 4; idx++) {
+            let iv = parseInt(parts[idx]);
+            if (iv < 0 || iv > 255) return err1;
+        }
+    }
+    function checkNetMask(v, allValues) {
+        return checkApIp(v, allValues);
+    }
+
+    function checkIpAddress(v, allValues, def) {
+        if (allValues.tclEnabled != "true") return;
+        if (!v) return "cannot be empty";
+        if (!v.match(/[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*/)
+            && !v.match(/.*\.local/))
+            return "must be either in the form 192.168.1.1 or xxx.local";
+    }
+
+    function checkXDR(v, allValues) {
+        if (!v) return;
+        let parts = v.split(',');
+        if (parseInt(parts[1]) == 0) return;
+        if (parseInt(parts[1]) != 0 && !parts[6]) return "missing transducer name";
+        for (let k in allValues) {
+            if (!k.match(/^XDR/)) continue;
+            let cmp = allValues[k];
+            if (cmp == v) {
+                return "same mapping already defined in " + k;
+            }
+            let cmpParts = cmp.split(',');
+            if (parseInt(cmpParts[1]) != 0 && parts[6] == cmpParts[6]) {
+                return "transducer " + parts[6] + " already defined in " + k;
+            }
+            //check similar mappings
+            if (parts[0] == cmpParts[0] && parts[2] == cmpParts[2] && parts[3] == cmpParts[3]) {
+                if (parts[4] == cmpParts[4] && parts[5] == cmpParts[5]) {
+                    return "mapping for the same entity already defined in " + k;
+                }
+                if ((parseInt(parts[4]) == 0 && parseInt(cmpParts[4]) == 1) ||
+                    (parseInt(parts[4]) == 1 && parseInt(cmpParts[4]) == 0)
+                ) {
+                    //ignore and single for the same mapping
+                    return "mapping for the same entity already defined in " + k;
+                }
+            }
+        }
+    }
+    let loggedChecks={};
+    function getAllConfigs(omitPass) {
+        let values = document.querySelectorAll('.configForm select , .configForm input');
+        let allValues = {};
+        for (let i = 0; i < values.length; i++) {
+            let v = values[i];
+            let name = v.getAttribute('name');
+            if (!name) continue;
+            if (name.indexOf("_") >= 0) continue;
+            if (v.getAttribute('disabled')) continue;
+            let def = getConfigDefition(name);
+            if (def.type === 'password' && (v.value == '' || omitPass)) {
+                continue;
+            }
+            let check = v.getAttribute('data-check');
+            if (check) {
+                let checkFunction;
+                try{
+                    checkFunction=eval(check);
+                }catch(e){}
+                if (typeof (checkFunction) === 'function') {
+                    if (! loggedChecks[check]){
+                        loggedChecks[check]=true;
+                        console.log("check:"+check);
+                    }
+                    let res = checkFunction(v.value, allValues, getConfigDefition(name));
+                    if (res) {
+                        let value = v.value;
+                        if (v.type === 'password') value = "******";
+                        let label = v.getAttribute('data-label');
+                        if (!label) label = v.getAttribute('name');
+                        v.classList.add("error");
+                        alert("invalid config for " + label + "(" + value + "):\n" + res);
+                        return;
+                    }
+                }
+            }
+            allValues[name] = v.value;
+        }
+        return allValues;
+    }
+    function changeConfig() {
+        ensurePass()
+            .then(function (pass) {
+                let newAdminPass;
+                let url = "/api/setConfig"
+                let body = "_hash=" + encodeURIComponent(pass) + "&";
+                let allValues = getAllConfigs();
+                if (!allValues) return;
+                for (let name in allValues) {
+                    if (name == 'adminPassword') {
+                        newAdminPass = allValues[name];
+                    }
+                    body += encodeURIComponent(name) + "=" + encodeURIComponent(allValues[name]) + "&";
+                }
+                fetch(url, {
+                    method: 'POST',
+                    headers: {
+                        'Content-Type': 'application/octet-stream' //we must lie here
+                    },
+                    body: body
+                })
+                    .then((rs) => rs.json())
+                    .then(function (status) {
+                        if (status.status == 'OK') {
+                            if (newAdminPass !== undefined) {
+                                forEl('#adminPassInput', function (el) {
+                                    el.valu = newAdminPass;
+                                });
+                                saveAdminPass(newAdminPass, true);
+                            }
                             alertRestart();
                         }
                         else {
-                            alert("update error: " + result);
+                            alert("unable to set config: " + status.status);
                         }
-                    }
-                    req.onerror = function (e) {
-                        ev.target.disabled=false;
-                        alert("unable to upload: " + e);
-                    }
-                    if (progressEl) {
-                        progressEl.style.width = 0;
-                        req.upload.onprogress = function (ev) {
-                            if (ev.lengthComputable) {
-                                let percent = 100 * ev.loaded / ev.total;
-                                progressEl.style.width = percent + "%";
-                            }
-                        }
-                    }
-                    let formData = new FormData();
-                    formData.append("file1", el.files[0]);
-                    req.open("POST", '/api/update?_hash=' + encodeURIComponent(hash));
-                    req.send(formData);
-                })
-                .catch(function (e) {
-                    ev.target.disabled=false;
-                 });
-        })
-        .catch(function (e) {
-            alert("This file is an invalid image file:\n" + e);
-            ev.target.disabled=false;
-        })
-}
-let HDROFFSET=288;
-let VERSIONOFFSET=16;
-let NAMEOFFSET=48;
-let MINSIZE = HDROFFSET + NAMEOFFSET + 32;
-let CHIPIDOFFSET=12; //2 bytes chip id here
-let imageCheckBytes={
-    0: 0xe9, //image magic
-    288: 0x32, //app header magic
-    289: 0x54,
-    290: 0xcd,
-    291: 0xab
-};
-function decodeFromBuffer(buffer,start,length){
-    while (length > 0 && buffer[start+length-1] == 0){
-        length--;
+                    })
+            })
+            .catch(function (e) { alert(e); })
     }
-    if (length <= 0) return "";
-    let decoder=new TextDecoder();
-    let rt=decoder.decode(buffer.slice(
-            start,
-            start+length));
-    return rt;        
-}
-function getChipId(buffer){
-    if (buffer.length < CHIPIDOFFSET+2) return -1;
-    return buffer[CHIPIDOFFSET]+256*buffer[CHIPIDOFFSET+1];
-}
-function checkImageFile(file){
-    return new Promise(function(resolve,reject){
-        if (! file) reject("no file");
-        if (file.size < MINSIZE) reject("file is too small");
-        let slice=file.slice(0,MINSIZE);
-    let reader=new FileReader();
-    reader.addEventListener('load',function(e){
-        let content=new Uint8Array(e.target.result);
-        for (let idx in imageCheckBytes){
-            if (content[idx] != imageCheckBytes[idx] ){
-                reject("missing magic byte at position "+idx+", expected "+
-                    imageCheckBytes[idx]+", got "+content[idx]);
+    function factoryReset() {
+        ensurePass()
+            .then(function (hash) {
+                if (!confirm("Really delete all configuration?\n" +
+                    "This will reset all your Wifi settings and disconnect you.")) {
+                    return;
+                }
+                getJson("/api/resetConfig?_hash=" + encodeURIComponent(hash))
+                    .then(function (status) {
+                        alertRestart();
+                    })
+            })
+            .catch(function (e) { });
+    }
+    function createCounterDisplay(parent, label, key, isEven) {
+        if (parent.querySelector("#" + key)) {
+            return;
+        }
+        let clazz = "row icon-row counter-row";
+        if (isEven) clazz += " even";
+        let row = addEl('div', clazz, parent);
+        row.setAttribute("id", key);
+        let icon = addEl('span', 'icon icon-more', row);
+        addEl('span', 'label', row, label);
+        let value = addEl('span', 'value', row, '---');
+        value.setAttribute('id', key + ".sumOk");
+        let display = addEl('div', clazz + " msgDetails hidden", parent);
+        display.setAttribute('id', key + ".ok");
+        row.addEventListener('click', function (ev) {
+            let rs = display.classList.toggle('hidden');
+            if (rs) {
+                icon.classList.add('icon-more');
+                icon.classList.remove('icon-less');
+            }
+            else {
+                icon.classList.remove('icon-more');
+                icon.classList.add('icon-less');
+            }
+        });
+    }
+    function validKey(key) {
+        if (!key) return;
+        return key.replace(/[^a-z_:A-Z0-9-]/g, '');
+    }
+    function updateMsgDetails(key, details) {
+        forEl('.msgDetails', function (frame) {
+            if (frame.getAttribute('id') !== key) return;
+            for (let k in details) {
+                k = validKey(k);
+                let el = frame.querySelector("[data-id=\"" + k + "\"] ");
+                if (!el) {
+                    el = addEl('div', 'row', frame);
+                    let cv = addEl('span', 'label', el, k);
+                    cv = addEl('span', 'value', el, details[k]);
+                    cv.setAttribute('data-id', k);
+                }
+                else {
+                    el.textContent = details[k];
+                }
+            }
+            forEl('.value', function (el) {
+                let k = el.getAttribute('data-id');
+                if (k && !details[k]) {
+                    el.parentElement.remove();
+                }
+            }, frame);
+        });
+    }
+
+    function showOverlay(text, isHtml, buttons) {
+        let el = document.getElementById('overlayContent');
+        if (text instanceof Object) {
+            el.textContent = '';
+            el.appendChild(text);
+        }
+        else {
+            if (isHtml) {
+                el.innerHTML = text;
+                el.classList.remove("text");
+            }
+            else {
+                el.textContent = text;
+                el.classList.add("text");
             }
         }
-        let version=decodeFromBuffer(content,HDROFFSET+VERSIONOFFSET,32);
-        let fwtype=decodeFromBuffer(content,HDROFFSET+NAMEOFFSET,32);
-        let chipId=getChipId(content);
-        let rt={
-            fwtype:fwtype,
-            version: version,
-            chipId:chipId
-        };
-        resolve(rt);    
-    });
-    reader.readAsArrayBuffer(slice);
-    });
-}
-window.setInterval(update, 1000);
-window.setInterval(function () {
-    let dp = document.getElementById('dashboardPage');
-    if (dp.classList.contains('hidden')) return;
-    getText('api/boatDataString').then(function (data) {
-        updateDashboard(data.split('\n'));
-    });
-}, 1000);
-window.addEventListener('load', function () {
-    let buttons = document.querySelectorAll('button');
-    for (let i = 0; i < buttons.length; i++) {
-        let be = buttons[i];
-        be.onclick = window[be.id]; //assume a function with the button id
-    }
-    forEl('.showMsgDetails', function (cd) {
-        cd.addEventListener('change', function (ev) {
-            let key = ev.target.getAttribute('data-key');
-            if (!key) return;
-            let el = document.getElementById(key);
-            if (!el) return;
-            if (ev.target.checked) el.classList.remove('hidden');
-            else (el.classList).add('hidden');
-        });
-    });
-    let tabs = document.querySelectorAll('.tab');
-    for (let i = 0; i < tabs.length; i++) {
-        tabs[i].addEventListener('click', function (ev) {
-            handleTab(ev.target);
+        buttons = (buttons ? buttons : []).concat([{ label: "Close", click: hideOverlay }]);
+        let container = document.getElementById('overlayContainer');
+        let btframe = container.querySelector('.overlayButtons');
+        btframe.textContent = '';
+        buttons.forEach((btconfig) => {
+            let bt = addEl('button', '', btframe, btconfig.label);
+            bt.addEventListener("click", btconfig.click);
         });
+        container.classList.remove('hidden');
     }
-    createDashboard();
-    loadConfigDefinitions();
-    try{
-        let storedPass=localStorage.getItem('adminPass');
-        if (storedPass){
-            forEl('#adminPassInput',function(el){
-                el.value=storedPass;
+    function hideOverlay() {
+        let container = document.getElementById('overlayContainer');
+        container.classList.add('hidden');
+        let el = document.getElementById('overlayContent');
+        el.textContent = '';
+    }
+    function checkChange(el, row, name) {
+        el.classList.remove("error");
+        let loaded = el.getAttribute('data-loaded');
+        if (loaded !== undefined) {
+            if (loaded != el.value) {
+                row.classList.add('changed');
+            }
+            else {
+                row.classList.remove("changed");
+            }
+        }
+        let dependend = conditionRelations[name];
+        if (dependend) {
+            for (let el in dependend) {
+                checkCondition(dependend[el]);
+            }
+        }
+    }
+    let configDefinitions = {};
+    let xdrConfig = {};
+    //a map between the name of a config item and a list of dependend items
+    let conditionRelations = {};
+    function getConfigDefition(name) {
+        if (!name) return {};
+        let def;
+        for (let k in configDefinitions) {
+            if (configDefinitions[k].name === name) {
+                def = configDefinitions[k];
+                break;
+            }
+        }
+        if (!def) return {};
+        return def;
+    }
+    function getConditions(name) {
+        let def = getConfigDefition(name);
+        if (!def) return;
+        let condition = def.condition;
+        if (!condition) return;
+        if (!(condition instanceof Array)) condition = [condition];
+        return condition;
+    }
+    function checkCondition(element) {
+        let name = element.getAttribute('name');
+        let condition = getConditions(name);
+        if (!condition) return;
+        let visible = false;
+        if (!condition instanceof Array) condition = [condition];
+        condition.forEach(function (cel) {
+            let lvis = true;
+            for (let k in cel) {
+                let item = document.querySelector('[name=' + k + ']');
+                if (item) {
+                    let compare = cel[k];
+                    if (compare instanceof Array) {
+                        if (compare.indexOf(item.value) < 0) lvis = false;
+                    }
+                    else {
+                        if (item.value != cel[k]) lvis = false;
+                    }
+                }
+            }
+            if (lvis) visible = true;
+        });
+        let row = closestParent(element, 'row');
+        if (!row) return;
+        if (visible) row.classList.remove('hidden');
+        else row.classList.add('hidden');
+    }
+    let caliv = 0;
+    function createCalSetInput(configItem, frame, clazz) {
+        let el = addEl('input', clazz, frame);
+        let cb = addEl('button', '', frame, 'C');
+        //el.disabled=true;
+        cb.addEventListener('click', (ev) => {
+            let cs = document.getElementById("calset").cloneNode(true);
+            cs.classList.remove("hidden");
+            cs.querySelector(".heading").textContent = configItem.label || configItem.name;
+            let vel = cs.querySelector(".val");
+            if (caliv != 0) window.clearInterval(caliv);
+            caliv = window.setInterval(() => {
+                if (document.body.contains(cs)) {
+                    fetch("/api/calibrate?name=" + encodeURIComponent(configItem.name))
+                        .then((r) => r.text())
+                        .then((txt) => {
+                            if (txt != vel.textContent) {
+                                vel.textContent = txt;
+                            }
+                        })
+                        .catch((e) => {
+                            alert(e);
+                            hideOverlay();
+                            window.clearInterval(caliv);
+                        })
+                }
+                else {
+                    window.clearInterval(caliv);
+                }
+            }, 200);
+            showOverlay(cs, false, [{
+                label: 'Set', click: () => {
+                    el.value = vel.textContent;
+                    let cev = new Event('change');
+                    el.dispatchEvent(cev);
+                }
+            }]);
+        })
+        el.setAttribute('name', configItem.name)
+        return el;
+    }
+    function createCalValInput(configItem, frame, clazz) {
+        let el = addEl('input', clazz, frame);
+        let cb = addEl('button', '', frame, 'C');
+        //el.disabled=true;
+        cb.addEventListener('click', (ev) => {
+            const sv = function (val, cfg) {
+                if (configItem.eval) {
+                    let v = parseFloat(val);
+                    let c = parseFloat(cfg);
+                    return (eval(configItem.eval));
+                }
+                return v;
+            };
+            let cs = document.getElementById("calval").cloneNode(true);
+            cs.classList.remove("hidden");
+            cs.querySelector(".heading").textContent = configItem.label || configItem.name;
+            let vel = cs.querySelector(".val");
+            let vinp = cs.querySelector("input");
+            vinp.value = el.value;
+            if (caliv != 0) window.clearInterval(caliv);
+            caliv = window.setInterval(() => {
+                if (document.body.contains(cs)) {
+                    fetch("/api/calibrate?name=" + encodeURIComponent(configItem.name))
+                        .then((r) => r.text())
+                        .then((txt) => {
+                            txt = sv(txt, vinp.value);
+                            if (txt != vel.textContent) {
+                                vel.textContent = txt;
+                            }
+                        })
+                        .catch((e) => {
+                            alert(e);
+                            hideOverlay();
+                            window.clearInterval(caliv);
+                        })
+                }
+                else {
+                    window.clearInterval(caliv);
+                }
+            }, 200);
+            showOverlay(cs, false, [{
+                label: 'Set', click: () => {
+                    el.value = vinp.value;
+                    let cev = new Event('change');
+                    el.dispatchEvent(cev);
+                }
+            }]);
+        })
+        el.setAttribute('name', configItem.name)
+        return el;
+    }
+    function createInput(configItem, frame, clazz) {
+        let el;
+        if (configItem.type === 'boolean' || configItem.type === 'list' || configItem.type == 'boatData') {
+            el = addEl('select', clazz, frame);
+            if (configItem.readOnly) el.setAttribute('disabled', true);
+            el.setAttribute('name', configItem.name)
+            let slist = [];
+            if (configItem.list) {
+                configItem.list.forEach(function (v) {
+                    if (v instanceof Object) {
+                        slist.push({ l: v.l, v: v.v });
+                    }
+                    else {
+                        slist.push({ l: v, v: v });
+                    }
+                })
+            }
+            else if (configItem.type != 'boatData') {
+                slist.push({ l: 'on', v: 'true' })
+                slist.push({ l: 'off', v: 'false' })
+            }
+            slist.forEach(function (sitem) {
+                let sitemEl = addEl('option', '', el, sitem.l);
+                sitemEl.setAttribute('value', sitem.v);
+            })
+            if (configItem.type == 'boatData') {
+                el.classList.add('boatDataSelect');
+            }
+            return el;
+        }
+        if (configItem.type === 'filter') {
+            return createFilterInput(configItem, frame, clazz);
+        }
+        if (configItem.type === 'xdr') {
+            return createXdrInput(configItem, frame, clazz);
+        }
+        if (configItem.type === "calset") {
+            return createCalSetInput(configItem, frame, clazz);
+        }
+        if (configItem.type === "calval") {
+            return createCalValInput(configItem, frame, clazz);
+        }
+        el = addEl('input', clazz, frame);
+        if (configItem.readOnly) el.setAttribute('disabled', true);
+        el.setAttribute('name', configItem.name)
+        if (configItem.type === 'password') {
+            el.setAttribute('type', 'password');
+            let vis = addEl('span', 'icon-eye icon', frame);
+            vis.addEventListener('click', function (v) {
+                if (vis.classList.toggle('active')) {
+                    el.setAttribute('type', 'text');
+                }
+                else {
+                    el.setAttribute('type', 'password');
+                }
             });
         }
-    }catch(e){}
-    let statusPage=document.getElementById('statusPageContent');
-    /*if (statusPage){
-        let even=true;
-        for (let c in counters){
-            createCounterDisplay(statusPage,counters[c],c,even);
-            even=!even;
+        else if (configItem.type === 'number') {
+            el.setAttribute('type', 'number');
         }
-    }*/
-    forEl('#uploadFile',function(el){
-        el.addEventListener('change',function(ev){
-            if (ev.target.files.length < 1) return;
-            let file=ev.target.files[0];
-            checkImageFile(file)
-                .then(function(res){
-                    forEl('#imageProperties',function(iel){
-                        let txt="["+res.chipId+"] ";
-                        txt+=res.fwtype+", "+res.version;
-                        iel.textContent=txt;
-                        iel.classList.remove("error");
-                    })    
-                })
-                .catch(function(e){
-                    forEl('#imageProperties',function(iel){
-                        iel.textContent=e;
-                        iel.classList.add("error");
-                    })
-                })
+        else {
+            el.setAttribute('type', 'text');
+        }
+        return el;
+    }
+
+    function setSelect(item, value) {
+        if (!item) return;
+        item.value = value;
+        if (item.value !== value) {
+            //missing option with his value
+            let o = addEl('option', undefined, item, value);
+            o.setAttribute('value', value);
+            item.value = value;
+        }
+    }
+
+    function updateSelectList(item, slist, opt_keepValue) {
+        let idx = 0;
+        let value;
+        if (opt_keepValue) value = item.value;
+        item.innerHTML = '';
+        slist.forEach(function (sitem) {
+            let sitemEl = addEl('option', '', item, sitem.l);
+            sitemEl.setAttribute('value', sitem.v !== undefined ? sitem.v : idx);
+            idx++;
         })
-    })
-});
\ No newline at end of file
+        if (value !== undefined) {
+            setSelect(item, value);
+        }
+    }
+    function getXdrCategories() {
+        let rt = [];
+        for (let c in xdrConfig) {
+            if (xdrConfig[c].enabled !== false) {
+                rt.push({ l: c, v: xdrConfig[c].id });
+            }
+        }
+        return rt;
+    }
+    function getXdrCategoryName(cid) {
+        category = parseInt(cid);
+        for (let c in xdrConfig) {
+            let base = xdrConfig[c];
+            if (parseInt(base.id) == category) {
+                return c;
+            }
+        }
+    }
+    function getXdrCategory(cid) {
+        category = parseInt(cid);
+        for (let c in xdrConfig) {
+            let base = xdrConfig[c];
+            if (parseInt(base.id) == category) {
+                return base;
+            }
+        }
+        return {};
+    }
+    function getXdrSelectors(category) {
+        let base = getXdrCategory(category);
+        return base.selector || [];
+    }
+    function getXdrFields(category) {
+        let base = getXdrCategory(category);
+        if (!base.fields) return [];
+        let rt = [];
+        base.fields.forEach(function (f) {
+            if (f.t === undefined) return;
+            if (parseInt(f.t) == 99) return; //unknown type
+            rt.push(f);
+        });
+        return rt;
+    }
+
+    function createXdrLine(parent, label) {
+        let d = addEl('div', 'xdrline', parent);
+        addEl('span', 'xdrlabel', d, label);
+        return d;
+    }
+    function showHideXdr(el, show, useParent) {
+        if (useParent) el = el.parentElement;
+        if (show) el.classList.remove('xdrunused');
+        else el.classList.add('xdrunused');
+    }
+
+    function createXdrInput(configItem, frame) {
+        let configCategory = configItem.category;
+        let el = addEl('div', 'xdrinput', frame);
+        let d = createXdrLine(el, 'Direction');
+        let direction = createInput({
+            type: 'list',
+            name: configItem.name + "_dir",
+            list: [
+                //GwXDRMappingDef::Direction
+                { l: 'off', v: 0 },
+                { l: 'bidir', v: 1 },
+                { l: 'to2K', v: 2 },
+                { l: 'from2K', v: 3 }
+            ],
+            readOnly: configItem.readOnly
+        }, d, 'xdrdir');
+        d = createXdrLine(el, 'Category');
+        let category = createInput({
+            type: 'list',
+            name: configItem.name + "_cat",
+            list: getXdrCategories(),
+            readOnly: configItem.readOnly
+        }, d, 'xdrcat');
+        d = createXdrLine(el, 'Source');
+        let selector = createInput({
+            type: 'list',
+            name: configItem.name + "_sel",
+            list: [],
+            readOnly: configItem.readOnly
+        }, d, 'xdrsel');
+        d = createXdrLine(el, 'Field');
+        let field = createInput({
+            type: 'list',
+            name: configItem.name + '_field',
+            list: [],
+            readOnly: configItem.readOnly
+        }, d, 'xdrfield');
+        d = createXdrLine(el, 'Instance');
+        let imode = createInput({
+            type: 'list',
+            name: configItem.name + "_imode",
+            list: [
+                //GwXDRMappingDef::InstanceMode
+                { l: 'single', v: 0 },
+                { l: 'ignore', v: 1 },
+                { l: 'auto', v: 2 }
+            ],
+            readOnly: configItem.readOnly
+        }, d, 'xdrimode');
+        let instance = createInput({
+            type: 'number',
+            name: configItem.name + "_instance",
+            readOnly: configItem.readOnly
+        }, d, 'xdrinstance');
+        d = createXdrLine(el, 'Transducer');
+        let xdrName = createInput({
+            type: 'text',
+            name: configItem.name + "_xdr",
+            readOnly: configItem.readOnly
+        }, d, 'xdrname');
+        d = createXdrLine(el, 'Example');
+        let example = addEl('div', 'xdrexample', d, '');
+        let data = addEl('input', 'xdrvalue', el);
+        data.setAttribute('type', 'hidden');
+        data.setAttribute('name', configItem.name);
+        if (configItem.readOnly) data.setAttribute('disabled', true);
+        let changeFunction = function () {
+            let parts = data.value.split(',');
+            direction.value = parts[1] || 0;
+            category.value = parts[0] || 0;
+            let selectors = getXdrSelectors(category.value);
+            updateSelectList(selector, selectors);
+            showHideXdr(selector, selectors.length > 0);
+            let fields = getXdrFields(category.value);
+            updateSelectList(field, fields);
+            showHideXdr(field, fields.length > 0);
+            selector.value = parts[2] || 0;
+            field.value = parts[3] || 0;
+            imode.value = parts[4] || 0;
+            instance.value = parts[5] || 0;
+            showHideXdr(instance, imode.value == 0);
+            xdrName.value = parts[6] || '';
+            let used = isXdrUsed(data);
+            let modified = data.value != data.getAttribute('data-loaded');
+            forEl('[data-category=' + configCategory + ']', function (el) {
+                if (used) {
+                    el.classList.add('xdrcused');
+                    el.classList.remove('xdrcunused');
+                    forEl('.categoryAdd', function (add) {
+                        add.textContent = xdrName.value;
+                    }, el);
+                }
+                else {
+                    el.classList.remove('xdrcused');
+                    el.classList.add('xdrcunused');
+                    forEl('.categoryAdd', function (add) {
+                        add.textContent = '';
+                    }, el);
+                }
+                if (modified) {
+                    el.classList.add('changed');
+                }
+                else {
+                    el.classList.remove('changed');
+                }
+            });
+            if (used) {
+                getText('/api/xdrExample?mapping=' + encodeURIComponent(data.value) + '&value=2.1')
+                    .then(function (txt) {
+                        example.textContent = txt;
+                    })
+            }
+            else {
+                example.textContent = '';
+            }
+        }
+        let updateFunction = function (evt) {
+            if (evt.target == category) {
+                selector.value = 0;
+                field.value = 0;
+                instance.value = 0;
+            }
+            let txt = category.value + "," + direction.value + "," +
+                selector.value + "," + field.value + "," + imode.value;
+            let instanceValue = parseInt(instance.value || 0);
+            if (isNaN(instanceValue)) instanceValue = 0;
+            if (instanceValue < 0) instanceValue = 0;
+            if (instanceValue > 255) instanceValue = 255;
+            txt += "," + instanceValue;
+            let xdr = xdrName.value.replace(/[^a-zA-Z0-9]/g, '');
+            xdr = xdr.substr(0, 12);
+            txt += "," + xdr;
+            data.value = txt;
+            let ev = new Event('change');
+            data.dispatchEvent(ev);
+        }
+        category.addEventListener('change', updateFunction);
+        direction.addEventListener('change', updateFunction);
+        selector.addEventListener('change', updateFunction);
+        field.addEventListener('change', updateFunction);
+        imode.addEventListener('change', updateFunction);
+        instance.addEventListener('change', updateFunction);
+        xdrName.addEventListener('change', updateFunction);
+        data.addEventListener('change', changeFunction);
+        return data;
+    }
+
+    function isXdrUsed(element) {
+        let parts = element.value.split(',');
+        if (!parts[1]) return false;
+        if (!parseInt(parts[1])) return false;
+        if (!parts[6]) return false;
+        return true;
+    }
+
+    function createFilterInput(configItem, frame) {
+        let el = addEl('div', 'filter', frame);
+        let ais = createInput({
+            type: 'list',
+            name: configItem.name + "_ais",
+            list: ['aison', 'aisoff'],
+            readOnly: configItem.readOnly
+        }, el);
+        let mode = createInput({
+            type: 'list',
+            name: configItem.name + "_mode",
+            list: ['whitelist', 'blacklist'],
+            readOnly: configItem.readOnly
+        }, el);
+        let sentences = createInput({
+            type: 'text',
+            name: configItem.name + "_sentences",
+            readOnly: configItem.readOnly
+        }, el);
+        let data = addEl('input', undefined, el);
+        data.setAttribute('type', 'hidden');
+        let changeFunction = function () {
+            let cv = data.value || "";
+            let parts = cv.split(":");
+            ais.value = (parts[0] == '0') ? "aisoff" : "aison";
+            mode.value = (parts[1] == '0') ? "whitelist" : "blacklist";
+            sentences.value = parts[2] || "";
+        }
+        let updateFunction = function () {
+            let nv = (ais.value == 'aison') ? "1" : "0";
+            nv += ":";
+            nv += (mode.value == 'blacklist') ? "1" : "0";
+            nv += ":";
+            nv += sentences.value;
+            data.value = nv;
+            let chev = new Event('change');
+            data.dispatchEvent(chev);
+        }
+        mode.addEventListener('change', updateFunction);
+        ais.addEventListener("change", updateFunction);
+        sentences.addEventListener("change", updateFunction);
+        data.addEventListener('change', function (ev) {
+            changeFunction();
+        });
+        data.setAttribute('name', configItem.name);
+        if (configItem.readOnly) data.setAttribute('disabled', true);
+        return data;
+    }
+    let moreicons = ['icon-more', 'icon-less'];
+
+    function collapseCategories(parent, expand) {
+        let doExpand = expand;
+        forEl('.category', function (cat) {
+            if (typeof (expand) === 'function') doExpand = expand(cat);
+            forEl('.content', function (content) {
+                if (doExpand) {
+                    content.classList.remove('hidden');
+                }
+                else {
+                    content.classList.add('hidden');
+                }
+            }, cat);
+            forEl('.title .icon', function (icon) {
+                toggleClass(icon, doExpand ? 1 : 0, moreicons);
+            }, cat);
+        }, parent);
+    }
+
+    function findFreeXdr(data) {
+        let page = document.getElementById('xdrPage');
+        let el = undefined;
+        collapseCategories(page, function (cat) {
+            if (el) return false;
+            let vEl = cat.querySelector('.xdrvalue');
+            if (!vEl) return false;
+            if (isXdrUsed(vEl)) return false;
+            el = vEl;
+            if (data) {
+                el.value = data;
+                let ev = new Event('change');
+                el.dispatchEvent(ev);
+                window.setTimeout(function () {
+                    cat.scrollIntoView();
+                }, 50);
+            }
+            return true;
+        });
+    }
+
+    function convertUnassigned(value) {
+        let rt = {};
+        value = parseInt(value);
+        if (isNaN(value)) return;
+        //see GwXDRMappings::addUnknown
+        let instance = value & 0x1ff;
+        value = value >> 9;
+        let field = value & 0x7f;
+        value = value >> 7;
+        let selector = value & 0x7f;
+        value = value >> 7;
+        let cid = value & 0x7f;
+        let category = getXdrCategory(cid);
+        let cname = getXdrCategoryName(cid);
+        if (!cname) return rt;
+        let fieldName = "";
+        let idx = 0;
+        (category.fields || []).forEach(function (f) {
+            if (f.v === undefined) {
+                if (idx == field) fieldName = f.l;
+            }
+            else {
+                if (parseInt(f.v) == field) fieldName = f.l;
+            }
+            idx++;
+        });
+        let selectorName = selector + "";
+        (category.selector || []).forEach(function (s) {
+            if (parseInt(s.v) == selector) selectorName = s.l;
+        });
+        rt.l = cname + "," + selectorName + "," + fieldName + "," + instance;
+        rt.v = cid + ",1," + selector + "," + field + ",0," + instance + ",";
+        return rt;
+    }
+
+    function unassignedAdd(ev) {
+        let dv = ev.target.getAttribute('data-value');
+        if (dv) {
+            findFreeXdr(dv);
+            hideOverlay();
+        }
+    }
+    function loadUnassigned() {
+        getText("/api/xdrUnmapped")
+            .then(function (txt) {
+                let ot = "";
+                txt.split('\n').forEach(function (line) {
+                    let cv = convertUnassigned(line);
+                    if (!cv || !cv.l) return;
+                    ot += '<div class="xdrunassigned"><span>' +
+                        cv.l + '</span>' +
+                        '<button class="addunassigned" data-value="' +
+                        cv.v +
+                        '">+</button></div>';
+                })
+                showOverlay(ot, true);
+                forEl('.addunassigned', function (bt) {
+                    bt.onclick = unassignedAdd;
+                });
+            })
+    }
+    function showXdrHelp() {
+        let helpContent = document.getElementById('xdrHelp');
+        if (helpContent) {
+            showOverlay(helpContent.innerHTML, true);
+        }
+    }
+    function formatDateForFilename(usePrefix, d) {
+        let rt = "";
+        if (usePrefix) {
+            let fwt = document.querySelector('.status-fwtype');
+            if (fwt) rt = fwt.textContent;
+            rt += "_";
+        }
+        if (!d) d = new Date();
+        [d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds()]
+            .forEach(function (v) {
+                if (v < 10) rt += "0" + v;
+                else rt += "" + v;
+            })
+        return rt;
+    }
+    function downloadData(data, name) {
+        let url = "data:application/octet-stream," + encodeURIComponent(JSON.stringify(data, undefined, 2));
+        let target = document.getElementById('downloadXdr');
+        if (!target) return;
+        target.setAttribute('href', url);
+        target.setAttribute('download', name);
+        target.click();
+    }
+    function exportConfig() {
+        let data = getAllConfigs(true);
+        if (!data) return;
+        downloadData(data, formatDateForFilename(true) + ".json");
+    }
+    function exportXdr() {
+        let data = {};
+        forEl('.xdrvalue', function (el) {
+            let name = el.getAttribute('name');
+            let value = el.value;
+            let err = checkXDR(value, data);
+            if (err) {
+                alert("error in " + name + ": " + value + "\n" + err);
+                return;
+            }
+            data[name] = value;
+        })
+        downloadData(data, "xdr" + formatDateForFilename(true) + ".json");
+    }
+    function importJson(opt_keyPattern) {
+        let clazz = 'importJson';
+        forEl('.' + clazz, function (ul) {
+            ul.remove();
+        });
+        let ip = addEl('input', clazz, document.body);
+        ip.setAttribute('type', 'file');
+        ip.addEventListener('change', function (ev) {
+            if (ip.files.length > 0) {
+                let f = ip.files[0];
+                let reader = new FileReader();
+                reader.onloadend = function (status) {
+                    try {
+                        let idata = JSON.parse(reader.result);
+                        let hasOverwrites = false;
+                        for (let k in idata) {
+                            if (opt_keyPattern && !k.match(opt_keyPattern)) {
+                                alert("file contains invalid key " + k);
+                                return;
+                            }
+                            let del = document.querySelector('[name=' + k + ']');
+                            if (del) {
+                                hasOverwrites = true;
+                            }
+                        }
+                        if (hasOverwrites) {
+                            if (!confirm("overwrite existing data?")) return;
+                        }
+                        for (let k in idata) {
+                            let del = document.querySelector('[name=' + k + ']');
+                            if (del) {
+                                if (del.tagName === 'SELECT') {
+                                    setSelect(del, idata[k]);
+                                }
+                                else {
+                                    del.value = idata[k];
+                                }
+                                let ev = new Event('change');
+                                del.dispatchEvent(ev);
+                            }
+                        }
+                    } catch (error) {
+                        alert("unable to parse upload: " + error);
+                        return;
+                    }
+                };
+                reader.readAsBinaryString(f);
+            }
+            ip.remove();
+        });
+        ip.click();
+    }
+    function importXdr() {
+        importJson(new RegExp(/^XDR[0-9][0-9]*/));
+    }
+    function importConfig() {
+        importJson();
+    }
+    function toggleClass(el, id, classList) {
+        let nc = classList[id];
+        let rt = false;
+        if (nc && !el.classList.contains(nc)) rt = true;
+        for (let i in classList) {
+            if (i == id) continue;
+            el.classList.remove(classList[i])
+        }
+        if (nc) el.classList.add(nc);
+        return rt;
+    }
+
+    function createConfigDefinitions(parent, capabilities, defs, includeXdr) {
+        let categories = {};
+        let frame = parent.querySelector('.configFormRows');
+        if (!frame) throw Error("no config form");
+        frame.innerHTML = '';
+        configDefinitions = defs;
+        let currentCategoryPopulated = true;
+        defs.forEach(function (item) {
+            if (!item.type || item.category === undefined) return;
+            if (item.category.match(/^xdr/)) {
+                if (!includeXdr) return;
+            }
+            else {
+                if (includeXdr) return;
+            }
+            let catEntry;
+            if (categories[item.category] === undefined) {
+                catEntry = {
+                    populated: false,
+                    frame: undefined,
+                    element: undefined
+                }
+                categories[item.category] = catEntry
+                catEntry.frame = addEl('div', 'category', frame);
+                catEntry.frame.setAttribute('data-category', item.category)
+                let categoryTitle = addEl('div', 'title', catEntry.frame);
+                let categoryButton = addEl('span', 'icon icon-more', categoryTitle);
+                addEl('span', 'label', categoryTitle, item.category);
+                addEl('span', 'categoryAdd', categoryTitle);
+                catEntry.element = addEl('div', 'content', catEntry.frame);
+                catEntry.element.classList.add('hidden');
+                categoryTitle.addEventListener('click', function (ev) {
+                    let rs = catEntry.element.classList.toggle('hidden');
+                    if (rs) {
+                        toggleClass(categoryButton, 0, moreicons);
+                    }
+                    else {
+                        toggleClass(categoryButton, 1, moreicons);
+                    }
+                })
+            }
+            else {
+                catEntry = categories[item.category];
+            }
+            let showItem = true;
+            let itemCapabilities = item.capabilities || {};
+            itemCapabilities['HIDE' + item.name] = null;
+            for (let capability in itemCapabilities) {
+                let values = itemCapabilities[capability];
+                let found = false;
+                if (!(values instanceof Array)) values = [values];
+                values.forEach(function (v) {
+                    if (v === null) {
+                        if (capabilities[capability] === undefined) found = true;
+                    }
+                    else {
+                        if (capabilities[capability] == v) found = true;
+                    }
+                });
+                if (!found) showItem = false;
+            }
+            let readOnly = false;
+            let mode = capabilities['CFGMODE' + item.name];
+            if (mode == 1) {
+                //hide
+                showItem = false;
+            }
+            if (mode == 2) {
+                readOnly = true;
+            }
+            if (showItem) {
+                item.readOnly = readOnly;
+                catEntry.populated = true;
+                let row = addEl('div', 'row', catEntry.element);
+                let label = item.label || item.name;
+                addEl('span', 'label', row, label);
+                let valueFrame = addEl('div', 'value', row);
+                let valueEl = createInput(item, valueFrame);
+                if (!valueEl) return;
+                valueEl.setAttribute('data-default', item.default);
+                if (!readOnly) valueEl.addEventListener('change', function (ev) {
+                    let el = ev.target;
+                    checkChange(el, row, item.name);
+                })
+                let condition = getConditions(item.name);
+                if (condition) {
+                    condition.forEach(function (cel) {
+                        for (let c in cel) {
+                            if (!conditionRelations[c]) {
+                                conditionRelations[c] = [];
+                            }
+                            conditionRelations[c].push(valueEl);
+                        }
+                    })
+                }
+                if (item.check) valueEl.setAttribute('data-check', item.check);
+                valueEl.setAttribute('data-label', label);
+                let btContainer = addEl('div', 'buttonContainer', row);
+                if (!readOnly) {
+                    let bt = addEl('button', 'defaultButton', btContainer, 'X');
+                    bt.setAttribute('data-default', item.default);
+                    bt.addEventListener('click', function (ev) {
+                        valueEl.value = valueEl.getAttribute('data-default');
+                        let changeEvent = new Event('change');
+                        valueEl.dispatchEvent(changeEvent);
+                    })
+                }
+                bt = addEl('button', 'infoButton', btContainer, '?');
+                bt.addEventListener('click', function (ev) {
+                    if (item.description) {
+                        showOverlay(item.description);
+                    }
+                    else {
+                        if (item.category.match(/^xdr/)) {
+                            showXdrHelp();
+                        }
+                    }
+                });
+            }
+        });
+        for (let cat in categories) {
+            let catEntry = categories[cat];
+            if (!catEntry.populated) {
+                catEntry.frame.remove();
+            }
+        }
+    }
+    function loadConfigDefinitions() {
+        getJson("api/capabilities")
+            .then(function (capabilities) {
+                if (capabilities.HELP_URL) {
+                    let el = document.getElementById('helpButton');
+                    if (el) el.setAttribute('data-url', capabilities.HELP_URL);
+                }
+                try{
+                    Object.freeze(capabilities);
+                    callListeners(api.EVENTS.capabilities,capabilities);
+                }catch (e){
+                    console.log(e);
+                }
+                getJson("config.json")
+                    .then(function (defs) {
+                        getJson("xdrconfig.json")
+                            .then(function (xdr) {
+                                xdrConfig = xdr;
+                                configDefinitions = defs;
+                                let normalConfig = document.getElementById('configPage');
+                                let xdrParent = document.getElementById('xdrPage');
+                                if (normalConfig) createConfigDefinitions(normalConfig, capabilities, defs, false);
+                                if (xdrParent) createConfigDefinitions(xdrParent, capabilities, defs, true);
+                                resetForm();
+                                getText('api/boatDataString').then(function (data) {
+                                    updateDashboard(data.split('\n'));
+                                });
+                            })
+                    })
+            })
+            .catch(function (err) { alert("unable to load config: " + err) })
+    }
+    function verifyPass(pass) {
+        return new Promise(function (resolve, reject) {
+            let hash = lastSalt + pass;
+            let md5hash = MD5(hash);
+            getJson('api/checkPass?hash=' + encodeURIComponent(md5hash))
+                .then(function (jsonData) {
+                    if (jsonData.status == 'OK') resolve(md5hash);
+                    else reject(jsonData.status);
+                    return;
+                })
+                .catch(function (error) { reject(error); })
+        });
+    }
+
+
+    function adminPassCancel() {
+        forEl('#adminPassOverlay', function (el) { el.classList.add('hidden') });
+        forEl('#adminPassInput', function (el) { el.value = '' });
+    }
+    function saveAdminPass(pass, forceIfSet) {
+        forEl('#adminPassKeep', function (el) {
+            try {
+                let old = localStorage.getItem('adminPass');
+                if (el.value == 'true' || (forceIfSet && old !== undefined)) {
+                    localStorage.setItem('adminPass', pass);
+                }
+                else {
+                    localStorage.removeItem('adminPass');
+                }
+            } catch (e) { }
+        });
+    }
+    function forgetPass() {
+        localStorage.removeItem('adminPass');
+        forEl('#adminPassInput', function (el) {
+            el.value = '';
+        });
+    }
+    function ensurePass() {
+        return new Promise(function (resolve, reject) {
+            if (!needAdminPass) {
+                resolve('');
+                return;
+            }
+            let pe = document.getElementById('adminPassInput');
+            let hint = document.getElementById('adminPassError');
+            if (!pe) {
+                reject('no input');
+                return;
+            }
+            if (pe.value == '') {
+                let ok = document.getElementById('adminPassOk');
+                if (!ok) {
+                    reject('no button');
+                    return;
+                }
+                ok.onclick = function () {
+                    verifyPass(pe.value)
+                        .then(function (pass) {
+                            forEl('#adminPassOverlay', function (el) { el.classList.add('hidden') });
+                            saveAdminPass(pe.value);
+                            resolve(pass);
+                        })
+                        .catch(function (err) {
+                            if (hint) {
+                                hint.textContent = "invalid password";
+                            }
+                        });
+                };
+                if (hint) hint.textContent = '';
+                forEl('#adminPassOverlay', function (el) { el.classList.remove('hidden') });
+            }
+            else {
+                verifyPass(pe.value)
+                    .then(function (np) { resolve(np); })
+                    .catch(function (err) {
+                        pe.value = '';
+                        ensurePass()
+                            .then(function (p) { resolve(p); })
+                            .catch(function (e) { reject(e); });
+                    });
+                return;
+            }
+        });
+    }
+    function converterInfo() {
+        getJson("api/converterInfo").then(function (json) {
+            let text = "<h3>Converted entities</h3>";
+            text += "<p><b>NMEA0183 to NMEA2000:</b><br/>";
+            text += "   " + (json.nmea0183 || "").replace(/,/g, ", ");
+            text += "</p>";
+            text += "<p><b>NMEA2000 to NMEA0183:</b><br/>";
+            text += "   " + (json.nmea2000 || "").replace(/,/g, ", ");
+            text += "</p>";
+            showOverlay(text, true);
+        });
+    }
+    function handleTab(el) {
+        let activeName = el.getAttribute('data-page');
+        if (!activeName) {
+            let extUrl = el.getAttribute('data-url');
+            if (!extUrl) return;
+            window.open(extUrl, el.getAttribute('data-window') || '_');
+        }
+        let activeTab = document.getElementById(activeName);
+        if (!activeTab) return;
+        let all = document.querySelectorAll('.tabPage');
+        for (let i = 0; i < all.length; i++) {
+            all[i].classList.add('hidden');
+        }
+        let tabs = document.querySelectorAll('.tab');
+        for (let i = 0; i < all.length; i++) {
+            tabs[i].classList.remove('active');
+        }
+        el.classList.add('active');
+        activeTab.classList.remove('hidden');
+    }
+    /**
+     *
+     * @param {number} coordinate
+     * @param axis
+     * @returns {string}
+     */
+    function formatLonLatsDecimal(coordinate, axis) {
+        coordinate = (coordinate + 540) % 360 - 180; // normalize for sphere being round
+
+        let abscoordinate = Math.abs(coordinate);
+        let coordinatedegrees = Math.floor(abscoordinate);
+
+        let coordinateminutes = (abscoordinate - coordinatedegrees) / (1 / 60);
+        let numdecimal = 2;
+        //correctly handle the toFixed(x) - will do math rounding
+        if (coordinateminutes.toFixed(numdecimal) == 60) {
+            coordinatedegrees += 1;
+            coordinateminutes = 0;
+        }
+        if (coordinatedegrees < 10) {
+            coordinatedegrees = "0" + coordinatedegrees;
+        }
+        if (coordinatedegrees < 100 && axis == 'lon') {
+            coordinatedegrees = "0" + coordinatedegrees;
+        }
+        let str = coordinatedegrees + "\u00B0";
+
+        if (coordinateminutes < 10) {
+            str += "0";
+        }
+        str += coordinateminutes.toFixed(numdecimal) + "'";
+        if (axis == "lon") {
+            str += coordinate < 0 ? "W" : "E";
+        } else {
+            str += coordinate < 0 ? "S" : "N";
+        }
+        return str;
+    };
+    function formatFixed(v, dig, fract) {
+        v = parseFloat(v);
+        if (dig === undefined) return v.toFixed(fract);
+        let s = v < 0 ? "-" : "";
+        v = Math.abs(v);
+        let rv = v.toFixed(fract);
+        let parts = rv.split('.');
+        parts[0] = "0000000000" + parts[0];
+        if (dig >= 10) dig = 10;
+        if (fract > 0) {
+            return s + parts[0].substr(parts[0].length - dig) + "." + parts[1];
+        }
+        return s + parts[0].substr(parts[0].length - dig);
+    }
+    let valueFormatters = {
+        formatCourse: {
+            f: function (v) {
+                let x = parseFloat(v);
+                let rt = x * 180.0 / Math.PI;
+                if (rt > 360) rt -= 360;
+                if (rt < 0) rt += 360;
+                return rt.toFixed(0);
+            },
+            u: '°'
+        },
+        formatKnots: {
+            f: function (v) {
+                let x = parseFloat(v);
+                x = x * 3600.0 / 1852.0;
+                return x.toFixed(2);
+            },
+            u: 'kn'
+        },
+        formatWind: {
+            f: function (v) {
+                let x = parseFloat(v);
+                x = x * 180.0 / Math.PI;
+                if (x > 180) x = -1 * (360 - x);
+                return x.toFixed(0);
+            },
+            u: '°'
+        },
+        mtr2nm: {
+            f: function (v) {
+                let x = parseFloat(v);
+                x = x / 1852.0;
+                return x.toFixed(2);
+            },
+            u: 'nm'
+        },
+        kelvinToC: {
+            f: function (v) {
+                let x = parseFloat(v);
+                x = x - 273.15;
+                return x.toFixed(0);
+            },
+            u: '°'
+        },
+        formatFixed0: {
+            f: function (v) {
+                let x = parseFloat(v);
+                return x.toFixed(0);
+            },
+            u: ''
+        },
+        formatDepth: {
+            f: function (v) {
+                let x = parseFloat(v);
+                return x.toFixed(1);
+            },
+            u: 'm'
+        },
+        formatLatitude: {
+            f: function (v) {
+                let x = parseFloat(v);
+                if (isNaN(x)) return '-----';
+                return formatLonLatsDecimal(x, 'lat');
+            },
+            u: '°'
+        },
+        formatLongitude: {
+            f: function (v) {
+                let x = parseFloat(v);
+                if (isNaN(x)) return '-----';
+                return formatLonLatsDecimal(x, 'lon');
+            },
+            u: ''
+        },
+        formatRot: {
+            f: function (v) {
+                let x = parseFloat(v);
+                if (isNaN(x)) return '---';
+                x = x * 180.0 / Math.PI;
+                return x.toFixed(2);
+            },
+            u: '°/s'
+        },
+        formatXte: {
+            f: function (v) {
+                let x = parseFloat(v);
+                if (isNaN(x)) return '---';
+                return x.toFixed(0);
+            },
+            u: 'm'
+        },
+        formatDate: {
+            f: function (v) {
+                v = parseFloat(v);
+                if (isNaN(v)) return "----/--/--";
+                //strange day offset from NMEA0183 lib
+                let d = new Date("2010/01/01");
+                let days = 14610 - d.getTime() / 1000 / 86400;
+                let tbase = (v - days) * 1000 * 86400;
+                let od = new Date(tbase);
+                return formatFixed(od.getFullYear(), 4, 0) +
+                    "/" + formatFixed(od.getMonth() + 1, 2, 0) +
+                    "/" + formatFixed(od.getDate(), 2, 0);
+            },
+            u: ''
+        },
+        formatTime: {
+            f: function (v) {
+                v = parseFloat(v);
+                if (isNaN(v)) return "--:--:--";
+                let hr = Math.floor(v / 3600.0);
+                let min = Math.floor((v - hr * 3600.0) / 60);
+                let s = Math.floor((v - hr * 3600.0 - min * 60.0));
+                return formatFixed(hr, 2, 0) + ':' +
+                    formatFixed(min, 2, 0) + ':' +
+                    formatFixed(s, 2, 0);
+            },
+            u: ''
+        }
+
+
+    }
+    function resizeFont(el, reset, maxIt) {
+        if (maxIt === undefined) maxIt = 10;
+        if (!el) return;
+        if (reset) el.style.fontSize = '';
+        while (el.scrollWidth > el.clientWidth && maxIt) {
+            let next = parseFloat(window.getComputedStyle(el).fontSize) * 0.9;
+            el.style.fontSize = next + "px";
+        }
+    }
+    function createDashboardItem(name, def, parent) {
+        if (!def.name) return;
+        let frame = addEl('div', 'dash', parent);
+        let title = addEl('span', 'dashTitle', frame, name);
+        let value = addEl('span', 'dashValue', frame);
+        value.setAttribute('id', 'data_' + name);
+        let fmt = valueFormatters[def.format];
+        if (def.format) value.classList.add(def.format);
+        let footer = addEl('div', 'footer', frame);
+        let src = addEl('span', 'source', footer);
+        src.setAttribute('id', 'source_' + name);
+        let u = fmt ? fmt.u : ' ';
+        if (!fmt && def.format && def.format.match(/formatXdr/)) {
+            u = def.format.replace(/formatXdr:[^:]*:/, '');
+        }
+        addEl('span', 'unit', footer, u);
+        return value;
+    }
+    function parseBoatDataLine(line) {
+        let rt = {};
+        let parts = line.split(',');
+        rt.name = parts[0];
+        rt.valid = parts[2] === '1';
+        rt.update = parseInt(parts[3]);
+        rt.source = parseInt(parts[4]);
+        rt.format = parts[1];
+        rt.value = parts[5];
+        return rt;
+    }
+    function createDashboard() {
+        let frame = document.getElementById('dashboardPage');
+        if (!frame) return;
+        frame.innerHTML = '';
+    }
+    function sourceName(v) {
+        if (v == 0) return "N2K";
+        for (let n in channelList) {
+            if (v == channelList[n].id) return n;
+            if (v >= channelList[n].id && v <= channelList[n].max) {
+                return n;
+            }
+        }
+        if (v < minUser) return "---";
+        return "USER[" + v + "]";
+    }
+    let lastSelectList = [];
+    function updateDashboard(data) {
+        let frame = document.getElementById('dashboardPage');
+        let showInvalid = true;
+        forEl('select[name=showInvalidData]', function (el) {
+            if (el.value == 'false') showInvalid = false;
+        })
+        let names = {};
+        for (let n in data) {
+            let current = parseBoatDataLine(data[n]);
+            if (!current.name) continue;
+            names[current.name] = true;
+            let de = document.getElementById('data_' + current.name);
+            let isValid = current.valid;
+            if (!de && frame && (isValid || showInvalid)) {
+                de = createDashboardItem(current.name, current, frame);
+            }
+            if (de && (!isValid && !showInvalid)) {
+                de.parentElement.remove();
+                continue;
+            }
+            if (de) {
+                let newContent = '----';
+                if (current.valid) {
+                    let formatter;
+                    if (current.format && current.format != "NULL") {
+                        let key = current.format.replace(/^\&/, '');
+                        formatter = valueFormatters[key];
+                    }
+                    if (formatter) {
+                        newContent = formatter.f(current.value);
+                    }
+                    else {
+                        let v = parseFloat(current.value);
+                        if (!isNaN(v)) {
+                            v = v.toFixed(3)
+                            newContent = v;
+                        }
+                        else {
+                            newContent = current.value;
+                        }
+                    }
+                }
+                else newContent = "---";
+                if (newContent !== de.textContent) {
+                    de.textContent = newContent;
+                    resizeFont(de, true);
+                }
+            }
+            let src = document.getElementById('source_' + current.name);
+            if (src) {
+                src.textContent = sourceName(current.source);
+            }
+        }
+        console.log("update");
+        forEl('.dashValue', function (el) {
+            let id = el.getAttribute('id');
+            if (id) {
+                if (!names[id.replace(/^data_/, '')]) {
+                    el.parentElement.remove();
+                }
+            }
+        });
+        let selectList = [];
+        for (let n in names) {
+            selectList.push({ l: n, v: n });
+        }
+        let selectChanged = false;
+        if (lastSelectList.length == selectList.length) {
+            for (let i = 0; i < lastSelectList.length; i++) {
+                if (selectList[i] != lastSelectList[i]) {
+                    selectChanged = true;
+                    break;
+                }
+            }
+        }
+        else {
+            selectChanged = true;
+        }
+        if (selectChanged) {
+            forEl('.boatDataSelect', function (el) {
+                updateSelectList(el, selectList, true);
+            });
+        }
+    }
+    function uploadBin(ev) {
+        let el = document.getElementById("uploadFile");
+        let progressEl = document.getElementById("uploadDone");
+        if (!el) return;
+        if (el.files.length < 1) return;
+        ev.target.disabled = true;
+        let file = el.files[0];
+        checkImageFile(file)
+            .then(function (result) {
+                let currentType;
+                let currentVersion;
+                let chipid;
+                forEl('.status-version', function (el) { currentVersion = el.textContent });
+                forEl('.status-fwtype', function (el) { currentType = el.textContent });
+                forEl('.status-chipid', function (el) { chipid = el.textContent });
+                let confirmText = 'Ready to update firmware?\n';
+                if (result.chipId != chipid) {
+                    confirmText += "WARNING: the chipid in the image (" + result.chipId;
+                    confirmText += ") does not match the current chip id (" + chipid + ").\n";
+                }
+                if (currentType != result.fwtype) {
+                    confirmText += "WARNING: image has different type: " + result.fwtype + "\n";
+                    confirmText += "** Really update anyway? - device can become unusable **";
+                }
+                else {
+                    if (currentVersion == result.version) {
+                        confirmText += "WARNING: image has the same version as current " + result.version;
+                    }
+                    else {
+                        confirmText += "version in image: " + result.version;
+                    }
+                }
+                if (!confirm(confirmText)) {
+                    ev.target.disabled = false;
+                    return;
+                }
+                ensurePass()
+                    .then(function (hash) {
+                        let len = file.size;
+                        let req = new XMLHttpRequest();
+                        req.onloadend = function () {
+                            ev.target.disabled = false;
+                            let result = "unknown error";
+                            try {
+                                let jresult = JSON.parse(req.responseText);
+                                if (jresult.status == 'OK') {
+                                    result = '';
+                                }
+                                else {
+                                    if (jresult.status) {
+                                        result = jresult.status;
+                                    }
+                                }
+                            } catch (e) {
+                                result = "Error " + req.status;
+                            }
+                            if (progressEl) {
+                                progressEl.style.width = 0;
+                            }
+                            if (!result) {
+                                alertRestart();
+                            }
+                            else {
+                                alert("update error: " + result);
+                            }
+                        }
+                        req.onerror = function (e) {
+                            ev.target.disabled = false;
+                            alert("unable to upload: " + e);
+                        }
+                        if (progressEl) {
+                            progressEl.style.width = 0;
+                            req.upload.onprogress = function (ev) {
+                                if (ev.lengthComputable) {
+                                    let percent = 100 * ev.loaded / ev.total;
+                                    progressEl.style.width = percent + "%";
+                                }
+                            }
+                        }
+                        let formData = new FormData();
+                        formData.append("file1", el.files[0]);
+                        req.open("POST", '/api/update?_hash=' + encodeURIComponent(hash));
+                        req.send(formData);
+                    })
+                    .catch(function (e) {
+                        ev.target.disabled = false;
+                    });
+            })
+            .catch(function (e) {
+                alert("This file is an invalid image file:\n" + e);
+                ev.target.disabled = false;
+            })
+    }
+    let HDROFFSET = 288;
+    let VERSIONOFFSET = 16;
+    let NAMEOFFSET = 48;
+    let MINSIZE = HDROFFSET + NAMEOFFSET + 32;
+    let CHIPIDOFFSET = 12; //2 bytes chip id here
+    let imageCheckBytes = {
+        0: 0xe9, //image magic
+        288: 0x32, //app header magic
+        289: 0x54,
+        290: 0xcd,
+        291: 0xab
+    };
+    function decodeFromBuffer(buffer, start, length) {
+        while (length > 0 && buffer[start + length - 1] == 0) {
+            length--;
+        }
+        if (length <= 0) return "";
+        let decoder = new TextDecoder();
+        let rt = decoder.decode(buffer.slice(
+            start,
+            start + length));
+        return rt;
+    }
+    function getChipId(buffer) {
+        if (buffer.length < CHIPIDOFFSET + 2) return -1;
+        return buffer[CHIPIDOFFSET] + 256 * buffer[CHIPIDOFFSET + 1];
+    }
+    function checkImageFile(file) {
+        return new Promise(function (resolve, reject) {
+            if (!file) reject("no file");
+            if (file.size < MINSIZE) reject("file is too small");
+            let slice = file.slice(0, MINSIZE);
+            let reader = new FileReader();
+            reader.addEventListener('load', function (e) {
+                let content = new Uint8Array(e.target.result);
+                for (let idx in imageCheckBytes) {
+                    if (content[idx] != imageCheckBytes[idx]) {
+                        reject("missing magic byte at position " + idx + ", expected " +
+                            imageCheckBytes[idx] + ", got " + content[idx]);
+                    }
+                }
+                let version = decodeFromBuffer(content, HDROFFSET + VERSIONOFFSET, 32);
+                let fwtype = decodeFromBuffer(content, HDROFFSET + NAMEOFFSET, 32);
+                let chipId = getChipId(content);
+                let rt = {
+                    fwtype: fwtype,
+                    version: version,
+                    chipId: chipId
+                };
+                resolve(rt);
+            });
+            reader.readAsArrayBuffer(slice);
+        });
+    }
+    const api= {
+        registerListener: function (callback) {
+            listeners.push(callback);
+        },
+        addEl: addEl,
+        forEl: forEl,
+        closestParent: closestParent,
+        EVENTS: {
+            init: 0,
+            capabilities: 1, //called when capabilities are loaded
+        }
+    };
+    function callListeners(event,data){
+        listeners.forEach((listener)=>{
+            if (typeof(listener) === 'function'){
+                listener(event,data);
+            }
+        })
+    }
+    window.esp32nmea = api;
+    window.setInterval(update, 1000);
+    window.setInterval(function () {
+        let dp = document.getElementById('dashboardPage');
+        if (dp.classList.contains('hidden')) return;
+        getText('api/boatDataString').then(function (data) {
+            updateDashboard(data.split('\n'));
+        });
+    }, 1000);
+    window.addEventListener('load', function () {
+        let buttons = document.querySelectorAll('button');
+        for (let i = 0; i < buttons.length; i++) {
+            let be = buttons[i];
+            let buttonFunction=eval(be.id);
+            if (typeof(buttonFunction) === 'function'){
+                be.onclick = buttonFunction; //assume a function with the button id
+                console.log("button: "+be.id);
+            }
+            else{
+                console.log("no handler for button "+be.id);
+            }
+        }
+        forEl('.showMsgDetails', function (cd) {
+            cd.addEventListener('change', function (ev) {
+                let key = ev.target.getAttribute('data-key');
+                if (!key) return;
+                let el = document.getElementById(key);
+                if (!el) return;
+                if (ev.target.checked) el.classList.remove('hidden');
+                else (el.classList).add('hidden');
+            });
+        });
+        let tabs = document.querySelectorAll('.tab');
+        for (let i = 0; i < tabs.length; i++) {
+            tabs[i].addEventListener('click', function (ev) {
+                handleTab(ev.target);
+            });
+        }
+        createDashboard();
+        loadConfigDefinitions();
+        try {
+            let storedPass = localStorage.getItem('adminPass');
+            if (storedPass) {
+                forEl('#adminPassInput', function (el) {
+                    el.value = storedPass;
+                });
+            }
+        } catch (e) { }
+        let statusPage = document.getElementById('statusPageContent');
+        /*if (statusPage){
+            let even=true;
+            for (let c in counters){
+                createCounterDisplay(statusPage,counters[c],c,even);
+                even=!even;
+            }
+        }*/
+        forEl('#uploadFile', function (el) {
+            el.addEventListener('change', function (ev) {
+                if (ev.target.files.length < 1) return;
+                let file = ev.target.files[0];
+                checkImageFile(file)
+                    .then(function (res) {
+                        forEl('#imageProperties', function (iel) {
+                            let txt = "[" + res.chipId + "] ";
+                            txt += res.fwtype + ", " + res.version;
+                            iel.textContent = txt;
+                            iel.classList.remove("error");
+                        })
+                    })
+                    .catch(function (e) {
+                        forEl('#imageProperties', function (iel) {
+                            iel.textContent = e;
+                            iel.classList.add("error");
+                        })
+                    })
+            })
+        })
+        callListeners(api.EVENTS.init);
+    });
+}());
\ No newline at end of file

From 98a95e62f7f9cee031e360c81d4f13eaf7b0032f Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Fri, 11 Oct 2024 20:13:45 +0200
Subject: [PATCH 25/58] first version for user js tasks

---
 lib/exampletask/index.js | 12 +++++++
 tools/testServer.py      | 14 +++++++-
 web/index.css            |  4 +--
 web/index.js             | 71 ++++++++++++++++++++--------------------
 4 files changed, 63 insertions(+), 38 deletions(-)
 create mode 100644 lib/exampletask/index.js

diff --git a/lib/exampletask/index.js b/lib/exampletask/index.js
new file mode 100644
index 0000000..0224a73
--- /dev/null
+++ b/lib/exampletask/index.js
@@ -0,0 +1,12 @@
+(function(){
+    let isActive=false;
+    window.esp32nmea2k.registerListener((id,data)=>{
+        if (id === 0){
+            //data is capabilities
+            if (data.testboard) isActive=true;
+        }
+        if (isActive){
+            console.log("exampletask listener",id,data);
+        }
+    })
+})();
diff --git a/tools/testServer.py b/tools/testServer.py
index ecbfb4d..d00c6cd 100755
--- a/tools/testServer.py
+++ b/tools/testServer.py
@@ -55,8 +55,17 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler):
             super().do_GET()
     def do_POST(self):
         if not self.do_proxy():            
-            super().do_POST()        
+            super().do_POST()
+    def guess_type(self,path):
+        if path.endswith('.gz'):
+            return super().guess_type(path[0:-3])
+        return super().guess_type(path)
+    def end_headers(self):
+        if hasattr(self,"isgz") and self.isgz:
+            self.send_header("Content-Encoding","gzip")
+        super().end_headers()        
     def translate_path(self, path):
+        self.isgz=False
         """Translate a /-separated PATH to the local filename syntax.
 
         Components that mean special things to the local file system
@@ -90,6 +99,9 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler):
                 rpath += '/'
             if os.path.exists(rpath):
                 return rpath
+            if os.path.exists(rpath+".gz"):
+                self.isgz=True
+                return rpath+".gz"
             if isSecond:
                 return rpath
             isSecond=True
diff --git a/web/index.css b/web/index.css
index aada09f..eb3b1ca 100644
--- a/web/index.css
+++ b/web/index.css
@@ -341,6 +341,6 @@ body {
 .error{
   color: red;
 }
-.changed input.error{
-  color: red;
+input.error{
+  background-color: rgba(255, 0, 0, 0.329);
 }
\ No newline at end of file
diff --git a/web/index.js b/web/index.js
index d61556b..1d02096 100644
--- a/web/index.js
+++ b/web/index.js
@@ -1,5 +1,4 @@
 (function () {
-    let self = this;
     let lastUpdate = (new Date()).getTime();
     let reloadConfig = false;
     let needAdminPass = true;
@@ -7,6 +6,8 @@
     let channelList = {};
     let minUser = 200;
     let listeners = [];
+    let buttonHandlers={};
+    let checkers={};
     function addEl(type, clazz, parent, text) {
         let el = document.createElement(type);
         if (clazz) {
@@ -48,7 +49,7 @@
         return fetch(url)
             .then(function (r) { return r.text() });
     }
-    function reset() {
+    buttonHandlers.reset=function() {
         ensurePass()
             .then(function (hash) {
                 fetch('/api/reset?_hash=' + encodeURIComponent(hash));
@@ -161,7 +162,8 @@
                 }
             });
     }
-    function checkMinMax(v, allValues, def) {
+    buttonHandlers.resetForm=resetForm;
+    checkers.checkMinMax=function(v, allValues, def) {
         let parsed = parseFloat(v);
         if (isNaN(parsed)) return "must be a number";
         if (def.min !== undefined) {
@@ -172,7 +174,7 @@
         }
     }
 
-    function checkSystemName(v) {
+    checkers.checkSystemName=function(v) {
         //2...32 characters for ssid
         let allowed = v.replace(/[^a-zA-Z0-9]*/g, '');
         if (allowed != v) return "contains invalid characters, only a-z, A-Z, 0-9";
@@ -184,11 +186,11 @@
             return "password must be at least 8 characters";
         }
     }
-    function checkAdminPass(v) {
+    checkers.checkAdminPass=function(v) {
         return checkApPass(v);
     }
 
-    function checkApIp(v, allValues) {
+    checkers.checkApIp=function(v, allValues) {
         if (!v) return "cannot be empty";
         let err1 = "must be in the form 192.168.x.x";
         if (!v.match(/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/)) return err1;
@@ -199,11 +201,11 @@
             if (iv < 0 || iv > 255) return err1;
         }
     }
-    function checkNetMask(v, allValues) {
-        return checkApIp(v, allValues);
+    checkers.checkNetMask=function(v, allValues) {
+        return checkers.checkApIp(v, allValues);
     }
 
-    function checkIpAddress(v, allValues, def) {
+    checkers.checkIpAddress=function(v, allValues, def) {
         if (allValues.tclEnabled != "true") return;
         if (!v) return "cannot be empty";
         if (!v.match(/[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*/)
@@ -211,7 +213,7 @@
             return "must be either in the form 192.168.1.1 or xxx.local";
     }
 
-    function checkXDR(v, allValues) {
+    checkers.checkXDR=function(v, allValues) {
         if (!v) return;
         let parts = v.split(',');
         if (parseInt(parts[1]) == 0) return;
@@ -256,14 +258,11 @@
             }
             let check = v.getAttribute('data-check');
             if (check) {
-                let checkFunction;
-                try{
-                    checkFunction=eval(check);
-                }catch(e){}
+                let checkFunction=checkers[check];
                 if (typeof (checkFunction) === 'function') {
                     if (! loggedChecks[check]){
                         loggedChecks[check]=true;
-                        console.log("check:"+check);
+                        //console.log("check:"+check);
                     }
                     let res = checkFunction(v.value, allValues, getConfigDefition(name));
                     if (res) {
@@ -276,12 +275,15 @@
                         return;
                     }
                 }
+                else{
+                    console.log("check not found:",check);
+                }
             }
             allValues[name] = v.value;
         }
         return allValues;
     }
-    function changeConfig() {
+    buttonHandlers.changeConfig=function() {
         ensurePass()
             .then(function (pass) {
                 let newAdminPass;
@@ -320,7 +322,7 @@
             })
             .catch(function (e) { alert(e); })
     }
-    function factoryReset() {
+    buttonHandlers.factoryReset=function() {
         ensurePass()
             .then(function (hash) {
                 if (!confirm("Really delete all configuration?\n" +
@@ -1014,7 +1016,7 @@
             hideOverlay();
         }
     }
-    function loadUnassigned() {
+    buttonHandlers.loadUnassigned=function() {
         getText("/api/xdrUnmapped")
             .then(function (txt) {
                 let ot = "";
@@ -1062,17 +1064,17 @@
         target.setAttribute('download', name);
         target.click();
     }
-    function exportConfig() {
+    buttonHandlers.exportConfig=function() {
         let data = getAllConfigs(true);
         if (!data) return;
         downloadData(data, formatDateForFilename(true) + ".json");
     }
-    function exportXdr() {
+    buttonHandlers.exportXdr=function() {
         let data = {};
         forEl('.xdrvalue', function (el) {
             let name = el.getAttribute('name');
             let value = el.value;
-            let err = checkXDR(value, data);
+            let err = checkers.checkXDR(value, data);
             if (err) {
                 alert("error in " + name + ": " + value + "\n" + err);
                 return;
@@ -1133,10 +1135,10 @@
         });
         ip.click();
     }
-    function importXdr() {
+    buttonHandlers.importXdr=function() {
         importJson(new RegExp(/^XDR[0-9][0-9]*/));
     }
-    function importConfig() {
+    buttonHandlers.importConfig=function() {
         importJson();
     }
     function toggleClass(el, id, classList) {
@@ -1287,7 +1289,7 @@
                 }
                 try{
                     Object.freeze(capabilities);
-                    callListeners(api.EVENTS.capabilities,capabilities);
+                    callListeners(api.EVENTS.init,capabilities);
                 }catch (e){
                     console.log(e);
                 }
@@ -1325,7 +1327,7 @@
     }
 
 
-    function adminPassCancel() {
+    buttonHandlers.adminPassCancel=function() {
         forEl('#adminPassOverlay', function (el) { el.classList.add('hidden') });
         forEl('#adminPassInput', function (el) { el.value = '' });
     }
@@ -1342,7 +1344,7 @@
             } catch (e) { }
         });
     }
-    function forgetPass() {
+    buttonHandlers.forgetPass=function() {
         localStorage.removeItem('adminPass');
         forEl('#adminPassInput', function (el) {
             el.value = '';
@@ -1395,7 +1397,7 @@
             }
         });
     }
-    function converterInfo() {
+    buttonHandlers.converterInfo=function() {
         getJson("api/converterInfo").then(function (json) {
             let text = "<h3>Converted entities</h3>";
             text += "<p><b>NMEA0183 to NMEA2000:</b><br/>";
@@ -1740,7 +1742,7 @@
             });
         }
     }
-    function uploadBin(ev) {
+    buttonHandlers.uploadBin=function(ev) {
         let el = document.getElementById("uploadFile");
         let progressEl = document.getElementById("uploadDone");
         if (!el) return;
@@ -1895,8 +1897,7 @@
         forEl: forEl,
         closestParent: closestParent,
         EVENTS: {
-            init: 0,
-            capabilities: 1, //called when capabilities are loaded
+            init: 0, //called when capabilities are loaded, data is capabilities
         }
     };
     function callListeners(event,data){
@@ -1906,7 +1907,7 @@
             }
         })
     }
-    window.esp32nmea = api;
+    window.esp32nmea2k = api;
     window.setInterval(update, 1000);
     window.setInterval(function () {
         let dp = document.getElementById('dashboardPage');
@@ -1919,10 +1920,10 @@
         let buttons = document.querySelectorAll('button');
         for (let i = 0; i < buttons.length; i++) {
             let be = buttons[i];
-            let buttonFunction=eval(be.id);
+            let buttonFunction=buttonHandlers[be.id];
             if (typeof(buttonFunction) === 'function'){
                 be.onclick = buttonFunction; //assume a function with the button id
-                console.log("button: "+be.id);
+                //console.log("button: "+be.id);
             }
             else{
                 console.log("no handler for button "+be.id);
@@ -1983,6 +1984,6 @@
                     })
             })
         })
-        callListeners(api.EVENTS.init);
     });
-}());
\ No newline at end of file
+}());
+

From e982389c75b661f14daf1e4233383435fbdddebb Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Sun, 13 Oct 2024 16:09:17 +0200
Subject: [PATCH 26/58] correctly handle data timeouts, always set source if
 for updates

---
 lib/boatData/GwBoatData.cpp             |  3 +--
 lib/boatData/GwBoatData.h               |  5 ++---
 lib/nmea0183ton2k/NMEA0183DataToN2K.cpp | 14 +++++++-------
 3 files changed, 10 insertions(+), 12 deletions(-)

diff --git a/lib/boatData/GwBoatData.cpp b/lib/boatData/GwBoatData.cpp
index f7be617..4be1450 100644
--- a/lib/boatData/GwBoatData.cpp
+++ b/lib/boatData/GwBoatData.cpp
@@ -47,7 +47,6 @@ GwBoatItemBase::GwBoatItemBase(String name, String format, GwBoatItemBase::TOTyp
     this->format = format;
     this->type = 0;
     this->lastUpdateSource = -1;
-    this->toType=TOType::user;
 }
 void GwBoatItemBase::setInvalidTime(unsigned long it, bool force){
     if (toType != TOType::user || force ){
@@ -375,7 +374,7 @@ GwBoatItem<T> *GwBoatData::getOrCreate(T initial, GwBoatItemNameProvider *provid
                                           provider->getBoatItemFormat(),
                                           provider->getInvalidTime(),
                                           &values);
-    rt->update(initial);
+    rt->update(initial,-1);
     LOG_DEBUG(GwLog::LOG, "creating boatItem %s, type %d",
               name.c_str(), rt->getCurrentType());
     return rt;
diff --git a/lib/boatData/GwBoatData.h b/lib/boatData/GwBoatData.h
index 1c4394c..18a5f08 100644
--- a/lib/boatData/GwBoatData.h
+++ b/lib/boatData/GwBoatData.h
@@ -105,8 +105,8 @@ template<class T> class GwBoatItem : public GwBoatItemBase{
         GwBoatItem(String name,String formatInfo,unsigned long invalidTime=INVALID_TIME,GwBoatItemMap *map=NULL);
         GwBoatItem(String name,String formatInfo,TOType toType,GwBoatItemMap *map=NULL);
         virtual ~GwBoatItem(){}
-        bool update(T nv, int source=-1);
-        bool updateMax(T nv,int sourceId=-1);
+        bool update(T nv, int source);
+        bool updateMax(T nv,int sourceId);
         T getData(){
             return data;
         }
@@ -185,7 +185,6 @@ public:
 #define GWSPECBOATDATA(clazz,name,toType,fmt)  \
     clazz *name=new clazz(#name,GwBoatItemBase::fmt,toType,&values) ;
 class GwBoatData{
-    static const unsigned long DEF_TIME=4000;
     private:
         GwLog *logger;
         GwBoatItemBase::GwBoatItemMap values;
diff --git a/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp b/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp
index 8bb9237..be01cac 100644
--- a/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp
+++ b/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp
@@ -103,7 +103,7 @@ private:
         if (v != NMEA0183UInt32NA){
             return target->update(v,sourceId);
         }
-        return v;
+        return false;
     }
     uint32_t getUint32(GwBoatItem<uint32_t> *src){
         return src->getDataWithDefault(N2kUInt32NA);
@@ -406,13 +406,13 @@ private:
             case NMEA0183Wind_Apparent:
                 shouldSend=updateDouble(boatData->AWA,WindAngle,msg.sourceId) && 
                            updateDouble(boatData->AWS,WindSpeed,msg.sourceId);
-                if (WindSpeed != NMEA0183DoubleNA) boatData->MaxAws->updateMax(WindSpeed);    
+                if (WindSpeed != NMEA0183DoubleNA) boatData->MaxAws->updateMax(WindSpeed,msg.sourceId);    
                 mapping=config.findWindMapping(GwConverterConfig::WindMapping::AWA_AWS);
                 break;
             case NMEA0183Wind_True:
                 shouldSend=updateDouble(boatData->TWA,WindAngle,msg.sourceId) && 
                            updateDouble(boatData->TWS,WindSpeed,msg.sourceId);
-                if (WindSpeed != NMEA0183DoubleNA) boatData->MaxTws->updateMax(WindSpeed);    
+                if (WindSpeed != NMEA0183DoubleNA) boatData->MaxTws->updateMax(WindSpeed,msg.sourceId);    
                 mapping=config.findWindMapping(GwConverterConfig::WindMapping::TWA_TWS);
                 break;      
             default:
@@ -458,7 +458,7 @@ private:
         bool shouldSend = false;
         shouldSend = updateDouble(boatData->AWA, WindAngle, msg.sourceId) &&
                      updateDouble(boatData->AWS, WindSpeed, msg.sourceId);
-        if (WindSpeed != NMEA0183DoubleNA) boatData->MaxAws->updateMax(WindSpeed);             
+        if (WindSpeed != NMEA0183DoubleNA) boatData->MaxAws->updateMax(WindSpeed,msg.sourceId);             
         if (shouldSend)
         {
             const GwConverterConfig::WindMapping mapping=config.findWindMapping(GwConverterConfig::WindMapping::AWA_AWS);
@@ -503,7 +503,7 @@ private:
         if (WindDirection != NMEA0183DoubleNA){
             shouldSend = updateDouble(boatData->TWD, WindDirection, msg.sourceId) &&
                          updateDouble(boatData->TWS, WindSpeed, msg.sourceId);
-            if (WindSpeed != NMEA0183DoubleNA) boatData->MaxTws->updateMax(WindSpeed);             
+            if (WindSpeed != NMEA0183DoubleNA) boatData->MaxTws->updateMax(WindSpeed,msg.sourceId);             
             if(shouldSend && boatData->HDT->isValid()) {
                 double twa = WindDirection-boatData->HDT->getData();
                 if(twa<0) { twa+=2*M_PI; }
@@ -602,10 +602,10 @@ private:
         }
         //offset == 0? SK does not allow this
         if (Offset != NMEA0183DoubleNA && Offset>=0 ){
-            if (! boatData->DBS->update(DepthBelowTransducer+Offset)) return;
+            if (! boatData->DBS->update(DepthBelowTransducer+Offset,msg.sourceId)) return;
         }
         if (Offset == NMEA0183DoubleNA) Offset=N2kDoubleNA;
-        if (! boatData->DBT->update(DepthBelowTransducer)) return;
+        if (! boatData->DBT->update(DepthBelowTransducer,msg.sourceId)) return;
         tN2kMsg n2kMsg;
         SetN2kWaterDepth(n2kMsg,1,DepthBelowTransducer,Offset);
         send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((Offset != N2kDoubleNA)?1:0));

From 9c979657bf157b497498f5ffd51d7b91c5e970c3 Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Sun, 13 Oct 2024 16:10:03 +0200
Subject: [PATCH 27/58] restructure boat data display handling, allow for more
 flexible user formatters

---
 lib/exampletask/index.js |  31 +++++-
 web/index.css            |   5 +-
 web/index.html           | 186 ++++++++++++++++++------------------
 web/index.js             | 199 ++++++++++++++++++++++++++++++++-------
 4 files changed, 290 insertions(+), 131 deletions(-)

diff --git a/lib/exampletask/index.js b/lib/exampletask/index.js
index 0224a73..6c5322b 100644
--- a/lib/exampletask/index.js
+++ b/lib/exampletask/index.js
@@ -1,12 +1,39 @@
 (function(){
+    const api=window.esp32nmea2k;
+    if (! api) return;
     let isActive=false;
-    window.esp32nmea2k.registerListener((id,data)=>{
-        if (id === 0){
+    const tabName="example";
+    const configName="exampleBDSel";
+    let boatItemName;
+    api.registerListener((id,data)=>{
+        if (id === api.EVENTS.init){
             //data is capabilities
             if (data.testboard) isActive=true;
+            if (isActive){
+                let page=api.addTabPage(tabName,"Example");
+                api.addEl('div','',page,"this is a test tab");
+            }
         }
         if (isActive){
             console.log("exampletask listener",id,data);
+            if (id === api.EVENTS.tab){
+                if (data === tabName){
+                    console.log("example tab activated");
+                }
+            }
+            if (id == api.EVENTS.config){
+                let nextboatItemName=data[configName];
+                console.log("value of "+configName,nextboatItemName);
+                if (nextboatItemName){
+                    api.addUserFormatter(nextboatItemName,"xxx",function(v){
+                        return "X"+v+"X";
+                    })
+                }
+                if (boatItemName !== undefined && boatItemName != nextboatItemName){
+                    api.removeUserFormatter(boatItemName);
+                }
+                boatItemName=nextboatItemName;
+            }
         }
     })
 })();
diff --git a/web/index.css b/web/index.css
index eb3b1ca..f248d97 100644
--- a/web/index.css
+++ b/web/index.css
@@ -22,7 +22,7 @@ body {
   overflow: hidden;
 }
 
-.tabPage{
+#tabPages{
   overflow: auto;
 }
 
@@ -120,6 +120,9 @@ body {
   .hidden{
     display: none !important;
   }
+  .dash.invalid{
+    display: none;
+  }
   #xdrPage .row>.label{
     display: none;
   }
diff --git a/web/index.html b/web/index.html
index e7d784d..0862868 100644
--- a/web/index.html
+++ b/web/index.html
@@ -23,104 +23,106 @@
   <div class="tab" data-page="updatePage">Update</div>
   <div class="tab" data-url="https://github.com/wellenvogel/esp32-nmea2000" data-window="help" id="helpButton">Help</div>
 </div>
-<div id="statusPage" class="tabPage">
-  <div id="statusPageContent">
-    <div class="row">
-      <span class="label">VERSION</span>
-      <span class="value" id="version">---</span>
-      <button class="infoButton" id="converterInfo">?</button>
-    </div>
+<div id="tabPages">
+  <div id="statusPage" class="tabPage">
+    <div id="statusPageContent">
+      <div class="row">
+        <span class="label">VERSION</span>
+        <span class="value" id="version">---</span>
+        <button class="infoButton" id="converterInfo">?</button>
+      </div>
 
-    <div class="row even">
-      <span class="label">Access Point IP</span>
-      <span class="value" id="apIp">---</span>
+      <div class="row even">
+        <span class="label">Access Point IP</span>
+        <span class="value" id="apIp">---</span>
+      </div>
+      <div class="row ">
+        <span class="label">wifi client connected</span>
+        <span class="value" id="wifiConnected">---</span>&nbsp;[<span class="value" id="wifiSSID">---</span>]
+      </div>
+      <div class="row even">
+        <span class="label">wifi client IP</span>
+        <span class="value" id="clientIP">---</span>
+      </div>
+      <div class="row">
+        <span class="label"># clients</span>
+        <span class="value" id="numClients">---</span>
+      </div>
+      <div class="row even">
+        <span class="label">TCP client connected</span>
+        <span class="value" id="clientCon">---</span>
+      </div>
+      <div class="row">
+        <span class="label">TCP client error</span>
+        <span class="value" id="clientErr">---</span>
+      </div>
+      <div class="row even">
+        <span class="label">Free heap</span>
+        <span class="value" id="heap">---</span>
+      </div>
+      <div class="row">
+        <span class="label">NMEA2000 State</span>
+        [<span class="value" id="n2knode">---</span>]&nbsp;
+        <span class="value" id="n2kstate">UNKNOWN</span>
+      </div>
     </div>
-    <div class="row ">
-      <span class="label">wifi client connected</span>
-      <span class="value" id="wifiConnected">---</span>&nbsp;[<span class="value" id="wifiSSID">---</span>]
-    </div>
-    <div class="row even">
-      <span class="label">wifi client IP</span>
-      <span class="value" id="clientIP">---</span>
-    </div>
-    <div class="row">
-      <span class="label"># clients</span>
-      <span class="value" id="numClients">---</span>
-    </div> 
-    <div class="row even">
-      <span class="label">TCP client connected</span>
-      <span class="value" id="clientCon">---</span>
-    </div>
-    <div class="row">
-      <span class="label">TCP client error</span>
-      <span class="value" id="clientErr">---</span>
-    </div> 
-    <div class="row even">
-      <span class="label">Free heap</span>
-      <span class="value" id="heap">---</span>
-    </div>
-    <div class="row">
-      <span class="label">NMEA2000 State</span>
-      [<span class="value" id="n2knode">---</span>]&nbsp;
-      <span class="value" id="n2kstate">UNKNOWN</span>
-    </div> 
+    <button id="reset">Reset</button>
   </div>
-  <button id="reset">Reset</button>
-</div>
-<div class="configForm tabPage hidden" id="configPage" >
-  <div class="buttons">
-    <button id="resetForm">ReloadConfig</button>
-    <button id="forgetPass">ForgetPass</button>
-    <button id="changeConfig">Save&Restart</button>
-    <button id="exportConfig">Export</button>
-    <button id="importConfig">Import</button>
-    <button id="factoryReset">FactoryReset</button>
+  <div class="configForm tabPage hidden" id="configPage">
+    <div class="buttons">
+      <button id="resetForm">ReloadConfig</button>
+      <button id="forgetPass">ForgetPass</button>
+      <button id="changeConfig">Save&Restart</button>
+      <button id="exportConfig">Export</button>
+      <button id="importConfig">Import</button>
+      <button id="factoryReset">FactoryReset</button>
+    </div>
+    <div class="configFormRows">
+
+    </div>
   </div>
-  <div class="configFormRows">
+  <div class="configForm tabPage hidden" id="xdrPage">
+    <div class="buttons">
+      <button id="resetForm">ReloadConfig</button>
+      <button id="changeConfig">Save&Restart</button>
+      <button id="loadUnassigned">Show Unmapped</button>
+      <button id="exportXdr">Export</button>
+      <button id="importXdr">Import</button>
+    </div>
+    <div class="configFormRows">
+
+    </div>
+  </div>
+  <div class="tabPage hidden" id="dashboardPage">
 
   </div>
-</div>
-<div class="configForm tabPage hidden" id="xdrPage" >
-  <div class="buttons">
-    <button id="resetForm">ReloadConfig</button>
-    <button id="changeConfig">Save&Restart</button>
-    <button id="loadUnassigned">Show Unmapped</button>
-    <button id="exportXdr">Export</button>
-    <button id="importXdr">Import</button>
-  </div>
-  <div class="configFormRows">
-
-  </div>
-</div>
-<div class="tabPage hidden" id="dashboardPage">
-
-</div>
-<div class="tabPage hidden" id="updatePage">
-  <div class="row">
-    <span class="label">firmware type</span>
-    <span class="value status-fwtype">---</span>
-  </div>
-  <div class="row">
-    <span class="label">chip type</span>
-    <span class="value status-chipid">---</span>
-  </div>
-  <div class="row">
-    <span class="label">currentVersion</span>
-    <span class="value status-version">---</span>
-  </div>
-  <div class="row">
-    <span class="label">New Firmware</span>
-    <input type="file" name="file1" id="uploadFile">
-  </div>
-  <div class="row">
-    <span class="label"></span>
-    <span id="imageProperties" class="value"></span>
-  </div>
-  <div id="uploadProgress">
-    <div id="uploadDone"></div>
-  </div>
-  <div class="buttons">
-    <button id="uploadBin">Upload</button>
+  <div class="tabPage hidden" id="updatePage">
+    <div class="row">
+      <span class="label">firmware type</span>
+      <span class="value status-fwtype">---</span>
+    </div>
+    <div class="row">
+      <span class="label">chip type</span>
+      <span class="value status-chipid">---</span>
+    </div>
+    <div class="row">
+      <span class="label">currentVersion</span>
+      <span class="value status-version">---</span>
+    </div>
+    <div class="row">
+      <span class="label">New Firmware</span>
+      <input type="file" name="file1" id="uploadFile">
+    </div>
+    <div class="row">
+      <span class="label"></span>
+      <span id="imageProperties" class="value"></span>
+    </div>
+    <div id="uploadProgress">
+      <div id="uploadDone"></div>
+    </div>
+    <div class="buttons">
+      <button id="uploadBin">Upload</button>
+    </div>
   </div>
 </div>
 
diff --git a/web/index.js b/web/index.js
index 1d02096..c0d6ac2 100644
--- a/web/index.js
+++ b/web/index.js
@@ -8,6 +8,7 @@
     let listeners = [];
     let buttonHandlers={};
     let checkers={};
+    let userFormatters={};
     function addEl(type, clazz, parent, text) {
         let el = document.createElement(type);
         if (clazz) {
@@ -19,7 +20,12 @@
             });
         }
         if (text) el.textContent = text;
-        if (parent) parent.appendChild(el);
+        if (parent) {
+            if (typeof(parent) != 'object'){
+                parent=document.querySelector(parent);
+            }
+            if (parent) parent.appendChild(el);
+        }
         return el;
     }
     function forEl(query, callback, base) {
@@ -122,6 +128,7 @@
     function resetForm(ev) {
         getJson("/api/config")
             .then(function (jsonData) {
+                callListeners(api.EVENTS.config,jsonData);
                 for (let k in jsonData) {
                     if (k == "useAdminPass") {
                         needAdminPass = jsonData[k] != 'false';
@@ -1418,16 +1425,15 @@
         }
         let activeTab = document.getElementById(activeName);
         if (!activeTab) return;
-        let all = document.querySelectorAll('.tabPage');
-        for (let i = 0; i < all.length; i++) {
-            all[i].classList.add('hidden');
-        }
-        let tabs = document.querySelectorAll('.tab');
-        for (let i = 0; i < all.length; i++) {
-            tabs[i].classList.remove('active');
-        }
+        forEl('.tabPage',function(pel){
+            pel.classList.add('hidden');
+        });
+        forEl('.tab',function(tel){
+            tel.classList.remove('active');
+        });
         el.classList.add('active');
         activeTab.classList.remove('hidden');
+        callListeners(api.EVENTS.tab,activeName);
     }
     /**
      *
@@ -1602,6 +1608,10 @@
         }
 
 
+    }
+    Object.freeze(valueFormatters);
+    for (let k in valueFormatters){
+        Object.freeze(valueFormatters[k]);
     }
     function resizeFont(el, reset, maxIt) {
         if (maxIt === undefined) maxIt = 10;
@@ -1612,23 +1622,62 @@
             el.style.fontSize = next + "px";
         }
     }
-    function createDashboardItem(name, def, parent) {
-        if (!def.name) return;
-        let frame = addEl('div', 'dash', parent);
-        let title = addEl('span', 'dashTitle', frame, name);
-        let value = addEl('span', 'dashValue', frame);
-        value.setAttribute('id', 'data_' + name);
-        let fmt = valueFormatters[def.format];
-        if (def.format) value.classList.add(def.format);
-        let footer = addEl('div', 'footer', frame);
-        let src = addEl('span', 'source', footer);
-        src.setAttribute('id', 'source_' + name);
+    function getUnit(def,useUser){
+        let fmt = useUser?(userFormatters[def.name] || valueFormatters[def.format]):valueFormatters[def.format] ;
         let u = fmt ? fmt.u : ' ';
         if (!fmt && def.format && def.format.match(/formatXdr/)) {
             u = def.format.replace(/formatXdr:[^:]*:/, '');
         }
-        addEl('span', 'unit', footer, u);
-        return value;
+        return u;
+    }
+    /**
+     * create a dashboard item if it does not exist
+     * @param {*} def 
+     * @param {*} show 
+     * @param {*} parent 
+     * @returns the value div of the dashboard item
+     */
+    function createOrHideDashboardItem(def,show, parent) {
+        if (!def.name) return;
+        let frame=document.getElementById('frame_'+def.name);
+        let build=false;
+        if (frame){
+            if (frame.classList.contains('invalid') && show){
+                build=true;
+                frame.classList.remove('invalid');
+                frame.innerHTML='';
+            }
+        }
+        else{
+            if (! parent) return;
+            frame = addEl('div', 'dash', parent);
+            frame.setAttribute('id','frame_'+def.name);
+            build=true;
+        }
+        if (! show){
+            if (!frame.classList.contains('invalid')){
+                frame.classList.add('invalid');
+                frame.innerHTML='';
+            }
+            return;    
+        }
+        if (build) {
+            let title = addEl('span', 'dashTitle', frame, def.name);
+            let value = addEl('span', 'dashValue', frame);
+            value.setAttribute('id', 'data_' + def.name);
+            if (def.format) value.classList.add(def.format);
+            let footer = addEl('div', 'footer', frame);
+            let src = addEl('span', 'source', footer);
+            src.setAttribute('id', 'source_' + def.name);
+            let u = getUnit(def, true)
+            addEl('span', 'unit', footer, u);
+            callListeners(api.EVENTS.dataItemCreated, frame);
+        }
+        let de = document.getElementById('data_' + def.name);
+        return de;
+    }
+    function hideDashboardItem(name){
+        createOrHideDashboardItem({name:name},false);
     }
     function parseBoatDataLine(line) {
         let rt = {};
@@ -1659,6 +1708,7 @@
     }
     let lastSelectList = [];
     function updateDashboard(data) {
+        callListeners(api.EVENTS.boatData,data);
         let frame = document.getElementById('dashboardPage');
         let showInvalid = true;
         forEl('select[name=showInvalidData]', function (el) {
@@ -1669,24 +1719,17 @@
             let current = parseBoatDataLine(data[n]);
             if (!current.name) continue;
             names[current.name] = true;
-            let de = document.getElementById('data_' + current.name);
-            let isValid = current.valid;
-            if (!de && frame && (isValid || showInvalid)) {
-                de = createDashboardItem(current.name, current, frame);
-            }
-            if (de && (!isValid && !showInvalid)) {
-                de.parentElement.remove();
-                continue;
-            }
+            let show = current.valid||showInvalid;
+            let de=createOrHideDashboardItem(current,show,frame);
             if (de) {
                 let newContent = '----';
                 if (current.valid) {
                     let formatter;
                     if (current.format && current.format != "NULL") {
                         let key = current.format.replace(/^\&/, '');
-                        formatter = valueFormatters[key];
+                        formatter = userFormatters[current.name]|| valueFormatters[key];
                     }
-                    if (formatter) {
+                    if (formatter && formatter.f) {
                         newContent = formatter.f(current.value);
                     }
                     else {
@@ -1711,11 +1754,14 @@
                 src.textContent = sourceName(current.source);
             }
         }
-        console.log("update");
-        forEl('.dashValue', function (el) {
+        //console.log("update");
+        //remove all items that are not send any more
+        //this can only happen if the device restarted
+        //otherwise data items will not go away - they will become invalid
+        forEl('.dash', function (el) {
             let id = el.getAttribute('id');
             if (id) {
-                if (!names[id.replace(/^data_/, '')]) {
+                if (!names[id.replace(/^frame_/, '')]) {
                     el.parentElement.remove();
                 }
             }
@@ -1889,15 +1935,96 @@
             reader.readAsArrayBuffer(slice);
         });
     }
+    function addTabPage(name,label){
+        if (label === undefined) label=name;
+        let tab=addEl('div','tab','#tabs',label);
+        tab.setAttribute('data-page',name);
+        tab.addEventListener('click',function(ev){
+            handleTab(ev.target);
+        })
+        let page=addEl('div','tabPage hidden','#tabPages');
+        page.setAttribute('id',name);
+        return page;
+    }
+    function addUserFormatter(name,unit,formatter){
+        if (unit !== undefined && formatter !== undefined){
+            userFormatters[name]={
+                u:unit,
+                f:formatter
+            }
+        }
+        else{
+            delete userFormatters[name];
+        }
+        hideDashboardItem(name); //will recreate it on next data receive
+    }
     const api= {
         registerListener: function (callback) {
             listeners.push(callback);
         },
+        /**
+         * helper for creating dom elements
+         * parameters:
+         *   type: the element type (e.g. div)
+         *   class: a list of classes separated by space
+         *   parent (opt): a parent element (either a dom element vor a query selector)
+         *   text (opt): the text to be set as textContent 
+         * returns: the newly created element
+         */
         addEl: addEl,
+        /**
+         * iterator helper for a query selector
+         * parameters:
+         *  query: the query selector
+         *  callback: the callback function (will be called with the element as param)
+         *  base (opt): a dome element to be used as the root (defaults to document)
+         */
         forEl: forEl,
+        /**
+         * find the closest parent that has a particular class
+         * parameters:
+         *  element: the element to start with
+         *  class: the class to be searched for
+         * returns: the element or undefined/null
+         */
         closestParent: closestParent,
+        /**
+         * add a new tab
+         * parameters: 
+         *   name - the name of the page
+         *   label (opt): the label for the new page 
+         * returns: the newly created element
+         */
+        addTabPage: addTabPage,
+        /**
+         * add a user defined formatter for a boat data item
+         * parameters:
+         *   name : the boat data item name
+         *   unit: the unit to be displayed
+         *   formatter: the formatter function (must return a string)
+         */
+        addUserFormatter: addUserFormatter,
+        removeUserFormatter: function(name){
+            addUserFormatter(name);
+        },
+        /**
+         * a dict of formatters
+         * each one has 2 members:
+         *   u: the unit
+         *   f: the formatter function
+         */
+        formatters: valueFormatters,
+        /**
+         * parse a line of boat data
+         * the line has name,format,valid,update,source,value
+         */
+        parseBoatDataLine: parseBoatDataLine,
         EVENTS: {
             init: 0, //called when capabilities are loaded, data is capabilities
+            tab: 1, //tab page activated data is the id of the tab page
+            config: 2, //data is the config object
+            boatData: 3, //data is the list of boat Data items
+            dataItemCreated: 4, //data is the frame item of the boat data display
         }
     };
     function callListeners(event,data){

From 47973b6bcf5d38332debafa006583b0d657b64d6 Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Sun, 13 Oct 2024 16:56:21 +0200
Subject: [PATCH 28/58] adjust user formatter api

---
 lib/exampletask/index.js | 49 +++++++++++++++++++++++++++++++++---
 web/index.js             | 54 ++++++++++++++++++++++++----------------
 2 files changed, 78 insertions(+), 25 deletions(-)

diff --git a/lib/exampletask/index.js b/lib/exampletask/index.js
index 6c5322b..eedd205 100644
--- a/lib/exampletask/index.js
+++ b/lib/exampletask/index.js
@@ -1,38 +1,81 @@
 (function(){
     const api=window.esp32nmea2k;
     if (! api) return;
+    //we only do something if a special capability is set
+    //on our case this is "testboard"
+    //so we only start any action when we receive the init event
+    //and we successfully checked that our requested capability is there
     let isActive=false;
     const tabName="example";
     const configName="exampleBDSel";
     let boatItemName;
+    let boatItemElement;
     api.registerListener((id,data)=>{
         if (id === api.EVENTS.init){
             //data is capabilities
+            //check if our requested capability is there (see GwExampleTask.h)
             if (data.testboard) isActive=true;
             if (isActive){
+                //add a simple additional tab page
+                //you will have to build the content of the page dynamically
+                //using normal dom manipulation methods
+                //you can use the helper addEl to create elements
                 let page=api.addTabPage(tabName,"Example");
                 api.addEl('div','',page,"this is a test tab");
             }
         }
         if (isActive){
-            console.log("exampletask listener",id,data);
+            //console.log("exampletask listener",id,data);
             if (id === api.EVENTS.tab){
                 if (data === tabName){
+                    //maybe we need some activity when our page is being activated
                     console.log("example tab activated");
                 }
             }
             if (id == api.EVENTS.config){
+                //we have a configuration that
+                //gives us the name of a boat data item we would like to 
+                //handle special
+                //in our case we just use an own formatter and add some
+                //css to the display field
+                //as this item can change we need to keep track of the 
+                //last item we handled
                 let nextboatItemName=data[configName];
                 console.log("value of "+configName,nextboatItemName);
                 if (nextboatItemName){
-                    api.addUserFormatter(nextboatItemName,"xxx",function(v){
-                        return "X"+v+"X";
+                    //register a user formatter that will be called whenever
+                    //there is a new valid value
+                    //we simply add an "X:" in front
+                    api.addUserFormatter(nextboatItemName,"m(x)",function(v,valid){
+                        if (!valid) return;
+                        return "X:"+v;
                     })
+                    //after this call the item will be recreated
                 }
                 if (boatItemName !== undefined && boatItemName != nextboatItemName){
+                    //if the boat item that we handle has changed, remove
+                    //the previous user formatter (this will recreate the item)
                     api.removeUserFormatter(boatItemName);
                 }
                 boatItemName=nextboatItemName;
+                boatItemElement=undefined;
+            }
+            if (id == api.EVENTS.dataItemCreated){
+                //this event is called whenever a data item has
+                //been created (or recreated)
+                //if this is the item we handle, we just add a css class
+                //we could also completely rebuild the dom below the element
+                //and use our formatter to directly write/draw the data
+                //avoid direct manipulation of the element (i.e. changing the classlist)
+                //as this element remains there all the time
+                if (boatItemName && boatItemName == data.name){
+                    boatItemElement=data.element;
+                    //use the helper forEl to find elements within the dashboard item
+                    //the value element has the class "dashValue"
+                    api.forEl(".dashValue",function(el){
+                        el.classList.add("examplecss");
+                    },boatItemElement);
+                }
             }
         }
     })
diff --git a/web/index.js b/web/index.js
index c0d6ac2..3329628 100644
--- a/web/index.js
+++ b/web/index.js
@@ -1671,7 +1671,7 @@
             src.setAttribute('id', 'source_' + def.name);
             let u = getUnit(def, true)
             addEl('span', 'unit', footer, u);
-            callListeners(api.EVENTS.dataItemCreated, frame);
+            callListeners(api.EVENTS.dataItemCreated,{name:def.name,element:frame});
         }
         let de = document.getElementById('data_' + def.name);
         return de;
@@ -1720,30 +1720,39 @@
             if (!current.name) continue;
             names[current.name] = true;
             let show = current.valid||showInvalid;
-            let de=createOrHideDashboardItem(current,show,frame);
-            if (de) {
-                let newContent = '----';
-                if (current.valid) {
-                    let formatter;
-                    if (current.format && current.format != "NULL") {
-                        let key = current.format.replace(/^\&/, '');
-                        formatter = userFormatters[current.name]|| valueFormatters[key];
-                    }
-                    if (formatter && formatter.f) {
-                        newContent = formatter.f(current.value);
+            let de = createOrHideDashboardItem(current, show, frame);
+            let newContent = '---';
+            if (current.valid) {
+                let formatter;
+                if (current.format && current.format != "NULL") {
+                    let key = current.format.replace(/^\&/, '');
+                    formatter = userFormatters[current.name] || valueFormatters[key];
+                }
+                if (formatter && formatter.f) {
+                    newContent = formatter.f(current.value,true);
+                    if (newContent === undefined) newContent = "";
+                }
+                else {
+                    let v = parseFloat(current.value);
+                    if (!isNaN(v)) {
+                        v = v.toFixed(3)
+                        newContent = v;
                     }
                     else {
-                        let v = parseFloat(current.value);
-                        if (!isNaN(v)) {
-                            v = v.toFixed(3)
-                            newContent = v;
-                        }
-                        else {
-                            newContent = current.value;
-                        }
+                        newContent = current.value;
                     }
                 }
-                else newContent = "---";
+            }
+            else {
+                let uf=userFormatters[current.name];
+                if (uf && uf.f){
+                    //call the user formatter
+                    //so that it can detect the invalid state
+                    newContent=uf.f(undefined,false);
+                }
+                if (newContent === undefined)newContent = "---";
+            }
+            if (de) {
                 if (newContent !== de.textContent) {
                     de.textContent = newContent;
                     resizeFont(de, true);
@@ -2024,7 +2033,8 @@
             tab: 1, //tab page activated data is the id of the tab page
             config: 2, //data is the config object
             boatData: 3, //data is the list of boat Data items
-            dataItemCreated: 4, //data is the frame item of the boat data display
+            dataItemCreated: 4, //data is an object with
+                                // name: the item name, element: the frame item of the boat data display
         }
     };
     function callListeners(event,data){

From 795117b6f4c2189f8b80a0cd7757bfeef0b19afc Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Sun, 13 Oct 2024 17:12:10 +0200
Subject: [PATCH 29/58] allow to add an url tab button

---
 lib/exampletask/index.css | 3 +++
 lib/exampletask/index.js  | 8 +++++++-
 web/index.css             | 1 +
 web/index.js              | 8 ++++++--
 4 files changed, 17 insertions(+), 3 deletions(-)
 create mode 100644 lib/exampletask/index.css

diff --git a/lib/exampletask/index.css b/lib/exampletask/index.css
new file mode 100644
index 0000000..b5b9328
--- /dev/null
+++ b/lib/exampletask/index.css
@@ -0,0 +1,3 @@
+.examplecss{
+    background-color: coral;
+}
\ No newline at end of file
diff --git a/lib/exampletask/index.js b/lib/exampletask/index.js
index eedd205..a85fe10 100644
--- a/lib/exampletask/index.js
+++ b/lib/exampletask/index.js
@@ -8,6 +8,7 @@
     let isActive=false;
     const tabName="example";
     const configName="exampleBDSel";
+    const infoUrl='https://github.com/wellenvogel/esp32-nmea2000/tree/master/lib/exampletask';
     let boatItemName;
     let boatItemElement;
     api.registerListener((id,data)=>{
@@ -21,7 +22,12 @@
                 //using normal dom manipulation methods
                 //you can use the helper addEl to create elements
                 let page=api.addTabPage(tabName,"Example");
-                api.addEl('div','',page,"this is a test tab");
+                api.addEl('div','hdg',page,"this is a test tab");
+                api.addEl('button','',page,'Info').addEventListener('click',function(ev){
+                    window.open(infoUrl,'info');
+                })
+                //add a tab for an external URL
+                api.addTabPage('exhelp','Info',infoUrl);
             }
         }
         if (isActive){
diff --git a/web/index.css b/web/index.css
index f248d97..257b739 100644
--- a/web/index.css
+++ b/web/index.css
@@ -223,6 +223,7 @@ body {
   }
   #tabs {
       display: flex;
+      flex-wrap: wrap;
       border-bottom: 1px solid grey;
       margin-bottom: 0.5em;
   }
diff --git a/web/index.js b/web/index.js
index 3329628..337c21c 100644
--- a/web/index.js
+++ b/web/index.js
@@ -1944,13 +1944,17 @@
             reader.readAsArrayBuffer(slice);
         });
     }
-    function addTabPage(name,label){
+    function addTabPage(name,label,url){
         if (label === undefined) label=name;
         let tab=addEl('div','tab','#tabs',label);
-        tab.setAttribute('data-page',name);
         tab.addEventListener('click',function(ev){
             handleTab(ev.target);
         })
+        if (url !== undefined){
+            tab.setAttribute('data-url',url);
+            return;
+        }
+        tab.setAttribute('data-page',name);
         let page=addEl('div','tabPage hidden','#tabPages');
         page.setAttribute('id',name);
         return page;

From 0ddc0d055db754d2113b3c39070edc6fddec601f Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Fri, 1 Nov 2024 11:57:53 +0100
Subject: [PATCH 30/58] add some doc for serial and USB ports

---
 Readme.md         |  1 +
 doc/serial-usb.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 52 insertions(+)
 create mode 100644 doc/serial-usb.md

diff --git a/Readme.md b/Readme.md
index 40f8d9b..6de6c6a 100644
--- a/Readme.md
+++ b/Readme.md
@@ -47,6 +47,7 @@ Hardware
 --------
 The software is prepared to run on different kinds of ESP32 based modules and accessoirs. For some of them prebuild binaries are available that only need to be flashed, others would require to add some definitions of the used PINs and features and to build the binary.
 For the list of hardware set ups refer to [Hardware](doc/Hardware.md).
+For details of the usage of serial devices and the USB connection refer to [Serial and USB](doc/serial-usb.md).
 
 There is a couple of prebuild binaries that you can directly flash to your device. For other combinations of hardware there is an [online build service](doc/BuildService.md) that will allow you to select your hardware and trigger a build.
 
diff --git a/doc/serial-usb.md b/doc/serial-usb.md
new file mode 100644
index 0000000..428e607
--- /dev/null
+++ b/doc/serial-usb.md
@@ -0,0 +1,51 @@
+Serial and USB
+==============
+The gateway software uses the [arduino layer](https://github.com/espressif/arduino-esp32) on top of the [espressif IDF framework](https://github.com/espressif/esp-idf).
+The handling of serial devices is mainly done by the implementations in the arduino-espressif layer.
+The gateway code adds some buffering on top of this implementation and ensures that normally only full nmea records are sent.
+If the external connection is to slow the gateway will drop complete records.
+All handling of the serial channels is done in the main task of the gateway. 
+
+Serial Devices
+--------------
+THe arduino espressif layer provides the serial devices as [Streams](https://github.com/espressif/arduino-esp32/blob/master/cores/esp32/Stream.h#L48).
+Main implementations are [HardwareSerial](https://github.com/espressif/arduino-esp32/blob/3670e2bf2aca822f2e1225fdb0e0796e490005a8/cores/esp32/HardwareSerial.h#L71) - for the UARTS and [HWCDC](https://github.com/espressif/arduino-esp32/blob/3670e2bf2aca822f2e1225fdb0e0796e490005a8/cores/esp32/HWCDC.h#L43)(C3/S3 only) - for the USB CDC hardware device.
+
+For the github versions: arduino-espressif 3.20009 maps to github tag 2.0.9.
+
+The arduino espressif layer creates a couple of global instances for the serial devices (potentially depending on some defines).
+The important defines for C3/S3 are:
+
+ * ARDUINO_USB_MODE - the gateway always expects this to be 1
+ * ARDUINO_USB_CDC_ON_BOOT - 0 or 1 (CB in the table below)
+
+The created devices for framework 3.20009:
+
+| Device(Variable) | Type(ESP32) | C3/S3 CB=0 | C3/S3 CB=1 |
+| ------------ | ------- | ------ | -----|
+| Serial0 | --- | --- | HardwareSerial(0) |
+| Serial1 | HardwareSerial(1) | HardwareSerial(1) | HardwareSerial(1) |
+| Serial2 | HardwareSerial(2) | HardwareSerial(2) | HardwareSerial(2) |
+| USBSerial | --- | HWCDC | ---- | 
+| Serial | HardwareSerial(0) | HardwareSerial(0) | HWCDC | 
+
+Unfortunately it seems that in newer versions of the framework the devices could change.
+
+The gateway will use the following serial devices:
+
+* USBserial:<br> 
+  For debug output and NMEA as USB connection. If you do not use an S3/C3 with ARDUINO_USB_CDC_ON_BOOT=0 you need to add a <br>_define USBSerial Serial_ somewhere in your build flags or in your task header.<br>
+  Currently the gateway does not support setting the pins for the USB channel (that would be possible in principle only if an external PHY device is used and the USB is connected to a normal UART).
+* Serial1:<br>
+  If you define GWSERIAL_TYPE (1,2,3,4) - see [defines](../lib/hardware/GwHardware.h#23) or GWSERIAL_MODE ("UNI","BI","TX","RX") it will be used for the first serial channel.
+* Serial2:<br>
+  If you define GWSERIAL2_TYPE (1,2,3,4) - see [defines](../lib/hardware/GwHardware.h#23) or GWSERIAL2_MODE ("UNI","BI","TX","RX") it will be used for the second serial channel.
+
+Hints
+-----
+For normal ESP32 chips you need to set <br>_define USBSerial Serial_ <br>and you can use up to 2 serial channels beside the USB channel.
+For C3/S3 chips you can go for 2 options:
+1. set ARDUINO_USB_CDC_ON_BOOT=1: in This case you still need to set<br> _define USBSerial Serial_<br> You can use up to 2 serial channels in the gateway core - but you still have Serial0 available for a third channel (not yet supported by the core - but can be used in your user code)
+2. set ARDUINO_USB_CDC_ON_BOOT=0: in this case USBSerial is already defined as the USB channel. You can use 2 channels in the gateway core and optional you can use Serial in your user code.
+
+If you do not set any of the GWSERIAL* defines (and they are not set by the core hardware definitions) you can freely use Serial1 and / or Serial2 in your user code.

From 56ec7a04068303289b0544d2915e3489a849ac22 Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Fri, 1 Nov 2024 17:01:44 +0100
Subject: [PATCH 31/58] intermediate: restructure serial handling

---
 lib/channel/GwChannelList.cpp | 13 +++++++------
 lib/channel/GwChannelList.h   | 11 +++--------
 lib/serial/GwSerial.cpp       |  4 ++--
 lib/serial/GwSerial.h         | 22 ++++++++++++++++++++--
 4 files changed, 32 insertions(+), 18 deletions(-)

diff --git a/lib/channel/GwChannelList.cpp b/lib/channel/GwChannelList.cpp
index c736ae2..58b96b5 100644
--- a/lib/channel/GwChannelList.cpp
+++ b/lib/channel/GwChannelList.cpp
@@ -83,7 +83,7 @@ public:
 };
 
 template<typename T>
-    class SerialWrapper : public GwChannelList::SerialWrapperBase{
+    class SerialWrapper : public GwSerial::SerialWrapperBase{
         private:
         template<class C>
         void beginImpl(C *s,unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1){}
@@ -184,7 +184,7 @@ void GwChannelList::addSerial(int id, int rx, int tx, int type){
     }
     LOG_DEBUG(GwLog::ERROR,"invalid serial config with id %d",id);
 }
-void GwChannelList::addSerial(GwChannelList::SerialWrapperBase *stream,int type,int rx,int tx){
+void GwChannelList::addSerial(GwSerial::SerialWrapperBase *stream,int type,int rx,int tx){
     const char *mode=nullptr;
     switch (type)
     {
@@ -207,7 +207,7 @@ void GwChannelList::addSerial(GwChannelList::SerialWrapperBase *stream,int type,
     }
     addSerial(stream,mode,rx,tx);
 }
-void GwChannelList::addSerial(GwChannelList::SerialWrapperBase *serialStream,const String &mode,int rx,int tx){
+void GwChannelList::addSerial(GwSerial::SerialWrapperBase *serialStream,const String &mode,int rx,int tx){
     int id=serialStream->getId();
     for (auto &&it:theChannels){
         if (it->isOwnSource(id)){
@@ -251,7 +251,7 @@ void GwChannelList::addSerial(GwChannelList::SerialWrapperBase *serialStream,con
     LOG_DEBUG(GwLog::DEBUG,"serial set up: mode=%s,rx=%d,canRead=%d,tx=%d,canWrite=%d",
         mode.c_str(),rx,(int)canRead,tx,(int)canWrite);
     serialStream->begin(logger,config->getInt(param->baud,115200),SERIAL_8N1,rx,tx);
-    GwSerial *serial = new GwSerial(logger, serialStream->getStream(), id, canRead);
+    GwSerial *serial = new GwSerial(logger, serialStream, canRead);
     LOG_DEBUG(GwLog::LOG, "starting serial %d ", id);
     GwChannel *channel = new GwChannel(logger, param->name, id);
     channel->setImpl(serial);
@@ -303,8 +303,9 @@ void GwChannelList::begin(bool fallbackSerial){
     GwChannel *channel=NULL;
     //usb
     if (! fallbackSerial){
-        GwSerial *usb=new GwSerial(NULL,&USBSerial,USB_CHANNEL_ID);
-        USBSerial.begin(config->getInt(config->usbBaud));
+        GwSerial::SerialWrapperBase *usbWrapper=new SerialWrapper<decltype(USBSerial)>(&USBSerial,USB_CHANNEL_ID);
+        usbWrapper->begin(NULL,config->getInt(config->usbBaud));
+        GwSerial *usb=new GwSerial(NULL,usbWrapper);
         logger->setWriter(new GwSerialLog(usb,config->getBool(config->usbActisense),getFlushTimeout(USBSerial)));
         logger->prefix="GWSERIAL:";
         channel=new GwChannel(logger,"USB",USB_CHANNEL_ID);
diff --git a/lib/channel/GwChannelList.h b/lib/channel/GwChannelList.h
index 373dc6a..6ca20f8 100644
--- a/lib/channel/GwChannelList.h
+++ b/lib/channel/GwChannelList.h
@@ -8,6 +8,7 @@
 #include "GWConfig.h"
 #include "GwJsonDocument.h"
 #include "GwApi.h"
+#include "GwSerial.h"
 #include <HardwareSerial.h>
 
 //NMEA message channels
@@ -23,12 +24,6 @@ class GwSocketServer;
 class GwTcpClient;
 class GwChannelList{
     private:
-    class SerialWrapperBase{
-        public:
-        virtual void begin(GwLog* logger,unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1)=0;
-        virtual Stream *getStream()=0;
-        virtual int getId()=0;
-    };
         GwLog *logger;
         GwConfigHandler *config;
         typedef std::vector<GwChannel *> ChannelList;
@@ -36,8 +31,8 @@ class GwChannelList{
         std::map<int,String> modes;
         GwSocketServer *sockets;
         GwTcpClient *client;
-        void addSerial(SerialWrapperBase *stream,const String &mode,int rx,int tx);
-        void addSerial(SerialWrapperBase *stream,int type,int rx,int tx);
+        void addSerial(GwSerial::SerialWrapperBase *stream,const String &mode,int rx,int tx);
+        void addSerial(GwSerial::SerialWrapperBase *stream,int type,int rx,int tx);
     public:
         void addSerial(int id, int rx, int tx, int type);
         GwChannelList(GwLog *logger, GwConfigHandler *config);
diff --git a/lib/serial/GwSerial.cpp b/lib/serial/GwSerial.cpp
index c810e58..28c8d34 100644
--- a/lib/serial/GwSerial.cpp
+++ b/lib/serial/GwSerial.cpp
@@ -40,10 +40,10 @@ class GwSerialStream: public Stream{
 
 
 
-GwSerial::GwSerial(GwLog *logger, Stream *s, int id,bool allowRead):serial(s)
+GwSerial::GwSerial(GwLog *logger, GwSerial::SerialWrapperBase *s, bool allowRead):serial(s)
 {
     LOG_DEBUG(GwLog::DEBUG,"creating GwSerial %p id %d",this,id);
-    this->id=id;
+    this->id=s->getId();
     this->logger = logger;
     String bufName="Ser(";
     bufName+=String(id);
diff --git a/lib/serial/GwSerial.h b/lib/serial/GwSerial.h
index 5298bc6..f50149e 100644
--- a/lib/serial/GwSerial.h
+++ b/lib/serial/GwSerial.h
@@ -16,11 +16,27 @@ class GwSerial : public GwChannelInterface{
         int id=-1;
         int overflows=0;
         size_t enqueue(const uint8_t *data, size_t len,bool partial=false);
-        Stream *serial;
         bool availableWrite=false; //if this is false we will wait for availabkleWrite until we flush again
     public:
+        class SerialWrapperBase{
+        public:
+        virtual void begin(GwLog* logger,unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1)=0;
+        virtual int getId()=0;
+        virtual int available(){return getStream()->available();}
+        size_t readBytes(uint8_t *buffer, size_t length){
+            return getStream()->readBytes(buffer,length);
+        }
+        virtual int availableForWrite(void){
+            return getStream()->availableForWrite();
+        }
+        size_t write(const uint8_t *buffer, size_t size){
+            return getStream()->write(buffer,size);
+        }
+        private:
+        virtual Stream *getStream()=0;
+        };
         static const int bufferSize=200;
-        GwSerial(GwLog *logger,Stream *stream,int id,bool allowRead=true);
+        GwSerial(GwLog *logger,SerialWrapperBase *stream,bool allowRead=true);
         ~GwSerial();
         bool isInitialized();
         virtual size_t sendToClients(const char *buf,int sourceId,bool partial=false);
@@ -30,5 +46,7 @@ class GwSerial : public GwChannelInterface{
         virtual Stream *getStream(bool partialWrites);
         bool getAvailableWrite(){return availableWrite;}
     friend GwSerialStream;
+    private:
+        SerialWrapperBase *serial;
 };
 #endif
\ No newline at end of file

From c6f601377ca3babb437242779fc604a91041744c Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Sun, 3 Nov 2024 16:15:52 +0100
Subject: [PATCH 32/58] intermediate,untested: reorganize channel handling

---
 lib/channel/GwChannel.cpp        |  17 +-
 lib/channel/GwChannel.h          |   7 +-
 lib/channel/GwChannelInterface.h |   1 +
 lib/channel/GwChannelList.cpp    | 325 ++++++++++++++-----------------
 lib/channel/GwChannelList.h      |   5 +-
 lib/hardware/GwHardware.h        |   1 +
 lib/serial/GwSerial.cpp          |  38 +++-
 lib/serial/GwSerial.h            |  79 +++++---
 8 files changed, 249 insertions(+), 224 deletions(-)

diff --git a/lib/channel/GwChannel.cpp b/lib/channel/GwChannel.cpp
index 93a2da3..069b5ae 100644
--- a/lib/channel/GwChannel.cpp
+++ b/lib/channel/GwChannel.cpp
@@ -218,14 +218,23 @@ void GwChannel::sendActisense(const tN2kMsg &msg, int sourceId){
     if (!enabled || ! impl || ! writeActisense || ! channelStream) return;
     //currently actisense only for channels with a single source id
     //so we can check it here
-    if (isOwnSource(sourceId)) return;
+    if (maxSourceId < 0 && this->sourceId == sourceId) return;
+    if (sourceId >= this->sourceId && sourceId <= maxSourceId) return;
     countOut->add(String(msg.PGN)); 
     msg.SendInActisenseFormat(channelStream);
 }
 
-bool GwChannel::isOwnSource(int id){
-    if (maxSourceId < 0) return id == sourceId;
-    else return (id >= sourceId && id <= maxSourceId);
+bool GwChannel::overlaps(const GwChannel *other) const{
+    if (maxSourceId < 0){
+        if (other->maxSourceId < 0) return sourceId == other->sourceId;
+        return (other->sourceId <= sourceId && other->maxSourceId >= sourceId);
+    }
+    if (other->maxSourceId < 0){
+        return other->sourceId >= sourceId && other->sourceId <= maxSourceId;
+    }
+    if (other->maxSourceId < sourceId) return false;
+    if (other->sourceId > maxSourceId) return false;
+    return true;
 }
 
 unsigned long GwChannel::countRx(){
diff --git a/lib/channel/GwChannel.h b/lib/channel/GwChannel.h
index 77af597..951290b 100644
--- a/lib/channel/GwChannel.h
+++ b/lib/channel/GwChannel.h
@@ -50,7 +50,7 @@ class GwChannel{
     );
 
     void setImpl(GwChannelInterface *impl);
-    bool isOwnSource(int id);
+    bool overlaps(const GwChannel *) const;
     void enable(bool enabled){
         this->enabled=enabled;
     }
@@ -73,5 +73,10 @@ class GwChannel{
     void sendActisense(const tN2kMsg &msg, int sourceId);
     unsigned long countRx();
     unsigned long countTx();
+    bool isOwnSource(int source){
+        if (maxSourceId < 0) return source == sourceId;
+        return (source >= sourceId && source <= maxSourceId);
+    }
+    String getMode(){return impl->getMode();}
 };
 
diff --git a/lib/channel/GwChannelInterface.h b/lib/channel/GwChannelInterface.h
index f9b076c..68f519b 100644
--- a/lib/channel/GwChannelInterface.h
+++ b/lib/channel/GwChannelInterface.h
@@ -6,4 +6,5 @@ class GwChannelInterface{
         virtual void readMessages(GwMessageFetcher *writer)=0;
         virtual size_t sendToClients(const char *buffer, int sourceId, bool partial=false)=0;
         virtual Stream * getStream(bool partialWrites){ return NULL;}
+        virtual String getMode(){return "UNKNOWN";}
 };
\ No newline at end of file
diff --git a/lib/channel/GwChannelList.cpp b/lib/channel/GwChannelList.cpp
index 58b96b5..c09966c 100644
--- a/lib/channel/GwChannelList.cpp
+++ b/lib/channel/GwChannelList.cpp
@@ -18,16 +18,53 @@ class SerInit{
 };
 std::vector<SerInit> serialInits;
 
+static int typeFromMode(const char *mode){
+    if (strcmp(mode,"UNI") == 0) return GWSERIAL_TYPE_UNI;
+    if (strcmp(mode,"BI") == 0) return GWSERIAL_TYPE_BI;
+    if (strcmp(mode,"RX") == 0) return GWSERIAL_TYPE_RX;
+    if (strcmp(mode,"TX") == 0) return GWSERIAL_TYPE_TX;
+    return GWSERIAL_TYPE_UNK;
+}
+
 #define CFG_SERIAL(ser,...) \
     __MSG("serial config " #ser); \
     static GwInitializer<SerInit> __serial ## ser ## _init \
         (serialInits,SerInit(ser,__VA_ARGS__));
 #ifdef _GWI_SERIAL1
-    CFG_SERIAL(1,_GWI_SERIAL1)
+    CFG_SERIAL(0,_GWI_SERIAL1)
 #endif
 #ifdef _GWI_SERIAL2
-    CFG_SERIAL(2,_GWI_SERIAL2)
+    CFG_SERIAL(1,_GWI_SERIAL2)
 #endif
+//handle separate defines
+    //serial 1
+    #ifndef GWSERIAL_TX
+      #define GWSERIAL_TX -1
+    #endif
+    #ifndef GWSERIAL_RX
+      #define GWSERIAL_RX -1
+    #endif
+    #ifdef GWSERIAL_TYPE
+        CFG_SERIAL(0,GWSERIAL_RX,GWSERIAL_TX,GWSERIAL_TYPE)
+    #else
+        #ifdef GWSERIAL_MODE
+            CFG_SERIAL(0,GWSERIAL_RX,GWSERIAL_TX,typeFromMode(GWSERIAL_MODE))
+        #endif
+    #endif
+    //serial 2
+    #ifndef GWSERIAL2_TX
+      #define GWSERIAL2_TX -1
+    #endif
+    #ifndef GWSERIAL2_RX
+      #define GWSERIAL2_RX -1
+    #endif
+    #ifdef GWSERIAL2_TYPE
+        CFG_SERIAL(1,GWSERIAL2_RX,GWSERIAL2_TX,GWSERIAL2_TYPE)
+    #else
+        #ifdef GWSERIAL2_MODE
+            CFG_SERIAL(1,GWSERIAL2_RX,GWSERIAL2_TX,typeFromMode(GWSERIAL2_MODE))
+        #endif
+    #endif
 class GwSerialLog : public GwLogWriter
 {
     static const size_t bufferSize = 4096;
@@ -35,13 +72,11 @@ class GwSerialLog : public GwLogWriter
     int wp = 0;
     GwSerial *writer;
     bool disabled = false;
-    long flushTimeout=200;
 public:
-    GwSerialLog(GwSerial *writer, bool disabled,long flushTimeout=200)
+    GwSerialLog(GwSerial *writer, bool disabled)
     {
         this->writer = writer;
         this->disabled = disabled;
-        this->flushTimeout=flushTimeout;
         logBuffer = new char[bufferSize];
         wp = 0;
     }
@@ -64,7 +99,7 @@ public:
         {
             while (handled < wp)
             {
-                if ( !writer->flush(flushTimeout)) break;
+                if ( !writer->flush()) break;
                 size_t rt = writer->sendToClients(logBuffer + handled, -1, true);
                 handled += rt;
             }
@@ -82,44 +117,6 @@ public:
     }
 };
 
-template<typename T>
-    class SerialWrapper : public GwSerial::SerialWrapperBase{
-        private:
-        template<class C>
-        void beginImpl(C *s,unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1){}
-        void beginImpl(HardwareSerial *s,unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1){
-            s->begin(baud,config,rxPin,txPin);
-        }
-        template<class C>
-        void setError(C* s, GwLog *logger){}
-        void setError(HardwareSerial *s,GwLog *logger){
-            LOG_DEBUG(GwLog::LOG,"enable serial errors for channel %d",id);
-            s->onReceiveError([logger,this](hardwareSerial_error_t err){
-                LOG_DEBUG(GwLog::ERROR,"serial error on id %d: %d",this->id,(int)err);
-            });
-        }
-        #if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3
-            void beginImpl(HWCDC *s,unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1){
-            s->begin(baud);
-        }
-        #endif
-        T *serial;
-        int id;
-        public:
-        SerialWrapper(T* s,int i):serial(s),id(i){}
-        virtual void begin(GwLog* logger,unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1) override{
-            beginImpl(serial,baud,config,rxPin,txPin);
-            setError(serial,logger);
-        };
-        virtual Stream *getStream() override{
-            return serial;
-        }
-        virtual int getId() override{
-            return id;
-        }
-
-    };
-
 
 GwChannelList::GwChannelList(GwLog *logger, GwConfigHandler *config){
     this->logger=logger;
@@ -139,10 +136,27 @@ typedef struct {
     const char *toN2K;
     const char *readF;
     const char *writeF;
+    const char *preventLog;
+    const char *readAct;
+    const char *writeAct;
     const char *name;
 } SerialParam;
 
 static  SerialParam serialParameters[]={
+    {
+        .id=USB_CHANNEL_ID,
+        .baud=GwConfigDefinitions::usbBaud,
+        .receive=GwConfigDefinitions::receiveUsb,
+        .send=GwConfigDefinitions::sendUsb,
+        .direction="",
+        .toN2K=GwConfigDefinitions::usbToN2k,
+        .readF=GwConfigDefinitions::usbReadFilter,
+        .writeF=GwConfigDefinitions::usbWriteFilter,
+        .preventLog=GwConfigDefinitions::usbActisense,
+        .readAct=GwConfigDefinitions::usbActisense,
+        .writeAct=GwConfigDefinitions::usbActSend,
+        .name="USB"
+    },
     {
         .id=SERIAL1_CHANNEL_ID,
         .baud=GwConfigDefinitions::serialBaud,
@@ -152,6 +166,9 @@ static  SerialParam serialParameters[]={
         .toN2K=GwConfigDefinitions::serialToN2k,
         .readF=GwConfigDefinitions::serialReadF,
         .writeF=GwConfigDefinitions::serialWriteF,
+        .preventLog="",
+        .readAct="",
+        .writeAct="",
         .name="Serial"
     },
     {
@@ -163,81 +180,38 @@ static  SerialParam serialParameters[]={
         .toN2K=GwConfigDefinitions::serial2ToN2k,
         .readF=GwConfigDefinitions::serial2ReadF,
         .writeF=GwConfigDefinitions::serial2WriteF,
+        .preventLog="",
+        .readAct="",
+        .writeAct="",
         .name="Serial2"
     }
 };
 
-static SerialParam *getSerialParam(int id){
-    for (size_t idx=0;idx<sizeof(serialParameters)/sizeof(SerialParam*);idx++){
-        if (serialParameters[idx].id == id) return &serialParameters[idx];
-    }
-    return nullptr;
-}
-void GwChannelList::addSerial(int id, int rx, int tx, int type){
-    if (id == 1){
-        addSerial(new SerialWrapper<decltype(Serial1)>(&Serial1,SERIAL1_CHANNEL_ID),type,rx,tx);
-        return;   
-    }
-    if (id == 2){
-        addSerial(new SerialWrapper<decltype(Serial2)>(&Serial2,SERIAL2_CHANNEL_ID),type,rx,tx);
-        return;   
-    }
-    LOG_DEBUG(GwLog::ERROR,"invalid serial config with id %d",id);
-}
-void GwChannelList::addSerial(GwSerial::SerialWrapperBase *stream,int type,int rx,int tx){
-    const char *mode=nullptr;
-    switch (type)
-    {
-    case GWSERIAL_TYPE_UNI:
-        mode="UNI";
-        break;
-    case GWSERIAL_TYPE_BI:
-        mode="BI";
-        break;
-    case GWSERIAL_TYPE_RX:
-        mode="RX";
-        break;
-    case GWSERIAL_TYPE_TX:
-        mode="TX";
-        break;
-    }
-    if (mode == nullptr) {
-        LOG_DEBUG(GwLog::ERROR,"unknown serial type %d",type);
-        return;
-    }
-    addSerial(stream,mode,rx,tx);
-}
-void GwChannelList::addSerial(GwSerial::SerialWrapperBase *serialStream,const String &mode,int rx,int tx){
-    int id=serialStream->getId();
-    for (auto &&it:theChannels){
-        if (it->isOwnSource(id)){
-            LOG_DEBUG(GwLog::ERROR,"trying to re-add serial id=%d, ignoring",id);
-            return;
-        }
-    }
-    SerialParam *param=getSerialParam(id);
-    if (param == nullptr){
-        logger->logDebug(GwLog::ERROR,"trying to set up an unknown serial channel: %d",id);
-        return;
-    }
-    if (rx < 0 && tx < 0){
-        logger->logDebug(GwLog::ERROR,"useless config for serial %d: both rx/tx undefined");
-        return;
-    }
-    modes[id]=String(mode);
+template<typename T>
+GwSerial* createSerial(GwLog *logger, T* s,int id, bool canRead=true){
+    return new GwSerialImpl<T>(logger,s,id,canRead);
+} 
+
+static GwChannel * createSerialChannel(GwConfigHandler *config,GwLog *logger, int idx,int type,int rx,int tx, bool setLog=false){
+    if (idx < 0 || idx >= sizeof(serialParameters)/sizeof(SerialParam*)) return nullptr;
+    SerialParam *param=&serialParameters[idx];
     bool canRead=false;
     bool canWrite=false;
-    if (mode == "BI"){
+    bool validType=false;
+    if (type == GWSERIAL_TYPE_BI){
         canRead=config->getBool(param->receive);
         canWrite=config->getBool(param->send);
+        validType=true;
     }
-    if (mode == "TX"){
+    if (type == GWSERIAL_TYPE_TX){
         canWrite=true;
+        validType=true;
     }
-    if (mode == "RX"){
+    if (type == GWSERIAL_TYPE_RX){
         canRead=true;
+        validType=true;
     }
-    if (mode == "UNI"){
+    if (type == GWSERIAL_TYPE_UNI ){
         String cfgMode=config->getString(param->direction);
         if (cfgMode == "receive"){
             canRead=true;
@@ -245,16 +219,41 @@ void GwChannelList::addSerial(GwSerial::SerialWrapperBase *serialStream,const St
         if (cfgMode == "send"){
             canWrite=true;
         }
+        validType=true;
+    }
+    if (! validType){
+        LOG_DEBUG(GwLog::ERROR,"invalid type for serial channel %d: %d",param->id,type);
+        return nullptr;
     }
     if (rx < 0) canRead=false;
     if (tx < 0) canWrite=false;
-    LOG_DEBUG(GwLog::DEBUG,"serial set up: mode=%s,rx=%d,canRead=%d,tx=%d,canWrite=%d",
-        mode.c_str(),rx,(int)canRead,tx,(int)canWrite);
-    serialStream->begin(logger,config->getInt(param->baud,115200),SERIAL_8N1,rx,tx);
-    GwSerial *serial = new GwSerial(logger, serialStream, canRead);
-    LOG_DEBUG(GwLog::LOG, "starting serial %d ", id);
-    GwChannel *channel = new GwChannel(logger, param->name, id);
-    channel->setImpl(serial);
+    LOG_DEBUG(GwLog::DEBUG,"serial set up: type=%d,rx=%d,canRead=%d,tx=%d,canWrite=%d",
+        type,rx,(int)canRead,tx,(int)canWrite);
+    GwSerial *serialStream=nullptr;
+    GwLog *streamLog=setLog?nullptr:logger;
+    switch(param->id){
+        case USB_CHANNEL_ID:
+            serialStream=createSerial(streamLog,&USBSerial,param->id);
+            break;
+        case SERIAL1_CHANNEL_ID:
+            serialStream=createSerial(streamLog,&Serial1,param->id);
+            break;
+        case SERIAL2_CHANNEL_ID:
+            serialStream=createSerial(streamLog,&Serial2,param->id);
+            break;
+    }
+    if (serialStream == nullptr){
+        LOG_DEBUG(GwLog::ERROR,"invalid serial config with id %d",param->id);
+        return nullptr;
+    }
+    serialStream->begin(config->getInt(param->baud,115200),SERIAL_8N1,rx,tx);
+    if (setLog){
+        logger->setWriter(new GwSerialLog(serialStream,config->getBool(param->preventLog,false)));
+        logger->prefix="GWSERIAL:";
+    }
+    LOG_DEBUG(GwLog::LOG, "starting serial %d ", param->id);
+    GwChannel *channel = new GwChannel(logger, param->name,param->id);
+    channel->setImpl(serialStream);
     channel->begin(
         canRead || canWrite,
         canWrite,
@@ -263,9 +262,20 @@ void GwChannelList::addSerial(GwSerial::SerialWrapperBase *serialStream,const St
         config->getString(param->writeF),
         false,
         config->getBool(param->toN2K),
-        false,
-        false);
-    LOG_DEBUG(GwLog::LOG, "%s", channel->toString().c_str());
+        config->getBool(param->readAct),
+        config->getBool(param->writeAct));
+    return channel;
+}
+void GwChannelList::addChannel(GwChannel * channel){
+    for (auto &&it:theChannels){
+        if (it->overlaps(channel)){
+            LOG_DEBUG(GwLog::ERROR,"trying to add channel with overlapping ids %s (%s), ignoring",
+                channel->toString().c_str(),
+                it->toString().c_str());
+            return;
+        }
+    }
+    LOG_DEBUG(GwLog::LOG, "adding channel %s", channel->toString().c_str());
     theChannels.push_back(channel);
 }
 void GwChannelList::preinit(){
@@ -290,38 +300,20 @@ void GwChannelList::preinit(){
         }
     }
 }
-template<typename S>
-long getFlushTimeout(S &s){
-    return 200;
-}
-template<>
-long getFlushTimeout(HardwareSerial &s){
-    return 2000;
-}
+#ifndef GWUSB_TX
+  #define GWUSB_TX -1
+#endif
+#ifndef GWUSB_RX
+  #define GWUSB_RX -1
+#endif
+
 void GwChannelList::begin(bool fallbackSerial){
     LOG_DEBUG(GwLog::DEBUG,"GwChannelList::begin");
     GwChannel *channel=NULL;
     //usb
     if (! fallbackSerial){
-        GwSerial::SerialWrapperBase *usbWrapper=new SerialWrapper<decltype(USBSerial)>(&USBSerial,USB_CHANNEL_ID);
-        usbWrapper->begin(NULL,config->getInt(config->usbBaud));
-        GwSerial *usb=new GwSerial(NULL,usbWrapper);
-        logger->setWriter(new GwSerialLog(usb,config->getBool(config->usbActisense),getFlushTimeout(USBSerial)));
-        logger->prefix="GWSERIAL:";
-        channel=new GwChannel(logger,"USB",USB_CHANNEL_ID);
-        channel->setImpl(usb);
-        channel->begin(true,
-            config->getBool(config->sendUsb),
-            config->getBool(config->receiveUsb),
-            config->getString(config->usbReadFilter),
-            config->getString(config->usbWriteFilter),
-            false,
-            config->getBool(config->usbToN2k),
-            config->getBool(config->usbActisense),
-            config->getBool(config->usbActSend)
-        );
-        theChannels.push_back(channel);
-        LOG_DEBUG(GwLog::LOG,"%s",channel->toString().c_str());
+        GwChannel *usb=createSerialChannel(config, logger,0,GWSERIAL_TYPE_BI,GWUSB_RX,GWUSB_TX,true);
+        addChannel(usb);
     }
     //TCP server
     sockets=new GwSocketServer(config,logger,MIN_TCP_CHANNEL_ID);
@@ -339,42 +331,13 @@ void GwChannelList::begin(bool fallbackSerial){
         false,
         false
     );
-    LOG_DEBUG(GwLog::LOG,"%s",channel->toString().c_str());
-    theChannels.push_back(channel);
+    addChannel(channel);
 
     //new serial config handling
     for (auto &&init:serialInits){
-        addSerial(init.serial,init.rx,init.tx,init.mode);
+        (logger,init.serial,init.rx,init.tx,init.mode);
     }
-    //handle separate defines
-    //serial 1
-    #ifndef GWSERIAL_TX
-      #define GWSERIAL_TX -1
-    #endif
-    #ifndef GWSERIAL_RX
-      #define GWSERIAL_RX -1
-    #endif
-    #ifdef GWSERIAL_TYPE
-        addSerial(new SerialWrapper<decltype(Serial1)>(&Serial1,SERIAL1_CHANNEL_ID),GWSERIAL_TYPE,GWSERIAL_RX,GWSERIAL_TX);
-    #else
-        #ifdef GWSERIAL_MODE
-            addSerial(new SerialWrapper<decltype(Serial1)>(&Serial1,SERIAL1_CHANNEL_ID),GWSERIAL_MODE,GWSERIAL_RX,GWSERIAL_TX);
-        #endif
-    #endif
-    //serial 2
-    #ifndef GWSERIAL2_TX
-      #define GWSERIAL2_TX -1
-    #endif
-    #ifndef GWSERIAL2_RX
-      #define GWSERIAL2_RX -1
-    #endif
-    #ifdef GWSERIAL2_TYPE
-        addSerial(new SerialWrapper<decltype(Serial2)>(&Serial2,SERIAL2_CHANNEL_ID),GWSERIAL2_TYPE,GWSERIAL2_RX,GWSERIAL2_TX);
-    #else
-        #ifdef GWSERIAL2_MODE
-            addSerial(new SerialWrapper<decltype(Serial2)>(&Serial2,SERIAL2_CHANNEL_ID),GWSERIAL2_MODE,GWSERIAL2_RX,GWSERIAL2_TX);
-        #endif
-    #endif
+    
     //tcp client
     bool tclEnabled=config->getBool(config->tclEnabled);
     channel=new GwChannel(logger,"TCPClient",TCP_CLIENT_CHANNEL_ID);
@@ -398,13 +361,13 @@ void GwChannelList::begin(bool fallbackSerial){
         false,
         false
         );
-    theChannels.push_back(channel);
-    LOG_DEBUG(GwLog::LOG,"%s",channel->toString().c_str());  
+    addChannel(channel);
     logger->flush();
 }
 String GwChannelList::getMode(int id){
-    auto it=modes.find(id);
-    if (it != modes.end()) return it->second;
+    for (auto && c: theChannels){
+        if (c->isOwnSource(id)) return c->getMode();
+    }
     return "UNKNOWN";
 }
 int GwChannelList::getJsonSize(){
@@ -429,8 +392,8 @@ void GwChannelList::toJson(GwJsonDocument &doc){
     });
 }
 GwChannel *GwChannelList::getChannelById(int sourceId){
-    for (auto it=theChannels.begin();it != theChannels.end();it++){
-        if ((*it)->isOwnSource(sourceId)) return *it;
+    for (auto && it: theChannels){
+        if (it->isOwnSource(sourceId)) return it;
     }
     return NULL;
 }
diff --git a/lib/channel/GwChannelList.h b/lib/channel/GwChannelList.h
index 6ca20f8..ba45c66 100644
--- a/lib/channel/GwChannelList.h
+++ b/lib/channel/GwChannelList.h
@@ -28,13 +28,10 @@ class GwChannelList{
         GwConfigHandler *config;
         typedef std::vector<GwChannel *> ChannelList;
         ChannelList theChannels;
-        std::map<int,String> modes;
         GwSocketServer *sockets;
         GwTcpClient *client;
-        void addSerial(GwSerial::SerialWrapperBase *stream,const String &mode,int rx,int tx);
-        void addSerial(GwSerial::SerialWrapperBase *stream,int type,int rx,int tx);
     public:
-        void addSerial(int id, int rx, int tx, int type);
+        void addChannel(GwChannel *);
         GwChannelList(GwLog *logger, GwConfigHandler *config);
         typedef std::function<void(GwChannel *)> ChannelAction;
         void allChannels(ChannelAction action);
diff --git a/lib/hardware/GwHardware.h b/lib/hardware/GwHardware.h
index 8306409..0b14ab3 100644
--- a/lib/hardware/GwHardware.h
+++ b/lib/hardware/GwHardware.h
@@ -24,6 +24,7 @@
 #define GWSERIAL_TYPE_BI 2
 #define GWSERIAL_TYPE_RX 3
 #define GWSERIAL_TYPE_TX 4
+#define GWSERIAL_TYPE_UNK 0
 #include <GwConfigItem.h>
 #include <HardwareSerial.h>
 #include "GwAppInfo.h"
diff --git a/lib/serial/GwSerial.cpp b/lib/serial/GwSerial.cpp
index 28c8d34..c0fb06e 100644
--- a/lib/serial/GwSerial.cpp
+++ b/lib/serial/GwSerial.cpp
@@ -1,4 +1,5 @@
 #include "GwSerial.h"
+#include "GwHardware.h"
 
 class GwSerialStream: public Stream{
     private:
@@ -40,11 +41,13 @@ class GwSerialStream: public Stream{
 
 
 
-GwSerial::GwSerial(GwLog *logger, GwSerial::SerialWrapperBase *s, bool allowRead):serial(s)
+GwSerial::GwSerial(GwLog *logger, Stream * stream,int id,int type,bool allowRead)
 {
     LOG_DEBUG(GwLog::DEBUG,"creating GwSerial %p id %d",this,id);
-    this->id=s->getId();
     this->logger = logger;
+    this->id=id;
+    this->stream=stream;
+    this->type=type;
     String bufName="Ser(";
     bufName+=String(id);
     bufName+=")";
@@ -62,6 +65,20 @@ GwSerial::~GwSerial()
     if (readBuffer) delete readBuffer;
 }
 
+String GwSerial::getMode(){
+    switch (type){
+        case GWSERIAL_TYPE_UNI:
+            return "UNI";
+        case GWSERIAL_TYPE_BI:
+            return "BI";
+        case GWSERIAL_TYPE_RX:
+            return "RX";
+        case GWSERIAL_TYPE_TX:
+            return "TX";
+    }
+    return "UNKNOWN";
+}
+
 bool GwSerial::isInitialized() { return initialized; }
 size_t GwSerial::enqueue(const uint8_t *data, size_t len, bool partial)
 {
@@ -70,9 +87,9 @@ size_t GwSerial::enqueue(const uint8_t *data, size_t len, bool partial)
 }
 GwBuffer::WriteStatus GwSerial::write(){
     if (! isInitialized()) return GwBuffer::ERROR;
-    size_t numWrite=serial->availableForWrite();          
+    size_t numWrite=availableForWrite();          
     size_t rt=buffer->fetchData(numWrite,[](uint8_t *buffer,size_t len, void *p){
-        return ((GwSerial *)p)->serial->write(buffer,len);
+        return ((GwSerial *)p)->stream->write(buffer,len);
     },this);
     if (rt != 0){
         LOG_DEBUG(GwLog::DEBUG+1,"Serial %d write %d",id,rt);
@@ -93,11 +110,11 @@ void GwSerial::loop(bool handleRead,bool handleWrite){
     write();
     if (! isInitialized()) return;
     if (! handleRead) return;
-    size_t available=serial->available();
+    size_t available=stream->available();
     if (! available) return;
     if (allowRead){
         size_t rd=readBuffer->fillData(available,[](uint8_t *buffer, size_t len, void *p)->size_t{
-            return ((GwSerial *)p)->serial->readBytes(buffer,len);
+            return ((GwSerial *)p)->stream->readBytes(buffer,len);
         },this);
         if (rd != 0){
             LOG_DEBUG(GwLog::DEBUG+2,"GwSerial %d read %d bytes",id,rd);
@@ -106,7 +123,7 @@ void GwSerial::loop(bool handleRead,bool handleWrite){
     else{
         uint8_t buffer[10];
         if (available > 10) available=10;
-        serial->readBytes(buffer,available);
+        stream->readBytes(buffer,available);
     }
 }
 void GwSerial::readMessages(GwMessageFetcher *writer){
@@ -115,10 +132,11 @@ void GwSerial::readMessages(GwMessageFetcher *writer){
     writer->handleBuffer(readBuffer);
 }
 
-bool GwSerial::flush(long max){
+bool GwSerial::flush(){
    if (! isInitialized()) return false;
+   long max=getFlushTimeout();
    if (! availableWrite) {
-    if ( serial->availableForWrite() < 1){
+    if ( availableForWrite() < 1){
         return false;
     }
     availableWrite=true;
@@ -128,7 +146,7 @@ bool GwSerial::flush(long max){
         if (write() != GwBuffer::AGAIN) return true;
         vTaskDelay(1);
    }
-   availableWrite=(serial->availableForWrite() > 0);
+   availableWrite=(availableForWrite() > 0);
    return false;
 }
 Stream * GwSerial::getStream(bool partialWrite){
diff --git a/lib/serial/GwSerial.h b/lib/serial/GwSerial.h
index f50149e..8e37974 100644
--- a/lib/serial/GwSerial.h
+++ b/lib/serial/GwSerial.h
@@ -6,10 +6,11 @@
 #include "GwChannelInterface.h"
 class GwSerialStream;
 class GwSerial : public GwChannelInterface{
-    private:
+    protected:
         GwBuffer *buffer;
         GwBuffer *readBuffer=NULL;
         GwLog *logger; 
+        Stream *stream;
         bool initialized=false;
         bool allowRead=true;
         GwBuffer::WriteStatus write();
@@ -17,36 +18,66 @@ class GwSerial : public GwChannelInterface{
         int overflows=0;
         size_t enqueue(const uint8_t *data, size_t len,bool partial=false);
         bool availableWrite=false; //if this is false we will wait for availabkleWrite until we flush again
+        virtual long getFlushTimeout(){return 2000;}
+        virtual int availableForWrite()=0;
+        int type=0;
     public:
-        class SerialWrapperBase{
-        public:
-        virtual void begin(GwLog* logger,unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1)=0;
-        virtual int getId()=0;
-        virtual int available(){return getStream()->available();}
-        size_t readBytes(uint8_t *buffer, size_t length){
-            return getStream()->readBytes(buffer,length);
-        }
-        virtual int availableForWrite(void){
-            return getStream()->availableForWrite();
-        }
-        size_t write(const uint8_t *buffer, size_t size){
-            return getStream()->write(buffer,size);
-        }
-        private:
-        virtual Stream *getStream()=0;
-        };
-        static const int bufferSize=200;
-        GwSerial(GwLog *logger,SerialWrapperBase *stream,bool allowRead=true);
-        ~GwSerial();
+        GwSerial(GwLog *logger,Stream *stream,int id,int type,bool allowRead=true);
+        virtual ~GwSerial();
         bool isInitialized();
         virtual size_t sendToClients(const char *buf,int sourceId,bool partial=false);
         virtual void loop(bool handleRead=true,bool handleWrite=true);
         virtual void readMessages(GwMessageFetcher *writer);
-        bool flush(long millis=200);
+        bool flush();
         virtual Stream *getStream(bool partialWrites);
         bool getAvailableWrite(){return availableWrite;}
+        virtual void begin(unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1)=0;
+        virtual String getMode() override;
     friend GwSerialStream;
-    private:
-        SerialWrapperBase *serial;
 };
+
+template<typename T>
+    class GwSerialImpl : public GwSerial{
+        private:
+        template<class C>
+        void beginImpl(C *s,unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1){}
+        void beginImpl(HardwareSerial *s,unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1){
+            s->begin(baud,config,rxPin,txPin);
+        }
+        template<class C>
+        void setError(C* s, GwLog *logger){}
+        void setError(HardwareSerial *s,GwLog *logger){
+            LOG_DEBUG(GwLog::LOG,"enable serial errors for channel %d",id);
+            s->onReceiveError([logger,this](hardwareSerial_error_t err){
+                LOG_DEBUG(GwLog::ERROR,"serial error on id %d: %d",this->id,(int)err);
+            });
+        }
+        #if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3
+            void beginImpl(HWCDC *s,unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1){
+            s->begin(baud);
+        }
+        #endif
+        template<class C>
+        long getFlushTimeoutImpl(const C*){return 2000;}
+        long getFlushTimeoutImpl(HWCDC *){return 200;}
+
+        T *serial;
+        protected:
+        virtual long getFlushTimeout() override{
+            return getFlushTimeoutImpl(serial);
+        }
+        virtual int availableForWrite(){
+            return serial->availableForWrite();
+        }
+        public:
+        GwSerialImpl(GwLog* logger,T* s,int i,int type,bool allowRead=true): GwSerial(logger,s,i,type,allowRead),serial(s){}
+        virtual ~GwSerialImpl(){}
+        virtual void begin(unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1) override{
+            beginImpl(serial,baud,config,rxPin,txPin);
+            setError(serial,logger);
+        };
+
+    };
+
+
 #endif
\ No newline at end of file

From 01d334d598f7a602352af2782745256edd8d21e2 Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Sun, 3 Nov 2024 17:33:30 +0100
Subject: [PATCH 33/58] only generate index.js, index.css on demand

---
 extra_script.py | 26 +++++++++++++++++++++-----
 1 file changed, 21 insertions(+), 5 deletions(-)

diff --git a/extra_script.py b/extra_script.py
index b8b5c97..ed62c64 100644
--- a/extra_script.py
+++ b/extra_script.py
@@ -375,14 +375,30 @@ def getLibs():
             rt.append(e)
     return rt
 
+
+
 def joinFiles(target,pattern,dirlist):
-    with gzip.open(target,"wb") as oh:
-        for dir in dirlist:
+    flist=[]
+    for dir in dirlist:
             fn=os.path.join(dir,pattern)
             if os.path.exists(fn):
-                print("adding %s to %s"%(fn,target))
-                with open(fn,"rb") as rh:
-                    shutil.copyfileobj(rh,oh)
+                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"]

From 40c4089f86cd8a575a83c8f8b5f116d75c3bf224 Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Sun, 3 Nov 2024 17:33:55 +0100
Subject: [PATCH 34/58] restructure channels, USB + serial1 working

---
 lib/channel/GwChannelList.cpp | 216 ++++++++++++++++++----------------
 lib/log/GwLog.h               |   2 +
 2 files changed, 117 insertions(+), 101 deletions(-)

diff --git a/lib/channel/GwChannelList.cpp b/lib/channel/GwChannelList.cpp
index c09966c..072d95f 100644
--- a/lib/channel/GwChannelList.cpp
+++ b/lib/channel/GwChannelList.cpp
@@ -18,6 +18,7 @@ class SerInit{
 };
 std::vector<SerInit> serialInits;
 
+
 static int typeFromMode(const char *mode){
     if (strcmp(mode,"UNI") == 0) return GWSERIAL_TYPE_UNI;
     if (strcmp(mode,"BI") == 0) return GWSERIAL_TYPE_BI;
@@ -31,90 +32,94 @@ static int typeFromMode(const char *mode){
     static GwInitializer<SerInit> __serial ## ser ## _init \
         (serialInits,SerInit(ser,__VA_ARGS__));
 #ifdef _GWI_SERIAL1
-    CFG_SERIAL(0,_GWI_SERIAL1)
+    CFG_SERIAL(SERIAL1_CHANNEL_ID,_GWI_SERIAL1)
 #endif
 #ifdef _GWI_SERIAL2
-    CFG_SERIAL(1,_GWI_SERIAL2)
+    CFG_SERIAL(SERIAL2_CHANNEL_ID,_GWI_SERIAL2)
 #endif
-//handle separate defines
-    //serial 1
-    #ifndef GWSERIAL_TX
-      #define GWSERIAL_TX -1
-    #endif
-    #ifndef GWSERIAL_RX
-      #define GWSERIAL_RX -1
-    #endif
-    #ifdef GWSERIAL_TYPE
-        CFG_SERIAL(0,GWSERIAL_RX,GWSERIAL_TX,GWSERIAL_TYPE)
-    #else
-        #ifdef GWSERIAL_MODE
-            CFG_SERIAL(0,GWSERIAL_RX,GWSERIAL_TX,typeFromMode(GWSERIAL_MODE))
-        #endif
-    #endif
-    //serial 2
-    #ifndef GWSERIAL2_TX
-      #define GWSERIAL2_TX -1
-    #endif
-    #ifndef GWSERIAL2_RX
-      #define GWSERIAL2_RX -1
-    #endif
-    #ifdef GWSERIAL2_TYPE
-        CFG_SERIAL(1,GWSERIAL2_RX,GWSERIAL2_TX,GWSERIAL2_TYPE)
-    #else
-        #ifdef GWSERIAL2_MODE
-            CFG_SERIAL(1,GWSERIAL2_RX,GWSERIAL2_TX,typeFromMode(GWSERIAL2_MODE))
-        #endif
-    #endif
-class GwSerialLog : public GwLogWriter
-{
-    static const size_t bufferSize = 4096;
-    char *logBuffer = NULL;
-    int wp = 0;
-    GwSerial *writer;
-    bool disabled = false;
-public:
-    GwSerialLog(GwSerial *writer, bool disabled)
+    // handle separate defines
+    // serial 1
+#ifndef GWSERIAL_TX
+#define GWSERIAL_TX -1
+#endif
+#ifndef GWSERIAL_RX
+#define GWSERIAL_RX -1
+#endif
+#ifdef GWSERIAL_TYPE
+    CFG_SERIAL(SERIAL1_CHANNEL_ID, GWSERIAL_RX, GWSERIAL_TX, GWSERIAL_TYPE)
+#else
+#ifdef GWSERIAL_MODE
+CFG_SERIAL(SERIAL1_CHANNEL_ID, GWSERIAL_RX, GWSERIAL_TX, typeFromMode(GWSERIAL_MODE))
+#endif
+#endif
+    // serial 2
+#ifndef GWSERIAL2_TX
+#define GWSERIAL2_TX -1
+#endif
+#ifndef GWSERIAL2_RX
+#define GWSERIAL2_RX -1
+#endif
+#ifdef GWSERIAL2_TYPE
+    CFG_SERIAL(SERIAL2_CHANNEL_ID, GWSERIAL2_RX, GWSERIAL2_TX, GWSERIAL2_TYPE)
+#else
+#ifdef GWSERIAL2_MODE
+CFG_SERIAL(SERIAL2_CHANNEL_ID, GWSERIAL2_RX, GWSERIAL2_TX, typeFromMode(GWSERIAL2_MODE))
+#endif
+#endif
+    class GwSerialLog : public GwLogWriter
     {
-        this->writer = writer;
-        this->disabled = disabled;
-        logBuffer = new char[bufferSize];
-        wp = 0;
-    }
-    virtual ~GwSerialLog() {}
-    virtual void write(const char *data)
-    {
-        if (disabled)
-            return;
-        int len = strlen(data);
-        if ((wp + len) >= (bufferSize - 1))
-            return;
-        strncpy(logBuffer + wp, data, len);
-        wp += len;
-        logBuffer[wp] = 0;
-    }
-    virtual void flush()
-    {
-        size_t handled = 0;
-        if (!disabled)
+        static const size_t bufferSize = 4096;
+        char *logBuffer = NULL;
+        int wp = 0;
+        GwSerial *writer;
+        bool disabled = false;
+
+    public:
+        GwSerialLog(GwSerial *writer, bool disabled)
         {
-            while (handled < wp)
-            {
-                if ( !writer->flush()) break;
-                size_t rt = writer->sendToClients(logBuffer + handled, -1, true);
-                handled += rt;
-            }
-            if (handled < wp){
-                if (handled > 0){
-                    memmove(logBuffer,logBuffer+handled,wp-handled);
-                    wp-=handled;
-                    logBuffer[wp]=0;
-                }
-                return;
-            }
+            this->writer = writer;
+            this->disabled = disabled;
+            logBuffer = new char[bufferSize];
+            wp = 0;
+        }
+        virtual ~GwSerialLog() {}
+        virtual void write(const char *data)
+        {
+            if (disabled)
+                return;
+            int len = strlen(data);
+            if ((wp + len) >= (bufferSize - 1))
+                return;
+            strncpy(logBuffer + wp, data, len);
+            wp += len;
+            logBuffer[wp] = 0;
+        }
+        virtual void flush()
+        {
+            size_t handled = 0;
+            if (!disabled)
+            {
+                while (handled < wp)
+                {
+                    if (!writer->flush())
+                        break;
+                    size_t rt = writer->sendToClients(logBuffer + handled, -1, true);
+                    handled += rt;
+                }
+                if (handled < wp)
+                {
+                    if (handled > 0)
+                    {
+                        memmove(logBuffer, logBuffer + handled, wp - handled);
+                        wp -= handled;
+                        logBuffer[wp] = 0;
+                    }
+                    return;
+                }
+            }
+            wp = 0;
+            logBuffer[0] = 0;
         }
-        wp = 0;
-        logBuffer[0] = 0;
-    }
 };
 
 
@@ -192,9 +197,23 @@ GwSerial* createSerial(GwLog *logger, T* s,int id, bool canRead=true){
     return new GwSerialImpl<T>(logger,s,id,canRead);
 } 
 
+static SerialParam * findSerialParam(int id){
+    SerialParam *param=nullptr;
+    for (auto && p: serialParameters){
+        if (id == p.id){
+            param=&p;
+            break;
+        }
+    }
+    return param;
+}
+
 static GwChannel * createSerialChannel(GwConfigHandler *config,GwLog *logger, int idx,int type,int rx,int tx, bool setLog=false){
-    if (idx < 0 || idx >= sizeof(serialParameters)/sizeof(SerialParam*)) return nullptr;
-    SerialParam *param=&serialParameters[idx];
+    SerialParam *param=findSerialParam(idx);
+    if (param == nullptr){
+        LOG_DEBUG(GwLog::ERROR,"invalid serial channel id %d",idx);
+        return nullptr;
+    }
     bool canRead=false;
     bool canWrite=false;
     bool validType=false;
@@ -225,9 +244,8 @@ static GwChannel * createSerialChannel(GwConfigHandler *config,GwLog *logger, in
         LOG_DEBUG(GwLog::ERROR,"invalid type for serial channel %d: %d",param->id,type);
         return nullptr;
     }
-    if (rx < 0) canRead=false;
-    if (tx < 0) canWrite=false;
-    LOG_DEBUG(GwLog::DEBUG,"serial set up: type=%d,rx=%d,canRead=%d,tx=%d,canWrite=%d",
+    LOG_DEBUG(GwLog::DEBUG,"serial set up: channel=%d, type=%d,rx=%d,canRead=%d,tx=%d,canWrite=%d",
+        idx,
         type,rx,(int)canRead,tx,(int)canWrite);
     GwSerial *serialStream=nullptr;
     GwLog *streamLog=setLog?nullptr:logger;
@@ -275,28 +293,20 @@ void GwChannelList::addChannel(GwChannel * channel){
             return;
         }
     }
-    LOG_DEBUG(GwLog::LOG, "adding channel %s", channel->toString().c_str());
+    LOG_INFO("adding channel %s", channel->toString().c_str());
     theChannels.push_back(channel);
 }
 void GwChannelList::preinit(){
     for (auto &&init:serialInits){
+        LOG_INFO("serial config found for %d",init.serial);
         if (init.fixedBaud >= 0){
-            switch(init.serial){
-              case 1:
-              {
-                LOG_DEBUG(GwLog::DEBUG,"setting fixed baud %d for serial",init.fixedBaud);
-                config->setValue(GwConfigDefinitions::serialBaud,String(init.fixedBaud),GwConfigInterface::READONLY);
-              }
-              break;
-              case 2:
-              {
-                LOG_DEBUG(GwLog::DEBUG,"setting fixed baud %d for serial2",init.fixedBaud);
-                config->setValue(GwConfigDefinitions::serial2Baud,String(init.fixedBaud),GwConfigInterface::READONLY);
-              }
-              break;
-              default:
-                LOG_DEBUG(GwLog::ERROR,"invalid serial definition %d found",init.serial)
+            SerialParam *param=findSerialParam(init.serial);
+            if (! param){
+                LOG_ERROR("invalid serial definition %d found",init.serial)
+                return;
             }
+            LOG_DEBUG(GwLog::DEBUG,"setting fixed baud %d for serial %d",init.fixedBaud,init.serial);
+            config->setValue(param->baud,String(init.fixedBaud),GwConfigInterface::READONLY);
         }
     }
 }
@@ -312,7 +322,7 @@ void GwChannelList::begin(bool fallbackSerial){
     GwChannel *channel=NULL;
     //usb
     if (! fallbackSerial){
-        GwChannel *usb=createSerialChannel(config, logger,0,GWSERIAL_TYPE_BI,GWUSB_RX,GWUSB_TX,true);
+        GwChannel *usb=createSerialChannel(config, logger,USB_CHANNEL_ID,GWSERIAL_TYPE_BI,GWUSB_RX,GWUSB_TX,true);
         addChannel(usb);
     }
     //TCP server
@@ -335,7 +345,11 @@ void GwChannelList::begin(bool fallbackSerial){
 
     //new serial config handling
     for (auto &&init:serialInits){
-        (logger,init.serial,init.rx,init.tx,init.mode);
+        LOG_INFO("creating serial channel %d, rx=%d,tx=%d,type=%d",init.serial,init.rx,init.tx,init.mode);
+        GwChannel *ser=createSerialChannel(config,logger,init.serial,init.mode,init.rx,init.tx);
+        if (ser != nullptr){
+            addChannel(ser);
+        }
     }
     
     //tcp client
diff --git a/lib/log/GwLog.h b/lib/log/GwLog.h
index 4958760..bdaa963 100644
--- a/lib/log/GwLog.h
+++ b/lib/log/GwLog.h
@@ -38,5 +38,7 @@ class GwLog{
         long long getRecordCounter(){return recordCounter;}
 };
 #define LOG_DEBUG(level,...){ if (logger != NULL && logger->isActive(level)) logger->logDebug(level,__VA_ARGS__);}
+#define LOG_INFO(...){ if (logger != NULL && logger->isActive(GwLog::LOG)) logger->logDebug(GwLog::LOG,__VA_ARGS__);}
+#define LOG_ERROR(...){ if (logger != NULL && logger->isActive(GwLog::ERROR)) logger->logDebug(GwLog::ERROR,__VA_ARGS__);}
 
 #endif
\ No newline at end of file

From 18b9946b62f8006d5cb482e7bf37cf1a42030920 Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Sun, 3 Nov 2024 18:05:14 +0100
Subject: [PATCH 35/58] intermediate: try special handling for USBCDC

---
 lib/serial/GwSerial.h | 28 ++++++++++++++++++++++++++--
 1 file changed, 26 insertions(+), 2 deletions(-)

diff --git a/lib/serial/GwSerial.h b/lib/serial/GwSerial.h
index 8e37974..75f73a8 100644
--- a/lib/serial/GwSerial.h
+++ b/lib/serial/GwSerial.h
@@ -4,6 +4,7 @@
 #include "GwLog.h"
 #include "GwBuffer.h"
 #include "GwChannelInterface.h"
+#include "hal/usb_serial_jtag_ll.h"
 class GwSerialStream;
 class GwSerial : public GwChannelInterface{
     protected:
@@ -39,6 +40,7 @@ class GwSerial : public GwChannelInterface{
 template<typename T>
     class GwSerialImpl : public GwSerial{
         private:
+        unsigned long lastWritable=0;
         template<class C>
         void beginImpl(C *s,unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1){}
         void beginImpl(HardwareSerial *s,unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1){
@@ -59,7 +61,29 @@ template<typename T>
         #endif
         template<class C>
         long getFlushTimeoutImpl(const C*){return 2000;}
-        long getFlushTimeoutImpl(HWCDC *){return 200;}
+        #if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3
+            long getFlushTimeoutImpl(HWCDC *){return 200;}
+        #endif
+
+        template<class C>
+        int availableForWrite(C* c){
+            return c->availableForWrite();
+        }
+
+        #if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3
+        int availableForWrite(HWCDC* c){
+            int rt=c->availableForWrite();
+            if (rt > 0) {
+                lastWritable=millis();
+                return rt;
+            }
+            if (usb_serial_jtag_ll_txfifo_writable() == 1){
+                LOG_INFO("USBserial restart");
+                usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY);
+            }
+            return rt;
+        }
+        #endif
 
         T *serial;
         protected:
@@ -67,7 +91,7 @@ template<typename T>
             return getFlushTimeoutImpl(serial);
         }
         virtual int availableForWrite(){
-            return serial->availableForWrite();
+            return availableForWrite(serial);
         }
         public:
         GwSerialImpl(GwLog* logger,T* s,int i,int type,bool allowRead=true): GwSerial(logger,s,i,type,allowRead),serial(s){}

From e3bab58f7e425d38bbffc0bf97c28b7107e6475d Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Mon, 4 Nov 2024 10:00:05 +0100
Subject: [PATCH 36/58] update to platform 6.8.1

---
 platformio.ini | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/platformio.ini b/platformio.ini
index 2ed6e04..00c9321 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -30,10 +30,10 @@ lib_deps =
 	Update
 
 [env]
-platform = espressif32 @ 6.3.2
+platform = espressif32 @ 6.8.1
 framework = arduino
 ;platform_packages=
-;	framework-arduinoespressif32 @ 3.20011.230801
+;	framework-arduinoespressif32 @ 3.20017.0
 ;	framework-espidf @ 3.50101.0
 lib_deps = 
 	${basedeps.lib_deps}

From 041b550ae9724088fde48698c075f869307fbca4 Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Mon, 4 Nov 2024 10:00:49 +0100
Subject: [PATCH 37/58] #81: restart HWCDC interrupt after 100ms

---
 lib/serial/GwSerial.h | 18 ++++++++++++++----
 1 file changed, 14 insertions(+), 4 deletions(-)

diff --git a/lib/serial/GwSerial.h b/lib/serial/GwSerial.h
index 75f73a8..83d98a6 100644
--- a/lib/serial/GwSerial.h
+++ b/lib/serial/GwSerial.h
@@ -4,7 +4,8 @@
 #include "GwLog.h"
 #include "GwBuffer.h"
 #include "GwChannelInterface.h"
-#include "hal/usb_serial_jtag_ll.h"
+
+#define USBCDC_RESTART_TIME 100
 class GwSerialStream;
 class GwSerial : public GwChannelInterface{
     protected:
@@ -71,15 +72,24 @@ template<typename T>
         }
 
         #if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3
+        /**
+         * issue #81
+         * workaround for the HWCDC beeing stuck at some point in time
+         * with availableForWrite == 0 but the ISR being disabled
+         * we simply give a small delay of 100ms for availableForWrite being 0
+         * and afterwards call isConnected that seems to retrigger the ISR
+        */
         int availableForWrite(HWCDC* c){
             int rt=c->availableForWrite();
             if (rt > 0) {
                 lastWritable=millis();
                 return rt;
             }
-            if (usb_serial_jtag_ll_txfifo_writable() == 1){
-                LOG_INFO("USBserial restart");
-                usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY);
+            unsigned long now=millis();
+            if (now > (lastWritable+USBCDC_RESTART_TIME)){
+                lastWritable=now;
+                LOG_ERROR("***Restart USBCDC***");
+                c->isConnected(); //this seems to retrigger the ISR
             }
             return rt;
         }

From 1d5577a777cbd24ee5755a8ceb911a046d8ad50f Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Mon, 4 Nov 2024 10:32:52 +0100
Subject: [PATCH 38/58] #81: directly enable the ISR on HWCDC restart if
 connected

---
 lib/serial/GwSerial.h | 11 ++++++++---
 tools/log.pl          | 28 ++++++++++++++++++++++++++++
 2 files changed, 36 insertions(+), 3 deletions(-)
 create mode 100755 tools/log.pl

diff --git a/lib/serial/GwSerial.h b/lib/serial/GwSerial.h
index 83d98a6..f3712ab 100644
--- a/lib/serial/GwSerial.h
+++ b/lib/serial/GwSerial.h
@@ -4,6 +4,9 @@
 #include "GwLog.h"
 #include "GwBuffer.h"
 #include "GwChannelInterface.h"
+#if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3
+  #include "hal/usb_serial_jtag_ll.h"
+#endif
 
 #define USBCDC_RESTART_TIME 100
 class GwSerialStream;
@@ -77,7 +80,7 @@ template<typename T>
          * workaround for the HWCDC beeing stuck at some point in time
          * with availableForWrite == 0 but the ISR being disabled
          * we simply give a small delay of 100ms for availableForWrite being 0
-         * and afterwards call isConnected that seems to retrigger the ISR
+         * and afterwards retrigger the ISR
         */
         int availableForWrite(HWCDC* c){
             int rt=c->availableForWrite();
@@ -88,8 +91,10 @@ template<typename T>
             unsigned long now=millis();
             if (now > (lastWritable+USBCDC_RESTART_TIME)){
                 lastWritable=now;
-                LOG_ERROR("***Restart USBCDC***");
-                c->isConnected(); //this seems to retrigger the ISR
+                if (c->isConnected()){
+                    //this retriggers the ISR
+                    usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY);
+                }
             }
             return rt;
         }
diff --git a/tools/log.pl b/tools/log.pl
new file mode 100755
index 0000000..fcc5f5e
--- /dev/null
+++ b/tools/log.pl
@@ -0,0 +1,28 @@
+#! /usr/bin/env perl
+use strict;
+use POSIX qw(strftime);
+my ($dev,$speed)=@ARGV;
+if (not defined $dev){
+    die "usage: $0 dev"
+}
+if (! -e $dev) {
+    die "$dev not found"
+}
+open(my $fh,"<",$dev) or die "unable to open $dev";
+if (defined $speed){
+    print("setting speed $speed");
+    system("stty speed $speed < $dev") == 0 or die "unable to set speed";    
+}
+my $last=0;
+while (<$fh>){
+    my $x=time();
+    if ($last != 0){
+        if ($x > ($last+5)){
+            print("****gap***\n");
+        }
+    }
+    printf strftime("%Y/%m/%d-%H%M%S",localtime($x));
+    printf("[%04.2f]: ",$x-$last); 
+    $last=$x;
+    print $_;
+}
\ No newline at end of file

From 5e592dee508c049e23add0b2979e43caa8002b0b Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Mon, 4 Nov 2024 10:50:17 +0100
Subject: [PATCH 39/58] set tty in raw mode for log tool

---
 tools/log.pl | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/tools/log.pl b/tools/log.pl
index fcc5f5e..86cb8e2 100755
--- a/tools/log.pl
+++ b/tools/log.pl
@@ -8,11 +8,12 @@ if (not defined $dev){
 if (! -e $dev) {
     die "$dev not found"
 }
-open(my $fh,"<",$dev) or die "unable to open $dev";
 if (defined $speed){
     print("setting speed $speed");
     system("stty speed $speed < $dev") == 0 or die "unable to set speed";    
 }
+system("stty raw < $dev") == 0 or die "unable to set raw mode for $dev";
+open(my $fh,"<",$dev) or die "unable to open $dev";
 my $last=0;
 while (<$fh>){
     my $x=time();

From d0dee367f8da43f0114190928711208737f0922f Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Mon, 4 Nov 2024 18:52:47 +0100
Subject: [PATCH 40/58] unify channel handling

---
 lib/channel/GwChannel.h       |   1 +
 lib/channel/GwChannelList.cpp | 233 ++++++++++++++++++++--------------
 2 files changed, 138 insertions(+), 96 deletions(-)

diff --git a/lib/channel/GwChannel.h b/lib/channel/GwChannel.h
index 951290b..66fb4ae 100644
--- a/lib/channel/GwChannel.h
+++ b/lib/channel/GwChannel.h
@@ -78,5 +78,6 @@ class GwChannel{
         return (source >= sourceId && source <= maxSourceId);
     }
     String getMode(){return impl->getMode();}
+    int getMinId(){return sourceId;};
 };
 
diff --git a/lib/channel/GwChannelList.cpp b/lib/channel/GwChannelList.cpp
index 072d95f..56f4598 100644
--- a/lib/channel/GwChannelList.cpp
+++ b/lib/channel/GwChannelList.cpp
@@ -144,10 +144,14 @@ typedef struct {
     const char *preventLog;
     const char *readAct;
     const char *writeAct;
+    const char *sendSeasmart;
     const char *name;
-} SerialParam;
+    int maxId;
+    size_t rxstatus;
+    size_t txstatus;
+} ChannelParam;
 
-static  SerialParam serialParameters[]={
+static  ChannelParam channelParameters[]={
     {
         .id=USB_CHANNEL_ID,
         .baud=GwConfigDefinitions::usbBaud,
@@ -160,7 +164,11 @@ static  SerialParam serialParameters[]={
         .preventLog=GwConfigDefinitions::usbActisense,
         .readAct=GwConfigDefinitions::usbActisense,
         .writeAct=GwConfigDefinitions::usbActSend,
-        .name="USB"
+        .sendSeasmart="",
+        .name="USB",
+        .maxId=-1,
+        .rxstatus=offsetof(GwApi::Status,GwApi::Status::usbRx),
+        .txstatus=offsetof(GwApi::Status,GwApi::Status::usbTx)
     },
     {
         .id=SERIAL1_CHANNEL_ID,
@@ -174,7 +182,11 @@ static  SerialParam serialParameters[]={
         .preventLog="",
         .readAct="",
         .writeAct="",
-        .name="Serial"
+        .sendSeasmart="",
+        .name="Serial",
+        .maxId=-1,
+        .rxstatus=offsetof(GwApi::Status,GwApi::Status::serRx),
+        .txstatus=offsetof(GwApi::Status,GwApi::Status::serTx)
     },
     {
         .id=SERIAL2_CHANNEL_ID,
@@ -188,8 +200,49 @@ static  SerialParam serialParameters[]={
         .preventLog="",
         .readAct="",
         .writeAct="",
-        .name="Serial2"
+        .sendSeasmart="",
+        .name="Serial2",
+        .maxId=-1,
+        .rxstatus=offsetof(GwApi::Status,GwApi::Status::ser2Rx),
+        .txstatus=offsetof(GwApi::Status,GwApi::Status::ser2Tx)
+    },
+    {
+        .id=MIN_TCP_CHANNEL_ID,
+        .baud="",
+        .receive=GwConfigDefinitions::readTCP,
+        .send=GwConfigDefinitions::sendTCP,
+        .direction="",
+        .toN2K=GwConfigDefinitions::tcpToN2k,
+        .readF=GwConfigDefinitions::tcpReadFilter,
+        .writeF=GwConfigDefinitions::tcpWriteFilter,
+        .preventLog="",
+        .readAct="",
+        .writeAct="",
+        .sendSeasmart=GwConfigDefinitions::sendSeasmart,
+        .name="TCPServer",
+        .maxId=MIN_TCP_CHANNEL_ID+10,
+        .rxstatus=offsetof(GwApi::Status,GwApi::Status::tcpSerRx),
+        .txstatus=offsetof(GwApi::Status,GwApi::Status::tcpSerTx)
+    },
+    {
+        .id=TCP_CLIENT_CHANNEL_ID,
+        .baud="",
+        .receive=GwConfigDefinitions::readTCL,
+        .send=GwConfigDefinitions::sendTCL,
+        .direction="",
+        .toN2K=GwConfigDefinitions::tclToN2k,
+        .readF=GwConfigDefinitions::tclReadFilter,
+        .writeF=GwConfigDefinitions::tclWriteFilter,
+        .preventLog="",
+        .readAct="",
+        .writeAct="",
+        .sendSeasmart=GwConfigDefinitions::tclSeasmart,
+        .name="TCPClient",
+        .maxId=-1,
+        .rxstatus=offsetof(GwApi::Status,GwApi::Status::tcpClRx),
+        .txstatus=offsetof(GwApi::Status,GwApi::Status::tcpClTx)
     }
+
 };
 
 template<typename T>
@@ -197,9 +250,9 @@ GwSerial* createSerial(GwLog *logger, T* s,int id, bool canRead=true){
     return new GwSerialImpl<T>(logger,s,id,canRead);
 } 
 
-static SerialParam * findSerialParam(int id){
-    SerialParam *param=nullptr;
-    for (auto && p: serialParameters){
+static ChannelParam * findChannelParam(int id){
+    ChannelParam *param=nullptr;
+    for (auto && p: channelParameters){
         if (id == p.id){
             param=&p;
             break;
@@ -208,12 +261,44 @@ static SerialParam * findSerialParam(int id){
     return param;
 }
 
-static GwChannel * createSerialChannel(GwConfigHandler *config,GwLog *logger, int idx,int type,int rx,int tx, bool setLog=false){
-    SerialParam *param=findSerialParam(idx);
+static GwSerial * createSerialImpl(GwConfigHandler *config,GwLog *logger, int idx,int rx,int tx, bool setLog=false){
+    LOG_DEBUG(GwLog::DEBUG,"create serial: channel=%d, rx=%d,tx=%d",
+        idx,rx,tx);
+    ChannelParam *param=findChannelParam(idx);
     if (param == nullptr){
         LOG_DEBUG(GwLog::ERROR,"invalid serial channel id %d",idx);
         return nullptr;
     }
+    GwSerial *serialStream=nullptr;
+    GwLog *streamLog=setLog?nullptr:logger;
+    switch(param->id){
+        case USB_CHANNEL_ID:
+            serialStream=createSerial(streamLog,&USBSerial,param->id);
+            break;
+        case SERIAL1_CHANNEL_ID:
+            serialStream=createSerial(streamLog,&Serial1,param->id);
+            break;
+        case SERIAL2_CHANNEL_ID:
+            serialStream=createSerial(streamLog,&Serial2,param->id);
+            break;
+    }
+    if (serialStream == nullptr){
+        LOG_DEBUG(GwLog::ERROR,"invalid serial config with id %d",param->id);
+        return nullptr;
+    }
+    serialStream->begin(config->getInt(param->baud,115200),SERIAL_8N1,rx,tx);
+    if (setLog){
+        logger->setWriter(new GwSerialLog(serialStream,config->getBool(param->preventLog,false)));
+        logger->prefix="GWSERIAL:";
+    }
+    return serialStream;
+}
+static GwChannel * createChannel(GwLog *logger, GwConfigHandler *config, int id,GwChannelInterface *impl, int type=GWSERIAL_TYPE_BI){
+    ChannelParam *param=findChannelParam(id);
+    if (param == nullptr){
+        LOG_DEBUG(GwLog::ERROR,"invalid channel id %d",id);
+        return nullptr;
+    }
     bool canRead=false;
     bool canWrite=false;
     bool validType=false;
@@ -241,37 +326,11 @@ static GwChannel * createSerialChannel(GwConfigHandler *config,GwLog *logger, in
         validType=true;
     }
     if (! validType){
-        LOG_DEBUG(GwLog::ERROR,"invalid type for serial channel %d: %d",param->id,type);
+        LOG_DEBUG(GwLog::ERROR,"invalid type for channel %d: %d",param->id,type);
         return nullptr;
     }
-    LOG_DEBUG(GwLog::DEBUG,"serial set up: channel=%d, type=%d,rx=%d,canRead=%d,tx=%d,canWrite=%d",
-        idx,
-        type,rx,(int)canRead,tx,(int)canWrite);
-    GwSerial *serialStream=nullptr;
-    GwLog *streamLog=setLog?nullptr:logger;
-    switch(param->id){
-        case USB_CHANNEL_ID:
-            serialStream=createSerial(streamLog,&USBSerial,param->id);
-            break;
-        case SERIAL1_CHANNEL_ID:
-            serialStream=createSerial(streamLog,&Serial1,param->id);
-            break;
-        case SERIAL2_CHANNEL_ID:
-            serialStream=createSerial(streamLog,&Serial2,param->id);
-            break;
-    }
-    if (serialStream == nullptr){
-        LOG_DEBUG(GwLog::ERROR,"invalid serial config with id %d",param->id);
-        return nullptr;
-    }
-    serialStream->begin(config->getInt(param->baud,115200),SERIAL_8N1,rx,tx);
-    if (setLog){
-        logger->setWriter(new GwSerialLog(serialStream,config->getBool(param->preventLog,false)));
-        logger->prefix="GWSERIAL:";
-    }
-    LOG_DEBUG(GwLog::LOG, "starting serial %d ", param->id);
-    GwChannel *channel = new GwChannel(logger, param->name,param->id);
-    channel->setImpl(serialStream);
+    GwChannel *channel = new GwChannel(logger, param->name,param->id,param->maxId);
+    channel->setImpl(impl);
     channel->begin(
         canRead || canWrite,
         canWrite,
@@ -282,9 +341,11 @@ static GwChannel * createSerialChannel(GwConfigHandler *config,GwLog *logger, in
         config->getBool(param->toN2K),
         config->getBool(param->readAct),
         config->getBool(param->writeAct));
+    LOG_INFO("created channel %s",channel->toString().c_str());
     return channel;
 }
 void GwChannelList::addChannel(GwChannel * channel){
+    if (channel == nullptr) return;
     for (auto &&it:theChannels){
         if (it->overlaps(channel)){
             LOG_DEBUG(GwLog::ERROR,"trying to add channel with overlapping ids %s (%s), ignoring",
@@ -300,7 +361,7 @@ void GwChannelList::preinit(){
     for (auto &&init:serialInits){
         LOG_INFO("serial config found for %d",init.serial);
         if (init.fixedBaud >= 0){
-            SerialParam *param=findSerialParam(init.serial);
+            ChannelParam *param=findChannelParam(init.serial);
             if (! param){
                 LOG_ERROR("invalid serial definition %d found",init.serial)
                 return;
@@ -322,39 +383,39 @@ void GwChannelList::begin(bool fallbackSerial){
     GwChannel *channel=NULL;
     //usb
     if (! fallbackSerial){
-        GwChannel *usb=createSerialChannel(config, logger,USB_CHANNEL_ID,GWSERIAL_TYPE_BI,GWUSB_RX,GWUSB_TX,true);
-        addChannel(usb);
+        GwSerial *usbSerial=createSerialImpl(config, logger,USB_CHANNEL_ID,GWUSB_RX,GWUSB_TX,true);
+        if (usbSerial != nullptr){
+            GwChannel *usbChannel=createChannel(logger,config,USB_CHANNEL_ID,usbSerial,GWSERIAL_TYPE_BI);
+            if (usbChannel != nullptr){
+                addChannel(usbChannel);
+            }
+            else{
+                delete usbSerial;
+            }
+        }
     }
     //TCP server
     sockets=new GwSocketServer(config,logger,MIN_TCP_CHANNEL_ID);
     sockets->begin();
-    channel=new GwChannel(logger,"TCPserver",MIN_TCP_CHANNEL_ID,MIN_TCP_CHANNEL_ID+10);
-    channel->setImpl(sockets);
-    channel->begin(
-        true,
-        config->getBool(config->sendTCP),
-        config->getBool(config->readTCP),
-        config->getString(config->tcpReadFilter),
-        config->getString(config->tcpWriteFilter),
-        config->getBool(config->sendSeasmart),
-        config->getBool(config->tcpToN2k),
-        false,
-        false
-    );
-    addChannel(channel);
+    addChannel(createChannel(logger,config,MIN_TCP_CHANNEL_ID,sockets));
 
     //new serial config handling
     for (auto &&init:serialInits){
         LOG_INFO("creating serial channel %d, rx=%d,tx=%d,type=%d",init.serial,init.rx,init.tx,init.mode);
-        GwChannel *ser=createSerialChannel(config,logger,init.serial,init.mode,init.rx,init.tx);
+        GwSerial *ser=createSerialImpl(config,logger,init.serial,init.rx,init.tx);
         if (ser != nullptr){
-            addChannel(ser);
+            channel=createChannel(logger,config,init.serial,ser,init.mode);
+            if (channel != nullptr){
+                addChannel(channel);
+            }
+            else{
+                delete ser;
+            }
         }
     }
     
     //tcp client
     bool tclEnabled=config->getBool(config->tclEnabled);
-    channel=new GwChannel(logger,"TCPClient",TCP_CLIENT_CHANNEL_ID);
     if (tclEnabled){
         client=new GwTcpClient(logger);
         client->begin(TCP_CLIENT_CHANNEL_ID,
@@ -362,20 +423,8 @@ void GwChannelList::begin(bool fallbackSerial){
             config->getInt(config->remotePort),
             config->getBool(config->readTCL)
         );
-        channel->setImpl(client);
     }
-    channel->begin(
-        tclEnabled,
-        config->getBool(config->sendTCL),
-        config->getBool(config->readTCL),
-        config->getString(config->tclReadFilter),
-        config->getString(config->tclReadFilter),
-        config->getBool(config->tclSeasmart),
-        config->getBool(config->tclToN2k),
-        false,
-        false
-        );
-    addChannel(channel);
+    addChannel(createChannel(logger,config,TCP_CLIENT_CHANNEL_ID,client));
     logger->flush();
 }
 String GwChannelList::getMode(int id){
@@ -412,30 +461,22 @@ GwChannel *GwChannelList::getChannelById(int sourceId){
     return NULL;
 }
 
+/**
+ * slightly tricky generic setter for the API status
+ * we expect all values to be unsigned long
+ * the offsets are always offsetof(GwApi::Status,GwApi::Status::xxx)
+*/
+static void setStatus(GwApi::Status *status,size_t offset,unsigned long v){
+    if (offset == 0) return;
+    *((unsigned long *)(((unsigned char *)status)+offset))=v;
+}
+
 void GwChannelList::fillStatus(GwApi::Status &status){
-    GwChannel *channel=getChannelById(USB_CHANNEL_ID);
-    if (channel){
-        status.usbRx=channel->countRx();
-        status.usbTx=channel->countTx();
-    }
-    channel=getChannelById(SERIAL1_CHANNEL_ID);
-    if (channel){
-        status.serRx=channel->countRx();
-        status.serTx=channel->countTx();
-    }
-    channel=getChannelById(SERIAL2_CHANNEL_ID);
-    if (channel){
-        status.ser2Rx=channel->countRx();
-        status.ser2Tx=channel->countTx();
-    }
-    channel=getChannelById(MIN_TCP_CHANNEL_ID);
-    if (channel){
-        status.tcpSerRx=channel->countRx();
-        status.tcpSerTx=channel->countTx();
-    }
-    channel=getChannelById(TCP_CLIENT_CHANNEL_ID);
-    if (channel){
-        status.tcpClRx=channel->countRx();
-        status.tcpClTx=channel->countTx();
+    for (auto && channel: theChannels){
+        ChannelParam *param=findChannelParam(channel->getMinId());
+        if (param != nullptr){
+            setStatus(&status,param->rxstatus,channel->countRx());
+            setStatus(&status,param->txstatus,channel->countTx());
+        }
     }
 }
\ No newline at end of file

From 490a5b9ba1781e9ea4fef40f6e584eac1b62833d Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Mon, 4 Nov 2024 20:21:33 +0100
Subject: [PATCH 41/58] #79: add udp writer

---
 lib/api/GwApi.h                  |  1 +
 lib/channel/GwChannelList.cpp    | 26 ++++++++++++++
 lib/channel/GwChannelList.h      |  1 +
 lib/socketserver/GwUdpWriter.cpp | 59 ++++++++++++++++++++++++++++++++
 lib/socketserver/GwUdpWriter.h   | 28 +++++++++++++++
 web/config.json                  | 41 ++++++++++++++++++++++
 6 files changed, 156 insertions(+)
 create mode 100644 lib/socketserver/GwUdpWriter.cpp
 create mode 100644 lib/socketserver/GwUdpWriter.h

diff --git a/lib/api/GwApi.h b/lib/api/GwApi.h
index d4d0716..00dbc1f 100644
--- a/lib/api/GwApi.h
+++ b/lib/api/GwApi.h
@@ -95,6 +95,7 @@ class GwApi{
                 unsigned long ser2Tx=0;
                 unsigned long tcpSerRx=0;
                 unsigned long tcpSerTx=0;
+                unsigned long udpwTx=0;
                 int tcpClients=0;
                 unsigned long tcpClRx=0;
                 unsigned long tcpClTx=0;
diff --git a/lib/channel/GwChannelList.cpp b/lib/channel/GwChannelList.cpp
index 56f4598..9b68b07 100644
--- a/lib/channel/GwChannelList.cpp
+++ b/lib/channel/GwChannelList.cpp
@@ -6,6 +6,7 @@
 #include "GwSocketServer.h"
 #include "GwSerial.h"
 #include "GwTcpClient.h"
+#include "GwUdpWriter.h"
 class SerInit{
     public:
         int serial=-1;
@@ -241,6 +242,24 @@ static  ChannelParam channelParameters[]={
         .maxId=-1,
         .rxstatus=offsetof(GwApi::Status,GwApi::Status::tcpClRx),
         .txstatus=offsetof(GwApi::Status,GwApi::Status::tcpClTx)
+    },
+    {
+        .id=UDPW_CHANNEL_ID,
+        .baud="",
+        .receive="",
+        .send=GwConfigDefinitions::udpwEnabled,
+        .direction="",
+        .toN2K="",
+        .readF="",
+        .writeF=GwConfigDefinitions::udpwWriteFilter,
+        .preventLog="",
+        .readAct="",
+        .writeAct="",
+        .sendSeasmart=GwConfigDefinitions::udpwSeasmart,
+        .name="UDPWriter",
+        .maxId=-1,
+        .rxstatus=0,
+        .txstatus=offsetof(GwApi::Status,GwApi::Status::udpwTx)
     }
 
 };
@@ -425,6 +444,13 @@ void GwChannelList::begin(bool fallbackSerial){
         );
     }
     addChannel(createChannel(logger,config,TCP_CLIENT_CHANNEL_ID,client));
+
+    //udp writer
+    if (config->getBool(GwConfigDefinitions::udpwEnabled)){
+        GwUdpWriter *writer=new GwUdpWriter(config,logger,UDPW_CHANNEL_ID);
+        writer->begin();
+        addChannel(createChannel(logger,config,UDPW_CHANNEL_ID,writer));
+    }
     logger->flush();
 }
 String GwChannelList::getMode(int id){
diff --git a/lib/channel/GwChannelList.h b/lib/channel/GwChannelList.h
index ba45c66..956b786 100644
--- a/lib/channel/GwChannelList.h
+++ b/lib/channel/GwChannelList.h
@@ -18,6 +18,7 @@
 #define SERIAL2_CHANNEL_ID 3
 #define TCP_CLIENT_CHANNEL_ID 4
 #define MIN_TCP_CHANNEL_ID 5
+#define UDPW_CHANNEL_ID 20
 
 #define MIN_USER_TASK 200
 class GwSocketServer;
diff --git a/lib/socketserver/GwUdpWriter.cpp b/lib/socketserver/GwUdpWriter.cpp
new file mode 100644
index 0000000..51a5037
--- /dev/null
+++ b/lib/socketserver/GwUdpWriter.cpp
@@ -0,0 +1,59 @@
+#include "GwUdpWriter.h"
+#include <ESPmDNS.h>
+#include <errno.h>
+#include "GwBuffer.h"
+#include "GwSocketConnection.h"
+#include "GwSocketHelper.h"
+
+GwUdpWriter::GwUdpWriter(const GwConfigHandler *config, GwLog *logger, int minId)
+{
+    this->config = config;
+    this->logger = logger;
+    this->minId = minId;
+}
+
+void GwUdpWriter::begin()
+{
+    fd=socket(AF_INET,SOCK_DGRAM,IPPROTO_IP);
+    if (fd < 0){
+        LOG_ERROR("unable to create udp socket");
+        return;
+    }
+    int enable = 1;
+    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
+    setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &enable, sizeof(int));
+    port=config->getInt(GwConfigDefinitions::udpwPort);
+    //TODO: check port
+    address=config->getString(GwConfigDefinitions::udpwAddress);
+    LOG_INFO("UDP writer created, address=%s, port=%d",
+            address.c_str(),port);
+    inet_pton(AF_INET, address.c_str(), &destination.sin_addr);
+    destination.sin_family = AF_INET;
+    destination.sin_port = htons(port);
+}
+
+void GwUdpWriter::loop(bool handleRead, bool handleWrite)
+{
+    
+}
+
+void GwUdpWriter::readMessages(GwMessageFetcher *writer)
+{
+    
+}
+size_t GwUdpWriter::sendToClients(const char *buf, int source,bool partial)
+{ 
+    if (source == minId) return 0;
+    size_t len=strlen(buf);
+    ssize_t err = sendto(fd,buf,len,0,(struct sockaddr *)&destination, sizeof(destination));
+    if (err < 0){
+        LOG_DEBUG(GwLog::DEBUG,"UDP writer error sending: %d",errno);
+        return 0;
+    }
+    return err;
+}
+
+
+GwUdpWriter::~GwUdpWriter()
+{
+}
\ No newline at end of file
diff --git a/lib/socketserver/GwUdpWriter.h b/lib/socketserver/GwUdpWriter.h
new file mode 100644
index 0000000..e98ea67
--- /dev/null
+++ b/lib/socketserver/GwUdpWriter.h
@@ -0,0 +1,28 @@
+#ifndef _GWUDPWRITER_H
+#define _GWUDPWRITER_H
+#include "GWConfig.h"
+#include "GwLog.h"
+#include "GwBuffer.h"
+#include "GwChannelInterface.h"
+#include <memory>
+#include <sys/socket.h>
+#include <arpa/inet.h>
+
+class GwUdpWriter: public GwChannelInterface{
+    private:
+        const GwConfigHandler *config;
+        GwLog *logger;
+        int fd=-1;
+        int minId;
+        int port;
+        String address;
+        struct sockaddr_in destination;
+    public:
+        GwUdpWriter(const GwConfigHandler *config,GwLog *logger,int minId);
+        ~GwUdpWriter();
+        void begin();
+        virtual void loop(bool handleRead=true,bool handleWrite=true);
+        virtual size_t sendToClients(const char *buf,int sourceId, bool partialWrite=false);
+        virtual void readMessages(GwMessageFetcher *writer);
+};
+#endif
\ No newline at end of file
diff --git a/web/config.json b/web/config.json
index 5c21eb5..b4336d6 100644
--- a/web/config.json
+++ b/web/config.json
@@ -826,6 +826,47 @@
         "description": "send NMEA2000 as seasmart to remote TCP server",
         "category": "TCP client"
     },
+    {
+        "name": "udpwEnabled",
+        "label": "enable",
+        "type": "boolean",
+        "default": "false",
+        "description":"enable the UDP writer",
+        "category":"UDP writer"
+    },
+    {
+        "name": "udpwPort",
+        "label": "remote port",
+        "type": "number",
+        "default": "10110",
+        "description": "the UDP port we send to",
+        "category": "UDP writer"
+    },
+    {
+        "name": "udpwAddress",
+        "label": "remote address",
+        "type": "string",
+        "default": "",
+        "check": "checkIpAddress",
+        "description": "the IP address we connect to in the form 192.168.1.2",
+        "category": "UDP writer"
+    },
+    {
+        "name": "udpwWriteFilter",
+        "label": "NMEA write Filter",
+        "type": "filter",
+        "default": "",
+        "description": "filter for NMEA0183 data when writing to remote UDP server\nselect aison|aisoff, set a whitelist or a blacklist with NMEA sentences like RMC,RMB",
+        "category": "UDP writer"
+    },
+    {
+        "name": "udpwSeasmart",
+        "label": "Seasmart out",
+        "type": "boolean",
+        "default": "false",
+        "description": "send NMEA2000 as seasmart to remote UDP server",
+        "category": "UDP writer"
+    },
     {
         "name": "wifiClient",
         "label": "wifi client",

From a5827e24d8481d7ae6114f98309300d26bf67ab0 Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Thu, 7 Nov 2024 19:47:27 +0100
Subject: [PATCH 42/58] different modes for UDP writer, allow to select network

---
 lib/socketserver/GwSocketHelper.h |   5 +
 lib/socketserver/GwUdpWriter.cpp  | 184 ++++++++++++++++++++++++++----
 lib/socketserver/GwUdpWriter.h    |  51 ++++++++-
 web/config.json                   |  34 +++++-
 4 files changed, 250 insertions(+), 24 deletions(-)

diff --git a/lib/socketserver/GwSocketHelper.h b/lib/socketserver/GwSocketHelper.h
index 8dea507..67ba3a6 100644
--- a/lib/socketserver/GwSocketHelper.h
+++ b/lib/socketserver/GwSocketHelper.h
@@ -17,4 +17,9 @@ class GwSocketHelper{
             if (setsockopt(socket, IPPROTO_TCP, TCP_KEEPCNT, &val, sizeof(val)) != ESP_OK) return false;
             return true;  
         }
+        static bool isMulticast(const String &addr){
+            in_addr iaddr;
+            if (inet_pton(AF_INET,addr.c_str(),&iaddr) != 1) return false;
+            return IN_MULTICAST(ntohl(iaddr.s_addr));
+        }
 };
\ No newline at end of file
diff --git a/lib/socketserver/GwUdpWriter.cpp b/lib/socketserver/GwUdpWriter.cpp
index 51a5037..c91880e 100644
--- a/lib/socketserver/GwUdpWriter.cpp
+++ b/lib/socketserver/GwUdpWriter.cpp
@@ -4,36 +4,175 @@
 #include "GwBuffer.h"
 #include "GwSocketConnection.h"
 #include "GwSocketHelper.h"
+#include "GWWifi.h"
+
+GwUdpWriter::WriterSocket::WriterSocket(GwLog *l,int p,const String &src,const String &dst, SourceMode sm) :
+     sourceMode(sm), source(src), destination(dst), port(p),logger(l)
+{
+    if (inet_pton(AF_INET, dst.c_str(), &dstA.sin_addr) != 1)
+    {
+        LOG_ERROR("UDPW: invalid destination ip address %s", dst.c_str());
+        return;
+    }
+    if (sourceMode != SourceMode::S_UNBOUND)
+    {
+        if (inet_pton(AF_INET, src.c_str(), &srcA) != 1)
+        {
+            LOG_ERROR("UDPW: invalid source ip address %s", src.c_str());
+            return;
+        }
+    }
+    dstA.sin_family=AF_INET;
+    dstA.sin_port=htons(port);
+    fd=socket(AF_INET,SOCK_DGRAM,IPPROTO_IP);
+    if (fd < 0){
+        LOG_ERROR("UDPW: unable to create udp socket: %d",errno);
+        return;
+    }
+    int enable = 1;
+    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
+    setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &enable, sizeof(int));
+    switch (sourceMode)
+    {
+    case SourceMode::S_SRC:
+    {
+        sockaddr_in bindA;
+        bindA.sin_family = AF_INET;
+        bindA.sin_port = htons(0); // let system select
+        bindA.sin_addr = srcA;
+        if (bind(fd, (struct sockaddr *)&bindA, sizeof(bindA)) != 0)
+        {
+            LOG_ERROR("UDPW: bind failed for address %s: %d", source.c_str(), errno);
+            ::close(fd);
+            fd = -1;
+            return;
+        }
+    }
+    break;
+    case SourceMode::S_MC:
+    {
+        if (setsockopt(fd,IPPROTO_IP,IP_MULTICAST_IF,&srcA,sizeof(srcA)) != 0){
+            LOG_ERROR("UDPW: unable to set MC source %s: %d",source.c_str(),errno);
+            ::close(fd);
+            fd=-1;
+            return;
+        }
+        int loop=0;
+        setsockopt(fd,IPPROTO_IP,IP_MULTICAST_LOOP,&loop,sizeof(loop));   
+    }
+    break;
+    default:
+      //not bound
+      break;
+    }
+}
+bool GwUdpWriter::WriterSocket::changed(const String &newSrc, const String &newDst){
+    if (newDst != destination) return true;
+    if (sourceMode == SourceMode::S_UNBOUND) return false;
+    return newSrc != source;
+}
+size_t GwUdpWriter::WriterSocket::send(const char *buf,size_t len){
+    if (fd < 0) return 0;
+    ssize_t err = sendto(fd,buf,len,0,(struct sockaddr *)&dstA, sizeof(dstA));
+    if (err < 0){
+        LOG_DEBUG(GwLog::DEBUG,"UDPW %s error sending: %d",destination.c_str(), errno);
+        return 0;
+    }
+    return err;
+}
 
 GwUdpWriter::GwUdpWriter(const GwConfigHandler *config, GwLog *logger, int minId)
 {
     this->config = config;
     this->logger = logger;
     this->minId = minId;
+    port=config->getInt(GwConfigDefinitions::udpwPort);
+    
+}
+void GwUdpWriter::checkStaSocket(){
+    String src;
+    String bc;
+    if (type == T_BCAP || type == T_MCAP || type == T_NORM || type == T_UNKNOWN ) return;
+    bool connected=false;
+    if (WiFi.isConnected()){
+        src=WiFi.localIP().toString();
+        bc=WiFi.broadcastIP().toString();
+        connected=true;
+    }
+    else{
+        if (staSocket == nullptr) return;
+    }
+    String dst;
+    WriterSocket::SourceMode sm=WriterSocket::SourceMode::S_SRC;
+    switch (type){
+        case T_BCALL:
+        case T_BCSTA:
+            sm=WriterSocket::SourceMode::S_SRC;
+            dst=bc;
+            break;
+        case T_MCALL:
+        case T_MCSTA:
+            dst=config->getString(GwConfigDefinitions::udpwMC);
+            sm=WriterSocket::SourceMode::S_MC;
+            break;
+
+    }
+    if (staSocket != nullptr)
+    {
+        if (!connected || staSocket->changed(src, dst))
+        {
+            staSocket->close();
+            delete staSocket;
+            staSocket = nullptr;
+            LOG_INFO("changing/stopping UDPW(sta) socket");
+        }
+    }
+    if (staSocket == nullptr && connected)
+    {
+        LOG_INFO("creating new UDP(sta) socket src=%s, dst=%s", src.c_str(), dst.c_str());
+        staSocket = new WriterSocket(logger, port, src, dst, WriterSocket::SourceMode::S_SRC);
+    }
 }
 
 void GwUdpWriter::begin()
 {
-    fd=socket(AF_INET,SOCK_DGRAM,IPPROTO_IP);
-    if (fd < 0){
-        LOG_ERROR("unable to create udp socket");
-        return;
+    if (type != T_UNKNOWN) return; //already started
+    type=(UType)(config->getInt(GwConfigDefinitions::udpwType));
+    LOG_INFO("UDPW begin, mode=%d",(int)type);
+    String src=WiFi.softAPIP().toString();
+    String dst;
+    WriterSocket::SourceMode sm=WriterSocket::SourceMode::S_UNBOUND;
+    bool createApSocket=false;
+    switch(type){
+        case T_BCALL:
+        case T_BCAP:
+            createApSocket=true;
+            dst=WiFi.softAPBroadcastIP().toString();   
+            sm=WriterSocket::SourceMode::S_SRC;
+            break;
+        case T_MCALL:
+        case T_MCAP:
+            createApSocket=true;
+            dst=config->getString(GwConfigDefinitions::udpwMC);   
+            sm=WriterSocket::SourceMode::S_SRC;
+            break;
+        case T_NORM:
+            createApSocket=true;
+            dst=config->getString(GwConfigDefinitions::udpwAddress);   
+            sm=WriterSocket::SourceMode::S_UNBOUND;
     }
-    int enable = 1;
-    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
-    setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &enable, sizeof(int));
-    port=config->getInt(GwConfigDefinitions::udpwPort);
-    //TODO: check port
-    address=config->getString(GwConfigDefinitions::udpwAddress);
-    LOG_INFO("UDP writer created, address=%s, port=%d",
-            address.c_str(),port);
-    inet_pton(AF_INET, address.c_str(), &destination.sin_addr);
-    destination.sin_family = AF_INET;
-    destination.sin_port = htons(port);
+    if (createApSocket){
+        LOG_INFO("creating new UDPW(ap) socket src=%s, dst=%s", src.c_str(), dst.c_str());
+        apSocket=new WriterSocket(logger,port,src,dst,sm);
+    }
+    checkStaSocket();
 }
 
 void GwUdpWriter::loop(bool handleRead, bool handleWrite)
 {
+    if (handleWrite){
+        checkStaSocket();
+    }
     
 }
 
@@ -45,12 +184,17 @@ size_t GwUdpWriter::sendToClients(const char *buf, int source,bool partial)
 { 
     if (source == minId) return 0;
     size_t len=strlen(buf);
-    ssize_t err = sendto(fd,buf,len,0,(struct sockaddr *)&destination, sizeof(destination));
-    if (err < 0){
-        LOG_DEBUG(GwLog::DEBUG,"UDP writer error sending: %d",errno);
-        return 0;
+    bool hasSent=false;
+    size_t res=0;
+    if (apSocket != nullptr){
+        res=apSocket->send(buf,len);
+        if (res > 0) hasSent=true;
     }
-    return err;
+    if (staSocket != nullptr){
+        res=staSocket->send(buf,len);
+        if (res > 0) hasSent=true;
+    }
+    return hasSent?len:0;
 }
 
 
diff --git a/lib/socketserver/GwUdpWriter.h b/lib/socketserver/GwUdpWriter.h
index e98ea67..e17a17e 100644
--- a/lib/socketserver/GwUdpWriter.h
+++ b/lib/socketserver/GwUdpWriter.h
@@ -9,14 +9,59 @@
 #include <arpa/inet.h>
 
 class GwUdpWriter: public GwChannelInterface{
+    public:
+    using UType=enum{
+        T_BCALL=0,
+        T_BCAP=1,
+        T_BCSTA=2,
+        T_NORM=3,
+        T_MCALL=4,
+        T_MCAP=5,
+        T_MCSTA=6,
+        T_UNKNOWN=-1
+    };
     private:
+        class WriterSocket{
+            public:
+            int fd=-1;
+            struct in_addr srcA;
+            struct sockaddr_in dstA;
+            String source;
+            String destination;
+            int port;
+            GwLog *logger;
+            using SourceMode=enum {
+                S_UNBOUND=0,
+                S_MC,
+                S_SRC
+            };
+            SourceMode sourceMode;
+            WriterSocket(GwLog *logger,int p,const String &src,const String &dst, SourceMode sm);
+            void close(){
+                if (fd > 0){
+                    ::close(fd);
+                }
+                fd=-1;
+            }
+            ~WriterSocket(){
+                close();
+            }
+            bool changed(const String &newSrc, const String &newDst);
+            size_t send(const char *buf,size_t len);
+        };
         const GwConfigHandler *config;
         GwLog *logger;
-        int fd=-1;
+        /**
+         * we use fd/address to send to the AP network
+         * and fd2,address2 to send to the station network
+         * for type "normal" we only use fd
+        */
+        WriterSocket *apSocket=nullptr; //also for T_NORM
+        WriterSocket *staSocket=nullptr;
         int minId;
         int port;
-        String address;
-        struct sockaddr_in destination;
+        UType type=T_UNKNOWN;
+        void checkStaSocket();
     public:
         GwUdpWriter(const GwConfigHandler *config,GwLog *logger,int minId);
         ~GwUdpWriter();
diff --git a/web/config.json b/web/config.json
index b4336d6..3e44a4b 100644
--- a/web/config.json
+++ b/web/config.json
@@ -842,6 +842,23 @@
         "description": "the UDP port we send to",
         "category": "UDP writer"
     },
+    {
+        "name": "udpwType",
+        "label": "remote address type",
+        "type": "list",
+        "default": "0",
+        "description": "to which networks/addresses do we send\nbc-all: send broadcast to AP and wifi client network\nbc-ap: send broadcast to access point only\nbc-cli: send broadcast to wifi client network\nnormal: normal target address\nmc-all: multicast to AP and wifi client network\nmc-ap:multicast to AP network\nmc-cli: muticast to wifi client network",
+        "list":[
+            {"l":"bc-all","v":"0"},
+            {"l":"bc-ap","v":"1"},
+            {"l":"bc-cli","v":"2"},
+            {"l":"normal","v":"3"},
+            {"l":"mc-all","v":"4"},
+            {"l":"mc-ap","v":"5"},
+            {"l":"mc-cli","v":"6"}
+        ],
+        "category": "UDP writer"
+    },
     {
         "name": "udpwAddress",
         "label": "remote address",
@@ -849,7 +866,22 @@
         "default": "",
         "check": "checkIpAddress",
         "description": "the IP address we connect to in the form 192.168.1.2",
-        "category": "UDP writer"
+        "category": "UDP writer",
+        "condition":{
+            "udpwType":["3"]
+        }
+    },
+    {
+        "name": "udpwMC",
+        "label": "multicast address",
+        "type": "string",
+        "default": "224.0.0.1",
+        "check": "checkMCAddress",
+        "description": "the multicast address we send to 224.0.0.0...239.255.255.255",
+        "category": "UDP writer",
+        "condition":{
+            "udpwType":["4","5","6"]
+        }
     },
     {
         "name": "udpwWriteFilter",

From b0d5e27b5a9aac6a3e99c478c44723e4f32326e5 Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Thu, 7 Nov 2024 20:16:58 +0100
Subject: [PATCH 43/58] make seasmart working again, only show counter for
 enabled directions

---
 lib/channel/GwChannel.cpp     | 16 ++++++++++------
 lib/channel/GwChannelList.cpp |  2 +-
 2 files changed, 11 insertions(+), 7 deletions(-)

diff --git a/lib/channel/GwChannel.cpp b/lib/channel/GwChannel.cpp
index 069b5ae..9c79983 100644
--- a/lib/channel/GwChannel.cpp
+++ b/lib/channel/GwChannel.cpp
@@ -58,8 +58,6 @@ GwChannel::GwChannel(GwLog *logger,
     this->name=name;
     this->sourceId=sourceId;
     this->maxSourceId=maxSourceId;
-    this->countIn=new GwCounter<String>(String("count")+name+String("in"));
-    this->countOut=new GwCounter<String>(String("count")+name+String("out"));
     this->impl=NULL;
     this->receiver=new GwChannelMessageReceiver(logger,this);
     this->actisenseReader=NULL;
@@ -100,6 +98,12 @@ void GwChannel::begin(
             actisenseReader->SetReadStream(channelStream);         
         }
     }
+    if (nmeaIn || readActisense){
+        this->countIn=new GwCounter<String>(String("count")+name+String("in"));
+    }
+    if (nmeaOut || seaSmartOut || writeActisense){
+        this->countOut=new GwCounter<String>(String("count")+name+String("out"));
+    }
 }
 void GwChannel::setImpl(GwChannelInterface *impl){
     this->impl=impl;
@@ -135,10 +139,10 @@ void GwChannel::updateCounter(const char *msg, bool out)
     }
     if (key[0] == 0) return;
     if (out){
-        countOut->add(key);
+        if (countOut) countOut->add(key);
     }
     else{
-        countIn->add(key);
+        if (countIn) countIn->add(key);
     }
 }
 
@@ -209,7 +213,7 @@ void GwChannel::parseActisense(N2kHandler handler){
     tN2kMsg N2kMsg;
 
     while (actisenseReader->GetMessageFromStream(N2kMsg)) {
-      countIn->add(String(N2kMsg.PGN));
+      if(countIn) countIn->add(String(N2kMsg.PGN));
       handler(N2kMsg,sourceId);
     }
 }
@@ -220,7 +224,7 @@ void GwChannel::sendActisense(const tN2kMsg &msg, int sourceId){
     //so we can check it here
     if (maxSourceId < 0 && this->sourceId == sourceId) return;
     if (sourceId >= this->sourceId && sourceId <= maxSourceId) return;
-    countOut->add(String(msg.PGN)); 
+    if(countOut) countOut->add(String(msg.PGN)); 
     msg.SendInActisenseFormat(channelStream);
 }
 
diff --git a/lib/channel/GwChannelList.cpp b/lib/channel/GwChannelList.cpp
index 9b68b07..b08bbfc 100644
--- a/lib/channel/GwChannelList.cpp
+++ b/lib/channel/GwChannelList.cpp
@@ -356,7 +356,7 @@ static GwChannel * createChannel(GwLog *logger, GwConfigHandler *config, int id,
         canRead,
         config->getString(param->readF),
         config->getString(param->writeF),
-        false,
+        config->getBool(param->sendSeasmart),
         config->getBool(param->toN2K),
         config->getBool(param->readAct),
         config->getBool(param->writeAct));

From 82f5e179875e9cfd4bfa1a5fd9f50c496638fe32 Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Fri, 8 Nov 2024 21:00:25 +0100
Subject: [PATCH 44/58] intermediate: udp reader

---
 lib/api/GwApi.h                   |   1 +
 lib/channel/GwChannelList.cpp     |  26 +++++
 lib/channel/GwChannelList.h       |   1 +
 lib/queue/GwBuffer.cpp            |  12 ++-
 lib/queue/GwBuffer.h              |   5 +-
 lib/socketserver/GwSocketHelper.h |   3 +
 lib/socketserver/GwUdpReader.cpp  | 158 ++++++++++++++++++++++++++++++
 lib/socketserver/GwUdpReader.h    |  45 +++++++++
 web/config.json                   | 143 ++++++++++++++++++++++++---
 web/index.js                      |  36 +++++--
 10 files changed, 406 insertions(+), 24 deletions(-)
 create mode 100644 lib/socketserver/GwUdpReader.cpp
 create mode 100644 lib/socketserver/GwUdpReader.h

diff --git a/lib/api/GwApi.h b/lib/api/GwApi.h
index 00dbc1f..5a24e92 100644
--- a/lib/api/GwApi.h
+++ b/lib/api/GwApi.h
@@ -96,6 +96,7 @@ class GwApi{
                 unsigned long tcpSerRx=0;
                 unsigned long tcpSerTx=0;
                 unsigned long udpwTx=0;
+                unsigned long udprRx=0;
                 int tcpClients=0;
                 unsigned long tcpClRx=0;
                 unsigned long tcpClTx=0;
diff --git a/lib/channel/GwChannelList.cpp b/lib/channel/GwChannelList.cpp
index b08bbfc..4371cef 100644
--- a/lib/channel/GwChannelList.cpp
+++ b/lib/channel/GwChannelList.cpp
@@ -7,6 +7,7 @@
 #include "GwSerial.h"
 #include "GwTcpClient.h"
 #include "GwUdpWriter.h"
+#include "GwUdpReader.h"
 class SerInit{
     public:
         int serial=-1;
@@ -260,8 +261,27 @@ static  ChannelParam channelParameters[]={
         .maxId=-1,
         .rxstatus=0,
         .txstatus=offsetof(GwApi::Status,GwApi::Status::udpwTx)
+    },
+    {
+        .id=UDPR_CHANNEL_ID,
+        .baud="",
+        .receive=GwConfigDefinitions::udprEnabled,
+        .send="",
+        .direction="",
+        .toN2K=GwConfigDefinitions::udprToN2k,
+        .readF=GwConfigDefinitions::udprReadFilter,
+        .writeF="",
+        .preventLog="",
+        .readAct="",
+        .writeAct="",
+        .sendSeasmart="",
+        .name="UDPReader",
+        .maxId=-1,
+        .rxstatus=offsetof(GwApi::Status,GwApi::Status::udprRx),
+        .txstatus=0
     }
 
+
 };
 
 template<typename T>
@@ -451,6 +471,12 @@ void GwChannelList::begin(bool fallbackSerial){
         writer->begin();
         addChannel(createChannel(logger,config,UDPW_CHANNEL_ID,writer));
     }
+    //udp reader
+    if (config->getBool(GwConfigDefinitions::udprEnabled)){
+        GwUdpReader *reader=new GwUdpReader(config,logger,UDPR_CHANNEL_ID);
+        reader->begin();
+        addChannel(createChannel(logger,config,UDPR_CHANNEL_ID,reader));
+    }
     logger->flush();
 }
 String GwChannelList::getMode(int id){
diff --git a/lib/channel/GwChannelList.h b/lib/channel/GwChannelList.h
index 956b786..f724716 100644
--- a/lib/channel/GwChannelList.h
+++ b/lib/channel/GwChannelList.h
@@ -19,6 +19,7 @@
 #define TCP_CLIENT_CHANNEL_ID 4
 #define MIN_TCP_CHANNEL_ID 5
 #define UDPW_CHANNEL_ID 20
+#define UDPR_CHANNEL_ID 21
 
 #define MIN_USER_TASK 200
 class GwSocketServer;
diff --git a/lib/queue/GwBuffer.cpp b/lib/queue/GwBuffer.cpp
index 8e12934..de0f070 100644
--- a/lib/queue/GwBuffer.cpp
+++ b/lib/queue/GwBuffer.cpp
@@ -21,7 +21,7 @@ GwBuffer::~GwBuffer(){
 }
 void GwBuffer::reset(String reason)
 {
-    LOG_DEBUG(GwLog::LOG,"reseting buffer %s, reason %s",this->name.c_str(),reason.c_str());
+    if (! reason.isEmpty())LOG_DEBUG(GwLog::LOG,"reseting buffer %s, reason %s",this->name.c_str(),reason.c_str());
     writePointer = buffer;
     readPointer = buffer;
     lp("reset");
@@ -33,6 +33,16 @@ size_t GwBuffer::freeSpace()
     }
     return readPointer - writePointer - 1;
 }
+size_t GwBuffer::continousSpace() const{
+    if (readPointer <= writePointer){
+        return bufferSize-offset(writePointer);
+    }
+    return readPointer-writePointer-1;
+}
+void GwBuffer::moveWp(size_t offset){
+    if (offset > continousSpace()) return;
+    writePointer+=offset;
+}
 size_t GwBuffer::usedSpace()
 {
     if (readPointer <= writePointer)
diff --git a/lib/queue/GwBuffer.h b/lib/queue/GwBuffer.h
index 17490ec..aa67e04 100644
--- a/lib/queue/GwBuffer.h
+++ b/lib/queue/GwBuffer.h
@@ -33,7 +33,7 @@ class GwBuffer{
         uint8_t *buffer;
         uint8_t *writePointer;
         uint8_t *readPointer;
-        size_t offset(uint8_t* ptr){
+        size_t offset(uint8_t* ptr) const{
             return (size_t)(ptr-buffer);
         }
         GwLog *logger;
@@ -54,6 +54,9 @@ class GwBuffer{
          * find the first occurance of x in the buffer, -1 if not found
          */
         int findChar(char x);
+        uint8_t *getWp(){return writePointer;}
+        size_t continousSpace() const; //free space from wp
+        void moveWp(size_t offset); //move the wp forward
 };
 
 #endif
\ No newline at end of file
diff --git a/lib/socketserver/GwSocketHelper.h b/lib/socketserver/GwSocketHelper.h
index 67ba3a6..f153d45 100644
--- a/lib/socketserver/GwSocketHelper.h
+++ b/lib/socketserver/GwSocketHelper.h
@@ -22,4 +22,7 @@ class GwSocketHelper{
             if (inet_pton(AF_INET,addr.c_str(),&iaddr) != 1) return false;
             return IN_MULTICAST(ntohl(iaddr.s_addr));
         }
+        static bool equals(const in_addr &left, const in_addr &right){
+            return left.s_addr == right.s_addr;
+        }
 };
\ No newline at end of file
diff --git a/lib/socketserver/GwUdpReader.cpp b/lib/socketserver/GwUdpReader.cpp
new file mode 100644
index 0000000..e8b619e
--- /dev/null
+++ b/lib/socketserver/GwUdpReader.cpp
@@ -0,0 +1,158 @@
+#include "GwUdpReader.h"
+#include <ESPmDNS.h>
+#include <errno.h>
+#include "GwBuffer.h"
+#include "GwSocketConnection.h"
+#include "GwSocketHelper.h"
+#include "GWWifi.h"
+
+
+GwUdpReader::GwUdpReader(const GwConfigHandler *config, GwLog *logger, int minId)
+{
+    this->config = config;
+    this->logger = logger;
+    this->minId = minId;
+    port=config->getInt(GwConfigDefinitions::udprPort);
+    buffer= new GwBuffer(logger,GwBuffer::RX_BUFFER_SIZE,"udprd");
+}
+
+void GwUdpReader::createAndBind(){
+    if (fd >= 0){
+        ::close(fd);
+    }
+    if (currentStationIp.isEmpty() && (type == T_STA || type == T_MCSTA)) return;
+    fd=socket(AF_INET,SOCK_DGRAM,IPPROTO_IP);
+    if (fd < 0){
+        LOG_ERROR("UDPR: unable to create udp socket: %d",errno);
+        return;
+    }
+    int enable = 1;
+    setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
+    if (type == T_STA)
+    {
+        if (inet_pton(AF_INET, currentStationIp.c_str(), &listenA.sin_addr) != 1)
+        {
+            LOG_ERROR("UDPR: invalid station ip address %s", currentStationIp.c_str());
+            close(fd);
+            fd = -1;
+            return;
+        }
+    }
+    if (bind(fd,(struct sockaddr *)&listenA,sizeof(listenA)) < 0){
+        LOG_ERROR("UDPR: unable to bind: %d",errno);
+        close(fd);
+        fd=-1;
+        return;
+    }
+    LOG_INFO("UDPR: socket created and bound");
+    if (type != T_MCALL && type != T_MCAP && type != T_MCSTA) {
+        return;
+    }
+    struct ip_mreq mc;
+    String mcAddr=config->getString(GwConfigDefinitions::udprMC);
+    if (inet_pton(AF_INET,mcAddr.c_str(),&mc.imr_multiaddr) != 1){
+        LOG_ERROR("UDPR: invalid multicast addr %s",mcAddr.c_str());
+        ::close(fd);
+        fd=-1;
+        return;
+    }
+    if (type == T_MCALL || type == T_MCAP){
+        mc.imr_interface=apAddr;
+        int res=setsockopt(fd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mc,sizeof(mc));
+        if (res != 0){
+            LOG_ERROR("UDPR: unable to add MC membership for AP:%d",errno);
+        }
+        else{
+            LOG_INFO("UDPR: membership for %s for AP",mcAddr.c_str());
+        }
+    }
+    if (!currentStationIp.isEmpty() && (type == T_MCALL || type == T_MCSTA))
+    {
+        mc.imr_interface = staAddr;
+        int res = setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mc, sizeof(mc));
+        if (res != 0)
+        {
+            LOG_ERROR("UDPR: unable to add MC membership for STA:%d", errno);
+        }
+        else{
+            LOG_INFO("UDPR: membership for %s for STA %s",mcAddr.c_str(),currentStationIp.c_str());
+        }
+    }
+}
+
+void GwUdpReader::begin()
+{
+    if (type != T_UNKNOWN) return; //already started
+    type=(UType)(config->getInt(GwConfigDefinitions::udprType));
+    LOG_INFO("UDPR begin, mode=%d",(int)type);
+    port=config->getInt(GwConfigDefinitions::udprPort);
+    listenA.sin_family=AF_INET;
+    listenA.sin_port=htons(port);
+    if (type != T_STA){
+        listenA.sin_addr.s_addr=htonl(INADDR_ANY);
+    }
+    String ap=WiFi.softAPIP().toString();
+    if (inet_pton(AF_INET, ap.c_str(), &apAddr) != 1)
+    {
+        LOG_ERROR("UDPR: invalid ap ip address %s", ap.c_str());
+        return;
+    }
+    String sta;
+    if (WiFi.isConnected()) sta=WiFi.localIP().toString();
+    setStationAdd(sta);
+    createAndBind();
+}
+
+bool GwUdpReader::setStationAdd(const String &sta){
+    if (sta == currentStationIp) return false;
+    currentStationIp=sta;
+    if (inet_pton(AF_INET, currentStationIp.c_str(), &staAddr) != 1){
+            LOG_ERROR("UDPR: invalid station ip address %s", currentStationIp.c_str());
+            return false;
+    }
+    LOG_INFO("UDPR: new station IP %s",currentStationIp.c_str());
+    return true;
+}
+void GwUdpReader::loop(bool handleRead, bool handleWrite)
+{
+    if (handleRead){
+        if (type == T_STA || type == T_MCALL || type == T_MCSTA){
+            //only change anything if we considered the station IP
+            String nextStationIp;
+            if (WiFi.isConnected()){
+                String nextStationIp=WiFi.localIP().toString();
+            }
+            if (setStationAdd(nextStationIp)){
+                LOG_INFO("UDPR: wifi client IP changed, restart");
+                createAndBind();
+            }
+        }
+    }
+    
+}
+
+void GwUdpReader::readMessages(GwMessageFetcher *writer)
+{
+    if (fd < 0) return;
+    //we expect one NMEA message in one UDP packet
+    buffer->reset();
+    struct sockaddr_in from;
+    socklen_t fromLen=sizeof(from);
+    ssize_t res=recvfrom(fd,buffer->getWp(),buffer->continousSpace(),MSG_DONTWAIT,
+        (struct sockaddr*)&from,&fromLen);
+    if (res <= 0) return;
+    if (GwSocketHelper::equals(from.sin_addr,apAddr)) return;
+    if (!currentStationIp.isEmpty() && (GwSocketHelper::equals(from.sin_addr,staAddr))) return;
+    buffer->moveWp(res);
+    LOG_DEBUG(GwLog::DEBUG,"UDPR: received %d bytes",res);
+    writer->handleBuffer(buffer);   
+}
+size_t GwUdpReader::sendToClients(const char *buf, int source,bool partial)
+{ 
+    return 0;
+}
+
+
+GwUdpReader::~GwUdpReader()
+{
+}
\ No newline at end of file
diff --git a/lib/socketserver/GwUdpReader.h b/lib/socketserver/GwUdpReader.h
new file mode 100644
index 0000000..08c56bb
--- /dev/null
+++ b/lib/socketserver/GwUdpReader.h
@@ -0,0 +1,45 @@
+#ifndef _GWUDPREADER_H
+#define _GWUDPREADER_H
+#include "GWConfig.h"
+#include "GwLog.h"
+#include "GwBuffer.h"
+#include "GwChannelInterface.h"
+#include <memory>
+#include <sys/socket.h>
+#include <arpa/inet.h>
+
+class GwUdpReader: public GwChannelInterface{
+    public:
+    using UType=enum{
+        T_ALL=0,
+        T_AP=1,
+        T_STA=2,
+        T_MCALL=4,
+        T_MCAP=5,
+        T_MCSTA=6,
+        T_UNKNOWN=-1
+    };
+    private:
+        const GwConfigHandler *config;
+        GwLog *logger;
+        int minId;
+        int port;
+        int fd=-1;
+        struct sockaddr_in listenA;
+        String listenIp;
+        String currentStationIp;
+        struct in_addr apAddr;
+        struct in_addr staAddr;
+        UType type=T_UNKNOWN;
+        void createAndBind();
+        bool setStationAdd(const String &sta);
+        GwBuffer *buffer=nullptr;
+    public:
+        GwUdpReader(const GwConfigHandler *config,GwLog *logger,int minId);
+        ~GwUdpReader();
+        void begin();
+        virtual void loop(bool handleRead=true,bool handleWrite=true);
+        virtual size_t sendToClients(const char *buf,int sourceId, bool partialWrite=false);
+        virtual void readMessages(GwMessageFetcher *writer);
+};
+#endif
\ No newline at end of file
diff --git a/web/config.json b/web/config.json
index 3e44a4b..0b75d27 100644
--- a/web/config.json
+++ b/web/config.json
@@ -691,6 +691,7 @@
         "label": "TCP port",
         "type": "number",
         "default": "10110",
+        "check":"checkPort",
         "description": "the TCP port we listen on",
         "category": "TCP server"
     },
@@ -766,8 +767,12 @@
         "label": "remote port",
         "type": "number",
         "default": "10110",
+        "check":"checkPort",
         "description": "the TCP port we connect to",
-        "category": "TCP client"
+        "category": "TCP client",
+        "condition":{
+            "tclEnabled":"true"
+        }
     },
     {
         "name": "remoteAddress",
@@ -776,7 +781,10 @@
         "default": "",
         "check": "checkIpAddress",
         "description": "the IP address we connect to in the form 192.168.1.2\nor an MDNS name like ESP32NMEA2K.local",
-        "category": "TCP client"
+        "category": "TCP client",
+        "condition":{
+            "tclEnabled":"true"
+        }
     },
     {
         "name": "sendTCL",
@@ -784,7 +792,10 @@
         "type": "boolean",
         "default": "true",
         "description": "send out NMEA data to remote TCP server",
-        "category": "TCP client"
+        "category": "TCP client",
+        "condition":{
+            "tclEnabled":"true"
+        }
     },
     {
         "name": "readTCL",
@@ -792,7 +803,10 @@
         "type": "boolean",
         "default": "true",
         "description": "receive NMEA data from remote TCP server",
-        "category": "TCP client"
+        "category": "TCP client",
+        "condition":{
+            "tclEnabled":"true"
+        }
     },
     {
         "name": "tclToN2k",
@@ -800,7 +814,10 @@
         "type": "boolean",
         "default": "true",
         "description": "convert NMEA0183 from remote TCP server to NMEA2000",
-        "category": "TCP client"
+        "category": "TCP client",
+        "condition":{
+            "tclEnabled":"true"
+        }
     },
     {
         "name": "tclReadFilter",
@@ -808,7 +825,10 @@
         "type": "filter",
         "default": "",
         "description": "filter for NMEA0183 data when reading from remote TCP server\nselect aison|aisoff, set a whitelist or a blacklist with NMEA sentences like RMC,RMB",
-        "category": "TCP client"
+        "category": "TCP client",
+        "condition":{
+            "tclEnabled":"true"
+        }
     },
     {
         "name": "tclWriteFilter",
@@ -816,7 +836,10 @@
         "type": "filter",
         "default": "",
         "description": "filter for NMEA0183 data when writing to remote TCP server\nselect aison|aisoff, set a whitelist or a blacklist with NMEA sentences like RMC,RMB",
-        "category": "TCP client"
+        "category": "TCP client",
+        "condition":{
+            "tclEnabled":"true"
+        }
     },
     {
         "name": "tclSeasmart",
@@ -824,7 +847,10 @@
         "type": "boolean",
         "default": "false",
         "description": "send NMEA2000 as seasmart to remote TCP server",
-        "category": "TCP client"
+        "category": "TCP client",
+        "condition":{
+            "tclEnabled":"true"
+        }
     },
     {
         "name": "udpwEnabled",
@@ -840,7 +866,11 @@
         "type": "number",
         "default": "10110",
         "description": "the UDP port we send to",
-        "category": "UDP writer"
+        "check":"checkPort",
+        "category": "UDP writer",
+        "condition":{
+            "udpwEnabled":"true"
+        }
     },
     {
         "name": "udpwType",
@@ -857,7 +887,10 @@
             {"l":"mc-ap","v":"5"},
             {"l":"mc-cli","v":"6"}
         ],
-        "category": "UDP writer"
+        "category": "UDP writer",
+        "condition":{
+            "udpwEnabled":"true"
+        }
     },
     {
         "name": "udpwAddress",
@@ -868,7 +901,8 @@
         "description": "the IP address we connect to in the form 192.168.1.2",
         "category": "UDP writer",
         "condition":{
-            "udpwType":["3"]
+            "udpwType":["3"],
+            "udpwEnabled":"true"
         }
     },
     {
@@ -880,7 +914,8 @@
         "description": "the multicast address we send to 224.0.0.0...239.255.255.255",
         "category": "UDP writer",
         "condition":{
-            "udpwType":["4","5","6"]
+            "udpwType":["4","5","6"],
+            "udpwEnabled":"true"
         }
     },
     {
@@ -889,7 +924,10 @@
         "type": "filter",
         "default": "",
         "description": "filter for NMEA0183 data when writing to remote UDP server\nselect aison|aisoff, set a whitelist or a blacklist with NMEA sentences like RMC,RMB",
-        "category": "UDP writer"
+        "category": "UDP writer",
+        "condition":{
+            "udpwEnabled":"true"
+        }
     },
     {
         "name": "udpwSeasmart",
@@ -897,7 +935,84 @@
         "type": "boolean",
         "default": "false",
         "description": "send NMEA2000 as seasmart to remote UDP server",
-        "category": "UDP writer"
+        "category": "UDP writer",
+        "condition":{
+            "udpwEnabled":"true"
+        }
+    },
+    {
+        "name": "udprEnabled",
+        "label": "enable",
+        "type": "boolean",
+        "default": "false",
+        "description":"enable the UDP reader",
+        "category":"UDP reader"
+    },
+    {
+        "name": "udprPort",
+        "label": "local port",
+        "type": "number",
+        "default": "10110",
+        "check":"checkPort",
+        "description": "the UDP port we listen on",
+        "category": "UDP reader",
+        "condition":{
+            "udprEnabled":"true"
+        }
+    },
+    {
+        "name": "udprType",
+        "label": "local address type",
+        "type": "list",
+        "default": "0",
+        "description": "to which networks/addresses do we listen\nall: listen on AP and wifi client network\nap: listen in access point network only\ncli: listen in wifi client network\nmc-all: receive multicast from AP and wifi client network\nmc-ap:receive multicast from AP network\nmc-cli: receive muticast wifi client network",
+        "list":[
+            {"l":"all","v":"0"},
+            {"l":"ap","v":"1"},
+            {"l":"cli","v":"2"},
+            {"l":"mc-all","v":"4"},
+            {"l":"mc-ap","v":"5"},
+            {"l":"mc-cli","v":"6"}
+        ],
+        "category": "UDP reader",
+        "condition":{
+            "udprEnabled":"true"
+        }
+    },
+    {
+        "name": "udprToN2k",
+        "label": "to NMEA2000",
+        "type": "boolean",
+        "default": "true",
+        "description": "convert NMEA0183 from UDP to NMEA2000",
+        "category": "UDP reader",
+        "condition":{
+            "udprEnabled":"true"
+        }
+    },
+    {
+        "name": "udprMC",
+        "label": "multicast address",
+        "type": "string",
+        "default": "224.0.0.1",
+        "check": "checkMCAddress",
+        "description": "the multicast address we listen on 224.0.0.0...239.255.255.255",
+        "category": "UDP reader",
+        "condition":{
+            "udprType":["4","5","6"],
+            "udprEnabled":"true"
+        }
+    },
+    {
+        "name": "udprReadFilter",
+        "label": "NMEA read Filter",
+        "type": "filter",
+        "default": "",
+        "description": "filter for NMEA0183 data when receiving\nselect aison|aisoff, set a whitelist or a blacklist with NMEA sentences like RMC,RMB",
+        "category": "UDP reader",
+        "condition":{
+            "udprEnabled":"true"
+        }
     },
     {
         "name": "wifiClient",
diff --git a/web/index.js b/web/index.js
index 337c21c..6962506 100644
--- a/web/index.js
+++ b/web/index.js
@@ -181,6 +181,12 @@
         }
     }
 
+    checkers.checkPort=function(v,allValues,def){
+        let parsed=parseInt(v);
+        if (isNaN(parsed)) return "must be a number";
+        if (parsed <1 || parsed >= 65536) return "port must be in the range 1..65536"; 
+    }
+
     checkers.checkSystemName=function(v) {
         //2...32 characters for ssid
         let allowed = v.replace(/[^a-zA-Z0-9]*/g, '');
@@ -213,13 +219,20 @@
     }
 
     checkers.checkIpAddress=function(v, allValues, def) {
-        if (allValues.tclEnabled != "true") return;
         if (!v) return "cannot be empty";
         if (!v.match(/[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*/)
             && !v.match(/.*\.local/))
             return "must be either in the form 192.168.1.1 or xxx.local";
     }
+    checkers.checkMCAddress=function(v, allValues, def) {
+        if (!v) return "cannot be empty";
+        if (!v.match(/[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*/))
+            return "must be in the form 224.0.0.1";
+        let parts=v.split(".");
+        let o1=parseInt(v[0]);
+        if (o1 < 224 || o1 > 239) return "mulicast address must be in the range 224.0.0.0 to 239.255.255.255"
 
+    }
     checkers.checkXDR=function(v, allValues) {
         if (!v) return;
         let parts = v.split(',');
@@ -264,21 +277,22 @@
                 continue;
             }
             let check = v.getAttribute('data-check');
-            if (check) {
+            if (check && conditionOk(name)) {
+                let cfgDef=getConfigDefition(name);
                 let checkFunction=checkers[check];
                 if (typeof (checkFunction) === 'function') {
                     if (! loggedChecks[check]){
                         loggedChecks[check]=true;
                         //console.log("check:"+check);
                     }
-                    let res = checkFunction(v.value, allValues, getConfigDefition(name));
+                    let res = checkFunction(v.value, allValues, cfgDef);
                     if (res) {
                         let value = v.value;
                         if (v.type === 'password') value = "******";
                         let label = v.getAttribute('data-label');
                         if (!label) label = v.getAttribute('name');
                         v.classList.add("error");
-                        alert("invalid config for " + label + "(" + value + "):\n" + res);
+                        alert("invalid config for "+cfgDef.category+":" + label + "(" + value + "):\n" + res);
                         return;
                     }
                 }
@@ -472,10 +486,10 @@
         if (!(condition instanceof Array)) condition = [condition];
         return condition;
     }
-    function checkCondition(element) {
-        let name = element.getAttribute('name');
+
+    function conditionOk(name){
         let condition = getConditions(name);
-        if (!condition) return;
+        if (!condition) return true;
         let visible = false;
         if (!condition instanceof Array) condition = [condition];
         condition.forEach(function (cel) {
@@ -493,7 +507,13 @@
                 }
             }
             if (lvis) visible = true;
-        });
+        }); 
+        return visible;   
+    }
+
+    function checkCondition(element) {
+        let name = element.getAttribute('name');
+        let visible=conditionOk(name);
         let row = closestParent(element, 'row');
         if (!row) return;
         if (visible) row.classList.remove('hidden');

From 4bded7bbb4e40f049b3c51280b4d46fcbf24c954 Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Mon, 11 Nov 2024 19:52:44 +0100
Subject: [PATCH 45/58] make sta udp receiver working

---
 lib/socketserver/GwUdpReader.cpp | 2 +-
 web/index.js                     | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/lib/socketserver/GwUdpReader.cpp b/lib/socketserver/GwUdpReader.cpp
index e8b619e..d7efbb9 100644
--- a/lib/socketserver/GwUdpReader.cpp
+++ b/lib/socketserver/GwUdpReader.cpp
@@ -120,7 +120,7 @@ void GwUdpReader::loop(bool handleRead, bool handleWrite)
             //only change anything if we considered the station IP
             String nextStationIp;
             if (WiFi.isConnected()){
-                String nextStationIp=WiFi.localIP().toString();
+                nextStationIp=WiFi.localIP().toString();
             }
             if (setStationAdd(nextStationIp)){
                 LOG_INFO("UDPR: wifi client IP changed, restart");
diff --git a/web/index.js b/web/index.js
index 6962506..a82f79e 100644
--- a/web/index.js
+++ b/web/index.js
@@ -229,7 +229,7 @@
         if (!v.match(/[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*/))
             return "must be in the form 224.0.0.1";
         let parts=v.split(".");
-        let o1=parseInt(v[0]);
+        let o1=parseInt(parts[0]);
         if (o1 < 224 || o1 > 239) return "mulicast address must be in the range 224.0.0.0 to 239.255.255.255"
 
     }

From b4eaad4dbfbd0c47bd4bb385669fc63993c5ce17 Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Mon, 11 Nov 2024 20:07:22 +0100
Subject: [PATCH 46/58] bind mc udp receiver to mc address

---
 lib/socketserver/GwUdpReader.cpp | 30 ++++++++++++++++++------------
 1 file changed, 18 insertions(+), 12 deletions(-)

diff --git a/lib/socketserver/GwUdpReader.cpp b/lib/socketserver/GwUdpReader.cpp
index d7efbb9..b88848c 100644
--- a/lib/socketserver/GwUdpReader.cpp
+++ b/lib/socketserver/GwUdpReader.cpp
@@ -49,13 +49,7 @@ void GwUdpReader::createAndBind(){
         return;
     }
     struct ip_mreq mc;
-    String mcAddr=config->getString(GwConfigDefinitions::udprMC);
-    if (inet_pton(AF_INET,mcAddr.c_str(),&mc.imr_multiaddr) != 1){
-        LOG_ERROR("UDPR: invalid multicast addr %s",mcAddr.c_str());
-        ::close(fd);
-        fd=-1;
-        return;
-    }
+    mc.imr_multiaddr=listenA.sin_addr;
     if (type == T_MCALL || type == T_MCAP){
         mc.imr_interface=apAddr;
         int res=setsockopt(fd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mc,sizeof(mc));
@@ -63,7 +57,7 @@ void GwUdpReader::createAndBind(){
             LOG_ERROR("UDPR: unable to add MC membership for AP:%d",errno);
         }
         else{
-            LOG_INFO("UDPR: membership for %s for AP",mcAddr.c_str());
+            LOG_INFO("UDPR: membership for for AP");
         }
     }
     if (!currentStationIp.isEmpty() && (type == T_MCALL || type == T_MCSTA))
@@ -75,7 +69,7 @@ void GwUdpReader::createAndBind(){
             LOG_ERROR("UDPR: unable to add MC membership for STA:%d", errno);
         }
         else{
-            LOG_INFO("UDPR: membership for %s for STA %s",mcAddr.c_str(),currentStationIp.c_str());
+            LOG_INFO("UDPR: membership for STA %s",currentStationIp.c_str());
         }
     }
 }
@@ -88,15 +82,27 @@ void GwUdpReader::begin()
     port=config->getInt(GwConfigDefinitions::udprPort);
     listenA.sin_family=AF_INET;
     listenA.sin_port=htons(port);
-    if (type != T_STA){
-        listenA.sin_addr.s_addr=htonl(INADDR_ANY);
-    }
+    listenA.sin_addr.s_addr=htonl(INADDR_ANY); //default
     String ap=WiFi.softAPIP().toString();
     if (inet_pton(AF_INET, ap.c_str(), &apAddr) != 1)
     {
         LOG_ERROR("UDPR: invalid ap ip address %s", ap.c_str());
         return;
     }
+    if (type == T_MCALL || type == T_MCAP || type == T_MCSTA){
+        String mcAddr=config->getString(GwConfigDefinitions::udprMC);
+        if (inet_pton(AF_INET, mcAddr.c_str(), &listenA.sin_addr) != 1)
+        {
+            LOG_ERROR("UDPR: invalid mc address %s", mcAddr.c_str());
+            close(fd);
+            fd = -1;
+            return;
+        }
+        LOG_INFO("UDPR: using multicast address %s",mcAddr.c_str());
+    }
+    if (type == T_AP){
+        listenA.sin_addr=apAddr;
+    }
     String sta;
     if (WiFi.isConnected()) sta=WiFi.localIP().toString();
     setStationAdd(sta);

From 098b9ba55826990c9fc6e57da7a5835765a258ea Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Mon, 11 Nov 2024 20:28:57 +0100
Subject: [PATCH 47/58] use GwBuffer::fillData for udp receive

---
 lib/queue/GwBuffer.cpp           | 10 ----------
 lib/queue/GwBuffer.h             |  5 +----
 lib/socketserver/GwUdpReader.cpp | 19 +++++++++++--------
 3 files changed, 12 insertions(+), 22 deletions(-)

diff --git a/lib/queue/GwBuffer.cpp b/lib/queue/GwBuffer.cpp
index de0f070..66a8d75 100644
--- a/lib/queue/GwBuffer.cpp
+++ b/lib/queue/GwBuffer.cpp
@@ -33,16 +33,6 @@ size_t GwBuffer::freeSpace()
     }
     return readPointer - writePointer - 1;
 }
-size_t GwBuffer::continousSpace() const{
-    if (readPointer <= writePointer){
-        return bufferSize-offset(writePointer);
-    }
-    return readPointer-writePointer-1;
-}
-void GwBuffer::moveWp(size_t offset){
-    if (offset > continousSpace()) return;
-    writePointer+=offset;
-}
 size_t GwBuffer::usedSpace()
 {
     if (readPointer <= writePointer)
diff --git a/lib/queue/GwBuffer.h b/lib/queue/GwBuffer.h
index aa67e04..145984f 100644
--- a/lib/queue/GwBuffer.h
+++ b/lib/queue/GwBuffer.h
@@ -18,9 +18,9 @@ class GwMessageFetcher{
  * buffer to safely inserte data if it fits
  * and to write out data if possible
  */
-typedef size_t (*GwBufferHandleFunction)(uint8_t *buffer, size_t len, void *param);
 class GwBuffer{
     public:
+        using GwBufferHandleFunction=std::function<size_t(uint8_t *buffer, size_t len, void *param)>;
         static const size_t TX_BUFFER_SIZE=1620; // app. 20 NMEA messages
         static const size_t RX_BUFFER_SIZE=600;  // enough for 1 NMEA message or actisense message or seasmart message
         typedef enum {
@@ -54,9 +54,6 @@ class GwBuffer{
          * find the first occurance of x in the buffer, -1 if not found
          */
         int findChar(char x);
-        uint8_t *getWp(){return writePointer;}
-        size_t continousSpace() const; //free space from wp
-        void moveWp(size_t offset); //move the wp forward
 };
 
 #endif
\ No newline at end of file
diff --git a/lib/socketserver/GwUdpReader.cpp b/lib/socketserver/GwUdpReader.cpp
index b88848c..612eb10 100644
--- a/lib/socketserver/GwUdpReader.cpp
+++ b/lib/socketserver/GwUdpReader.cpp
@@ -142,15 +142,18 @@ void GwUdpReader::readMessages(GwMessageFetcher *writer)
     if (fd < 0) return;
     //we expect one NMEA message in one UDP packet
     buffer->reset();
-    struct sockaddr_in from;
-    socklen_t fromLen=sizeof(from);
-    ssize_t res=recvfrom(fd,buffer->getWp(),buffer->continousSpace(),MSG_DONTWAIT,
+    size_t rd=buffer->fillData(buffer->freeSpace(),
+    [this](uint8_t *rcvb,size_t rcvlen,void *param)->size_t{
+        struct sockaddr_in from;
+        socklen_t fromLen=sizeof(from);
+        ssize_t res=recvfrom(fd,rcvb,rcvlen,MSG_DONTWAIT,
         (struct sockaddr*)&from,&fromLen);
-    if (res <= 0) return;
-    if (GwSocketHelper::equals(from.sin_addr,apAddr)) return;
-    if (!currentStationIp.isEmpty() && (GwSocketHelper::equals(from.sin_addr,staAddr))) return;
-    buffer->moveWp(res);
-    LOG_DEBUG(GwLog::DEBUG,"UDPR: received %d bytes",res);
+        if (res <= 0) return 0;
+        if (GwSocketHelper::equals(from.sin_addr,apAddr)) return 0;
+        if (!currentStationIp.isEmpty() && (GwSocketHelper::equals(from.sin_addr,staAddr))) return 0;
+        return res;
+    },this);
+    if (buffer->usedSpace() > 0)(GwLog::DEBUG,"UDPR: received %d bytes",buffer->usedSpace());
     writer->handleBuffer(buffer);   
 }
 size_t GwUdpReader::sendToClients(const char *buf, int source,bool partial)

From 4504b8832f262f8c2816929de61031f1d8a48024 Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Wed, 13 Nov 2024 10:59:00 +0100
Subject: [PATCH 48/58] #66: pick minimal changes of latest version from
 NMEA0183-AIS

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

diff --git a/lib/nmea2ktoais/NMEA0183AISMessages.cpp b/lib/nmea2ktoais/NMEA0183AISMessages.cpp
index 081a1b6..a0f9ec0 100644
--- a/lib/nmea2ktoais/NMEA0183AISMessages.cpp
+++ b/lib/nmea2ktoais/NMEA0183AISMessages.cpp
@@ -219,7 +219,7 @@ bool SetAISClassBMessage18(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, u
 bool SetAISClassBMessage24PartA(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, uint32_t UserID, char *Name) {
 
   bool found = false;
-  for (int i = 0; i < vships.size(); i++) {
+  for (size_t i = 0; i < vships.size(); i++) {
     if ( vships[i]->_userID == UserID ) {
       found = true;
       break;

From 7ea500bad650b17bc4210fb94422e3ffd5674d00 Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Wed, 13 Nov 2024 15:08:27 +0100
Subject: [PATCH 49/58] #87: include system name in export file name

---
 web/index.js | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/web/index.js b/web/index.js
index a82f79e..eabd250 100644
--- a/web/index.js
+++ b/web/index.js
@@ -1071,8 +1071,12 @@
     function formatDateForFilename(usePrefix, d) {
         let rt = "";
         if (usePrefix) {
+            let hdl= document.getElementById('headline');
+            if (hdl){
+                rt=hdl.textContent+"_";
+            }
             let fwt = document.querySelector('.status-fwtype');
-            if (fwt) rt = fwt.textContent;
+            if (fwt) rt += fwt.textContent;
             rt += "_";
         }
         if (!d) d = new Date();

From 2c87be78db93550762ddc4cf9e7ed251cd119c3a Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Wed, 13 Nov 2024 17:05:17 +0100
Subject: [PATCH 50/58] make changing timeouts working correctly

---
 lib/boatData/GwBoatData.cpp | 62 ++++++++++++++++++++++---------------
 lib/boatData/GwBoatData.h   | 19 +++++++++---
 src/main.cpp                |  1 +
 3 files changed, 52 insertions(+), 30 deletions(-)

diff --git a/lib/boatData/GwBoatData.cpp b/lib/boatData/GwBoatData.cpp
index 4be1450..39d22c8 100644
--- a/lib/boatData/GwBoatData.cpp
+++ b/lib/boatData/GwBoatData.cpp
@@ -48,12 +48,36 @@ GwBoatItemBase::GwBoatItemBase(String name, String format, GwBoatItemBase::TOTyp
     this->type = 0;
     this->lastUpdateSource = -1;
 }
-void GwBoatItemBase::setInvalidTime(unsigned long it, bool force){
-    if (toType != TOType::user || force ){
-        invalidTime=it;
+void GwBoatItemBase::setInvalidTime(GwConfigHandler *cfg){
+    if (toType != TOType::user){
+        unsigned long timeout=GwBoatItemBase::INVALID_TIME;
+        switch(getToType()){
+            case GwBoatItemBase::TOType::ais:
+                timeout=cfg->getInt(GwConfigDefinitions::timoAis);
+                break;
+            case GwBoatItemBase::TOType::def:
+                timeout=cfg->getInt(GwConfigDefinitions::timoDefault);
+                break;
+            case GwBoatItemBase::TOType::lng:
+                timeout=cfg->getInt(GwConfigDefinitions::timoLong);
+                break;
+            case GwBoatItemBase::TOType::sensor:
+                timeout=cfg->getInt(GwConfigDefinitions::timoSensor);
+                break;
+            case GwBoatItemBase::TOType::keep:
+                timeout=0;
+                break;
+        }
+        invalidTime=timeout;
     }
 }
 size_t GwBoatItemBase::getJsonSize() { return JSON_OBJECT_SIZE(10); }
+
+void GwBoatItemBase::GwBoatItemMap::add(const String &name,GwBoatItemBase *item){
+    boatData->setInvalidTime(item);
+    (*this)[name]=item;
+}
+
 #define STRING_SIZE 40
 GwBoatItemBase::StringWriter::StringWriter()
 {
@@ -127,7 +151,7 @@ GwBoatItem<T>::GwBoatItem(String name, String formatInfo, unsigned long invalidT
     this->type = GwBoatItemTypes::getType(dummy);
     if (map)
     {
-        (*map)[name] = this;
+        map->add(name,this);
     }
 }
 template <class T>
@@ -137,7 +161,7 @@ GwBoatItem<T>::GwBoatItem(String name, String formatInfo, GwBoatItemBase::TOType
     this->type = GwBoatItemTypes::getType(dummy);
     if (map)
     {
-        (*map)[name] = this;
+        map->add(name,this);
     }
 }
 
@@ -322,28 +346,12 @@ void GwBoatDataSatList::toJsonDoc(GwJsonDocument *doc, unsigned long minTime)
 GwBoatData::GwBoatData(GwLog *logger, GwConfigHandler *cfg)
 {
     this->logger = logger;
+    this->config = cfg;
+}
+void GwBoatData::begin(){
     for (auto &&it : values){
-        unsigned long timeout=GwBoatItemBase::INVALID_TIME;
-        switch(it.second->getToType()){
-            case GwBoatItemBase::TOType::ais:
-                timeout=cfg->getInt(GwConfigDefinitions::timoAis);
-                break;
-            case GwBoatItemBase::TOType::def:
-                timeout=cfg->getInt(GwConfigDefinitions::timoDefault);
-                break;
-            case GwBoatItemBase::TOType::lng:
-                timeout=cfg->getInt(GwConfigDefinitions::timoLong);
-                break;
-            case GwBoatItemBase::TOType::sensor:
-                timeout=cfg->getInt(GwConfigDefinitions::timoSensor);
-                break;
-            case GwBoatItemBase::TOType::keep:
-                timeout=0;
-                break;
-        }
-        it.second->setInvalidTime(timeout);
+        it.second->setInvalidTime(config);
     }
-
 }
 GwBoatData::~GwBoatData()
 {
@@ -456,6 +464,10 @@ double GwBoatData::getDoubleValue(String name, double defaultv)
         return defaultv;
     return it->second->getDoubleValue();
 }
+
+void GwBoatData::setInvalidTime(GwBoatItemBase *item){
+    if (config != nullptr) item->setInvalidTime(config);
+}
 double formatCourse(double cv)
 {
     double rt = cv * 180.0 / M_PI;
diff --git a/lib/boatData/GwBoatData.h b/lib/boatData/GwBoatData.h
index 18a5f08..a4df3b4 100644
--- a/lib/boatData/GwBoatData.h
+++ b/lib/boatData/GwBoatData.h
@@ -14,6 +14,8 @@
 #define ROT_WA_FACTOR 60
 
 class GwJsonDocument;
+class GwBoatData;
+
 class GwBoatItemBase{
     public:
         using TOType=enum{
@@ -56,7 +58,6 @@ class GwBoatItemBase{
         GWSC(formatRot);
         GWSC(formatDate);
         GWSC(formatTime);
-        typedef std::map<String,GwBoatItemBase*> GwBoatItemMap;
     protected:
         int type;
         unsigned long lastSet=0;
@@ -93,10 +94,15 @@ class GwBoatItemBase{
         virtual double getDoubleValue()=0;
         String getName(){return name;}
         const String & getFormat() const{return format;}
-        virtual void setInvalidTime(unsigned long it, bool force=true);
+        virtual void setInvalidTime(GwConfigHandler *cfg);
         TOType getToType(){return toType;}
+        class GwBoatItemMap : public std::map<String,GwBoatItemBase*>{
+            GwBoatData *boatData;
+            public:
+            GwBoatItemMap(GwBoatData *bd):boatData(bd){}
+            void add(const String &name,GwBoatItemBase *item);
+        };
 };
-class GwBoatData;
 template<class T> class GwBoatItem : public GwBoatItemBase{
     protected:
         T data;
@@ -186,8 +192,9 @@ public:
     clazz *name=new clazz(#name,GwBoatItemBase::fmt,toType,&values) ;
 class GwBoatData{
     private:
-        GwLog *logger;
-        GwBoatItemBase::GwBoatItemMap values;
+        GwLog *logger=nullptr;
+        GwConfigHandler *config=nullptr;
+        GwBoatItemBase::GwBoatItemMap values{this};
     public:
 
     GWBOATDATA(double,COG,formatCourse) // course over ground
@@ -231,9 +238,11 @@ class GwBoatData{
     public:
         GwBoatData(GwLog *logger, GwConfigHandler *cfg);
         ~GwBoatData();
+        void begin();
         template<class T> GwBoatItem<T> *getOrCreate(T initial,GwBoatItemNameProvider *provider);
         template<class T> bool update(T value,int source,GwBoatItemNameProvider *provider);
         template<class T> T getDataWithDefault(T defaultv, GwBoatItemNameProvider *provider);
+        void setInvalidTime(GwBoatItemBase *item);
         bool isValid(String name);
         double getDoubleValue(String name,double defaultv);
         GwBoatItemBase *getBase(String name);
diff --git a/src/main.cpp b/src/main.cpp
index 714c0a5..90e5691 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -784,6 +784,7 @@ void setup() {
     logger.prefix="FALLBACK:";
   logger.setWriter(new DefaultLogWriter());
 #endif
+  boatData.begin();
   userCodeHandler.startInitTasks(MIN_USER_TASK);
   channels.preinit();
   config.stopChanges();

From a8a0df4b70c42e05ba0ac04b6e7317fe733d4947 Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Wed, 13 Nov 2024 18:07:50 +0100
Subject: [PATCH 51/58] #50: fix handling of GSV messages (completely wrong sat
 info)

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

diff --git a/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp b/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp
index be01cac..bd593d5 100644
--- a/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp
+++ b/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp
@@ -860,7 +860,7 @@ private:
             LOG_DEBUG(GwLog::DEBUG,"GSV invalid current %u %s",current,msg.line);
             return;
         }
-        for (int idx=2;idx < msg.FieldCount();idx+=4){
+        for (int idx=3;idx < msg.FieldCount();idx+=4){
             if (msg.FieldLen(idx) < 1 ||
                 msg.FieldLen(idx+1) < 1 ||
                 msg.FieldLen(idx+2) < 1 ||

From f73390c9aef827e187e05d0a48aefeaaf8df1bfb Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Thu, 14 Nov 2024 15:20:13 +0100
Subject: [PATCH 52/58] enable channel if only seasmart or actisense is
 configured

---
 lib/channel/GwChannelList.cpp | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/lib/channel/GwChannelList.cpp b/lib/channel/GwChannelList.cpp
index 4371cef..7861f3d 100644
--- a/lib/channel/GwChannelList.cpp
+++ b/lib/channel/GwChannelList.cpp
@@ -369,17 +369,20 @@ static GwChannel * createChannel(GwLog *logger, GwConfigHandler *config, int id,
         return nullptr;
     }
     GwChannel *channel = new GwChannel(logger, param->name,param->id,param->maxId);
+    bool sendSeaSmart=config->getBool(param->sendSeasmart);
+    bool readAct=config->getBool(param->readAct);
+    bool writeAct=config->getBool(param->writeAct);
     channel->setImpl(impl);
     channel->begin(
-        canRead || canWrite,
+        canRead || canWrite || readAct || writeAct|| sendSeaSmart,
         canWrite,
         canRead,
         config->getString(param->readF),
         config->getString(param->writeF),
-        config->getBool(param->sendSeasmart),
+        sendSeaSmart,
         config->getBool(param->toN2K),
-        config->getBool(param->readAct),
-        config->getBool(param->writeAct));
+        readAct,
+        writeAct);
     LOG_INFO("created channel %s",channel->toString().c_str());
     return channel;
 }

From 048a16d9391bf3a97910f03aa562c2335b697df8 Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Thu, 14 Nov 2024 16:16:40 +0100
Subject: [PATCH 53/58] add listeners for status handling

---
 web/index.js | 106 ++++++++++++++++++++++++++++-----------------------
 1 file changed, 59 insertions(+), 47 deletions(-)

diff --git a/web/index.js b/web/index.js
index eabd250..538a51a 100644
--- a/web/index.js
+++ b/web/index.js
@@ -76,47 +76,49 @@
         }
         getJson('/api/status')
             .then(function (jsonData) {
+                if (jsonData.salt !== undefined) {
+                    lastSalt=jsonData.salt;
+                    delete jsonData.salt;
+                }
+                if (jsonData.minUser !== undefined){
+                    minUser=jsonData.minUser;
+                    delete jsonData.minUser;
+                }
+                callListeners(api.EVENTS.status,jsonData);
                 let statusPage = document.getElementById('statusPageContent');
                 let even = true; //first counter
-                for (let k in jsonData) {
-                    if (k == "salt") {
-                        lastSalt = jsonData[k];
-                        continue;
-                    }
-                    if (k == "minUser") {
-                        minUser = parseInt(jsonData[k]);
-                        continue;
-                    }
-                    if (!statusPage) continue;
-                    if (typeof (jsonData[k]) === 'object') {
-                        if (k.indexOf('count') == 0) {
-                            createCounterDisplay(statusPage, k.replace("count", "").replace(/in$/, " in").replace(/out$/, " out"), k, even);
-                            even = !even;
-                            for (let sk in jsonData[k]) {
-                                let key = k + "." + sk;
-                                if (typeof (jsonData[k][sk]) === 'object') {
-                                    //msg details
-                                    updateMsgDetails(key, jsonData[k][sk]);
-                                }
-                                else {
-                                    let el = document.getElementById(key);
-                                    if (el) el.textContent = jsonData[k][sk];
+                if (statusPage){
+                    for (let k in jsonData) {
+                        if (typeof (jsonData[k]) === 'object') {
+                            if (k.indexOf('count') == 0) {
+                                createCounterDisplay(statusPage, k.replace("count", "").replace(/in$/, " in").replace(/out$/, " out"), k, even);
+                                even = !even;
+                                for (let sk in jsonData[k]) {
+                                    let key = k + "." + sk;
+                                    if (typeof (jsonData[k][sk]) === 'object') {
+                                        //msg details
+                                        updateMsgDetails(key, jsonData[k][sk]);
+                                    }
+                                    else {
+                                        let el = document.getElementById(key);
+                                        if (el) el.textContent = jsonData[k][sk];
+                                    }
                                 }
                             }
+                            if (k.indexOf("ch") == 0) {
+                                //channel def
+                                let name = k.substring(2);
+                                channelList[name] = jsonData[k];
+                            }
                         }
-                        if (k.indexOf("ch") == 0) {
-                            //channel def
-                            let name = k.substring(2);
-                            channelList[name] = jsonData[k];
+                        else {
+                            let el = document.getElementById(k);
+                            if (el) el.textContent = jsonData[k];
+                            forEl('.status-' + k, function (el) {
+                                el.textContent = jsonData[k];
+                            });
                         }
                     }
-                    else {
-                        let el = document.getElementById(k);
-                        if (el) el.textContent = jsonData[k];
-                        forEl('.status-' + k, function (el) {
-                            el.textContent = jsonData[k];
-                        });
-                    }
                 }
                 lastUpdate = (new Date()).getTime();
                 if (reloadConfig) {
@@ -382,6 +384,7 @@
                 icon.classList.add('icon-less');
             }
         });
+        callListeners(api.EVENTS.counterDisplayCreated,row);
     }
     function validKey(key) {
         if (!key) return;
@@ -1996,8 +1999,16 @@
         hideDashboardItem(name); //will recreate it on next data receive
     }
     const api= {
-        registerListener: function (callback) {
-            listeners.push(callback);
+        registerListener: function (callback,opt_event) {
+            if (opt_event === undefined){
+                listeners.push(callback);
+            }
+            else{
+                listeners.push({
+                    event:opt_event,
+                    callback:callback
+                })
+            }
         },
         /**
          * helper for creating dom elements
@@ -2058,11 +2069,13 @@
         parseBoatDataLine: parseBoatDataLine,
         EVENTS: {
             init: 0, //called when capabilities are loaded, data is capabilities
-            tab: 1, //tab page activated data is the id of the tab page
-            config: 2, //data is the config object
-            boatData: 3, //data is the list of boat Data items
+            tab: 1, //tab page activated, data is the id of the tab page
+            config: 2, //called when the config data is loaded,data is the config object
+            boatData: 3, //called when boatData is received, data is the list of boat Data items
             dataItemCreated: 4, //data is an object with
                                 // name: the item name, element: the frame item of the boat data display
+            status: 5, //status received, data is the status object
+            counterDisplayCreated: 6 //data is the row for the display
         }
     };
     function callListeners(event,data){
@@ -2070,6 +2083,13 @@
             if (typeof(listener) === 'function'){
                 listener(event,data);
             }
+            else if (typeof(listener) === 'object'){
+                if (listener.event === event){
+                    if (typeof(listener.callback) === 'function'){
+                        listener.callback(event,data);
+                    }
+                }
+            }
         })
     }
     window.esp32nmea2k = api;
@@ -2120,14 +2140,6 @@
                 });
             }
         } catch (e) { }
-        let statusPage = document.getElementById('statusPageContent');
-        /*if (statusPage){
-            let even=true;
-            for (let c in counters){
-                createCounterDisplay(statusPage,counters[c],c,even);
-                even=!even;
-            }
-        }*/
         forEl('#uploadFile', function (el) {
             el.addEventListener('change', function (ev) {
                 if (ev.target.files.length < 1) return;

From 506dd7ea9f11d5876c68e8a42c1431ed8abb2c8d Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Thu, 14 Nov 2024 16:16:51 +0100
Subject: [PATCH 54/58] add example listener

---
 lib/exampletask/index.js | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/lib/exampletask/index.js b/lib/exampletask/index.js
index a85fe10..da098e0 100644
--- a/lib/exampletask/index.js
+++ b/lib/exampletask/index.js
@@ -11,6 +11,11 @@
     const infoUrl='https://github.com/wellenvogel/esp32-nmea2000/tree/master/lib/exampletask';
     let boatItemName;
     let boatItemElement;
+    api.registerListener((id,data)=>{
+        if (isActive){
+            console.log("exampletask status listener",data);
+        }
+    },api.EVENTS.status)
     api.registerListener((id,data)=>{
         if (id === api.EVENTS.init){
             //data is capabilities

From 538f643fbffb83db65c5cc52bf5c3a7cf855c9ed Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Thu, 14 Nov 2024 18:15:12 +0100
Subject: [PATCH 55/58] add registerRequestHandler to the API with examples

---
 lib/api/GwApi.h                   | 16 +++++++++++++
 lib/exampletask/GwExampleTask.cpp | 37 +++++++++++++++++++++++++++++++
 lib/exampletask/Readme.md         | 20 +++++++++++++++++
 lib/exampletask/index.js          | 15 +++++++++++++
 lib/gwwebserver/GwWebServer.h     |  3 ++-
 lib/queue/GwSynchronized.h        |  4 ++--
 lib/usercode/GwUserCode.cpp       | 30 +++++++++++++++++++++++++
 lib/usercode/GwUserCode.h         |  2 ++
 src/main.cpp                      | 10 ++++++++-
 9 files changed, 133 insertions(+), 4 deletions(-)

diff --git a/lib/api/GwApi.h b/lib/api/GwApi.h
index 5a24e92..9d79ef3 100644
--- a/lib/api/GwApi.h
+++ b/lib/api/GwApi.h
@@ -6,7 +6,9 @@
 #include "GWConfig.h"
 #include "GwBoatData.h"
 #include "GwXDRMappings.h"
+#include "GwSynchronized.h"
 #include <map>
+#include <ESPAsyncWebServer.h>
 class GwApi;
 typedef void (*GwUserTaskFunction)(GwApi *);
 //API to be used for additional tasks
@@ -171,6 +173,20 @@ class GwApi{
         virtual void remove(int idx){}
         virtual TaskInterfaces * taskInterfaces()=0;
 
+        /**
+         * register handler for web URLs
+         * Please be aware that this handler function will always be called from a separate
+         * task. So you must ensure proper synchronization!
+        */
+        using HandlerFunction=std::function<void(AsyncWebServerRequest *)>;
+        /**
+         * @param url: the url of that will trigger the handler.
+         *             it will be prefixed with /api/user/<taskname>
+         *             taskname is the name that you used in addUserTask
+         * @param handler: the handler function (see remark above about thread synchronization)
+        */
+        virtual void registerRequestHandler(const String &url,HandlerFunction handler)=0;
+
         /**
          * only allowed during init methods
         */
diff --git a/lib/exampletask/GwExampleTask.cpp b/lib/exampletask/GwExampleTask.cpp
index 340b785..9b4cbcd 100644
--- a/lib/exampletask/GwExampleTask.cpp
+++ b/lib/exampletask/GwExampleTask.cpp
@@ -7,6 +7,7 @@
 #include <vector>
 #include "N2kMessages.h"
 #include "GwXdrTypeMappings.h"
+
 /**
  * INVALID!!! - the next interface declaration will not work
  *              as it is not in the correct header file
@@ -144,6 +145,26 @@ String formatValue(GwApi::BoatValue *value){
     return String(buffer);
 }
 
+class ExampleWebData{
+    SemaphoreHandle_t lock;
+    int data=0;
+    public:
+    ExampleWebData(){
+        lock=xSemaphoreCreateMutex();
+    }
+    ~ExampleWebData(){
+        vSemaphoreDelete(lock);
+    }
+    void set(int v){
+        GWSYNCHRONIZED(&lock);
+        data=v;
+    }
+    int get(){
+        GWSYNCHRONIZED(&lock);
+        return data;
+    }
+};
+
 void exampleTask(GwApi *api){
     GwLog *logger=api->getLogger();
     //get some configuration data
@@ -172,8 +193,24 @@ void exampleTask(GwApi *api){
     LOG_DEBUG(GwLog::LOG,"exampleNotWorking update returned %d",(int)nwrs);
     String voltageTransducer=api->getConfig()->getString(GwConfigDefinitions::exTransducer);
     int voltageInstance=api->getConfig()->getInt(GwConfigDefinitions::exInstanceId);
+    ExampleWebData webData;
+    /**
+     * an example web request handler
+     * it uses a synchronized data structure as it gets called from a different thread
+     * be aware that you must not block for longer times here!
+    */
+    api->registerRequestHandler("data",[&webData](AsyncWebServerRequest *request){
+        int data=webData.get();
+        char buffer[30];
+        snprintf(buffer,29,"%d",data);
+        buffer[29]=0;
+        request->send(200,"text/plain",buffer); 
+    });
+    int loopcounter=0;
     while(true){
         delay(1000);
+        loopcounter++;
+        webData.set(loopcounter);
         /*
         * getting values from the internal data store (boatData) requires some special handling
         * our tasks runs (potentially) at some time on a different core then the main code
diff --git a/lib/exampletask/Readme.md b/lib/exampletask/Readme.md
index 965e177..2a7e42e 100644
--- a/lib/exampletask/Readme.md
+++ b/lib/exampletask/Readme.md
@@ -32,6 +32,26 @@ Files
      This file allows to add some config definitions that are needed for our task. For the possible options have a look at the global [config.json](../../web/config.json). Be careful not to overwrite config defitions from the global file. A good practice wood be to prefix the names of definitions with parts of the library name. Always put them in a separate category so that they do not interfere with the system ones.
      The defined config items can later be accessed in the code (see the example in [GwExampleTask.cpp](GwExampleTask.cpp)).
 
+   * [index.js](index.js)<br>
+     You can add javascript code that will contribute to the UI of the system. The WebUI provides a small API that allows you to "hook" into some functions to include your own parts of the UI. This includes adding new tabs, modifying/replacing the data display items, modifying the status display or accessing the config items.
+     For the API refer to [../../web/index.js](../../web/index.js#L2001).
+     To start interacting just register for some events like api.EVENTS.init. You can check the capabilities you have defined to see if your task is active.
+     By registering an own formatter [api.addUserFormatter](../../web/index.js#L2054) you can influence the way boat data items are shown.
+     You can even go for an own display by registering for the event *dataItemCreated* and replace the dom element content with your own html. By additionally having added a user formatter you can now fill your own html with the current value.
+     By using [api.addTabPage](../../web/index.js#L2046) you can add new tabs that you can populate with your own code. Or you can link to an external URL.<br>
+     Please be aware that your js code is always combined with the code from the core into one js file.<br>
+     For fast testing there is a small python script that allow you to test the UI without always flushing each change.
+     Just run it with
+     ```
+     tools/testServer.py nnn http://x.x.x.x/api
+     ```
+     with nnn being the local port and x.x.x.x the address of a running system. Open `http://localhost:nnn` in your browser.<br>
+     After a change just start the compilation and reload the page.
+
+   * [index.css](index.css)<br>
+     You can add own css to influence the styling of the display.
+
+
  Interfaces
  ----------
  The task init function and the task function interact with the core using an [API](../api/GwApi.h) that they get when started.
diff --git a/lib/exampletask/index.js b/lib/exampletask/index.js
index da098e0..155991c 100644
--- a/lib/exampletask/index.js
+++ b/lib/exampletask/index.js
@@ -28,6 +28,21 @@
                 //you can use the helper addEl to create elements
                 let page=api.addTabPage(tabName,"Example");
                 api.addEl('div','hdg',page,"this is a test tab");
+                let vrow=api.addEl('div','row',page);
+                api.addEl('span','label',vrow,'loops: ');
+                let lcount=api.addEl('span','value',vrow,'0');
+                //query the loop count
+                window.setInterval(()=>{
+                    fetch('/api/user/exampleTask/data')
+                        .then((res)=>{
+                            if (! res.ok) throw Error("server error: "+res.status);
+                            return res.text();
+                        })
+                        .then((txt)=>{
+                            lcount.textContent=txt;
+                        })
+                        .catch((e)=>console.log("rq:",e));
+                },1000);
                 api.addEl('button','',page,'Info').addEventListener('click',function(ev){
                     window.open(infoUrl,'info');
                 })
diff --git a/lib/gwwebserver/GwWebServer.h b/lib/gwwebserver/GwWebServer.h
index 84c265a..c2b48d8 100644
--- a/lib/gwwebserver/GwWebServer.h
+++ b/lib/gwwebserver/GwWebServer.h
@@ -4,6 +4,7 @@
 #include <functional>
 #include "GwMessage.h"
 #include "GwLog.h"
+#include "GwApi.h"
 class GwWebServer{
     private:
         AsyncWebServer *server;
@@ -11,7 +12,7 @@ class GwWebServer{
         GwLog *logger;
     public:
         typedef GwRequestMessage *(RequestCreator)(AsyncWebServerRequest *request);
-        using HandlerFunction=std::function<void(AsyncWebServerRequest *)>;
+        using HandlerFunction=GwApi::HandlerFunction;
         GwWebServer(GwLog *logger, GwRequestQueue *queue,int port);
         ~GwWebServer();
         void begin();
diff --git a/lib/queue/GwSynchronized.h b/lib/queue/GwSynchronized.h
index 53241db..786b5f0 100644
--- a/lib/queue/GwSynchronized.h
+++ b/lib/queue/GwSynchronized.h
@@ -7,10 +7,10 @@ class GwSynchronized{
     public:
         GwSynchronized(SemaphoreHandle_t *locker){
             this->locker=locker;
-            xSemaphoreTake(*locker, portMAX_DELAY);
+            if (locker != nullptr) xSemaphoreTake(*locker, portMAX_DELAY);
         }
         ~GwSynchronized(){
-            xSemaphoreGive(*locker);
+            if (locker != nullptr) xSemaphoreGive(*locker);
         }
 };
 
diff --git a/lib/usercode/GwUserCode.cpp b/lib/usercode/GwUserCode.cpp
index 4522c92..211eda9 100644
--- a/lib/usercode/GwUserCode.cpp
+++ b/lib/usercode/GwUserCode.cpp
@@ -191,6 +191,7 @@ class TaskApi : public GwApiInternal
     SemaphoreHandle_t *mainLock;
     SemaphoreHandle_t localLock;
     std::map<int,GwCounter<String>> counter;
+    std::map<String,GwApi::HandlerFunction> webHandlers;
     String name;
     bool counterUsed=false;
     int counterIdx=0;
@@ -315,6 +316,10 @@ public:
     virtual bool addXdrMapping(const GwXDRMappingDef &def){
         return api->addXdrMapping(def);
     }
+    virtual void registerRequestHandler(const String &url,HandlerFunction handler){
+        GWSYNCHRONIZED(&localLock);
+        webHandlers[url]=handler;
+    }
     virtual void addCapability(const String &name, const String &value){
         if (! isInit) return;
         userCapabilities[name]=value;
@@ -335,6 +340,16 @@ public:
     virtual void setCalibrationValue(const String &name, double value){
         api->setCalibrationValue(name,value);
     }
+    virtual bool handleWebRequest(const String &url,AsyncWebServerRequest *req){
+        GWSYNCHRONIZED(&localLock);
+        auto it=webHandlers.find(url);
+        if (it == webHandlers.end()){
+            api->getLogger()->logDebug(GwLog::LOG,"no web handler task=%s url=%s",name.c_str(),url.c_str());
+            return false;
+        }
+        it->second(req);
+        return true;
+    }
 
 };
 
@@ -404,4 +419,19 @@ int GwUserCode::getJsonSize(){
         }
     }
     return rt;
+}
+void GwUserCode::handleWebRequest(const String &url,AsyncWebServerRequest *req){
+    int sep1=url.indexOf('/');
+    String tname;
+    if (sep1 > 0){
+        tname=url.substring(0,sep1);
+        for (auto &&it:userTasks){
+            if (it.api && it.name == tname){
+                if (it.api->handleWebRequest(url.substring(sep1+1),req)) return;
+                break;
+            }
+        }
+    }
+    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/lib/usercode/GwUserCode.h b/lib/usercode/GwUserCode.h
index a218bc9..94e745d 100644
--- a/lib/usercode/GwUserCode.h
+++ b/lib/usercode/GwUserCode.h
@@ -11,6 +11,7 @@ class GwApiInternal : public GwApi{
     ~GwApiInternal(){}
     virtual void fillStatus(GwJsonDocument &status){};
     virtual int getJsonSize(){return 0;};
+    virtual bool handleWebRequest(const String &url,AsyncWebServerRequest *req){return false;}
 };
 class GwUserTask{
     public:
@@ -50,5 +51,6 @@ class GwUserCode{
         Capabilities *getCapabilities();
         void fillStatus(GwJsonDocument &status);
         int getJsonSize();
+        void handleWebRequest(const String &url,AsyncWebServerRequest *);
 };
 #endif
\ No newline at end of file
diff --git a/src/main.cpp b/src/main.cpp
index 90e5691..ae8f3d5 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -348,6 +348,8 @@ public:
     }
     return xdrMappings.addFixedMapping(mapping);
   }
+  virtual void registerRequestHandler(const String &url,HandlerFunction handler){
+  }
   virtual void addCapability(const String &name, const String &value){}
   virtual bool addUserTask(GwUserTaskFunction task,const String Name, int stackSize=2000){
     return false;
@@ -768,6 +770,7 @@ void loopFunction(void *){
     //delay(1);
   }
 }
+const String USERPREFIX="/api/user/";
 void setup() {
   mainLock=xSemaphoreCreateMutex();
   uint8_t chipid[6];
@@ -845,7 +848,12 @@ void setup() {
                               snprintf(buffer,29,"%g",value);
                               buffer[29]=0;
                               request->send(200,"text/plain",buffer);    
-  });                                                        
+  });
+  webserver.registerHandler((USERPREFIX+"*").c_str(),[&USERPREFIX](AsyncWebServerRequest *req){
+                              String turl=req->url().substring(USERPREFIX.length());
+                              logger.logDebug(GwLog::DEBUG,"user web request for %s",turl.c_str());
+                              userCodeHandler.handleWebRequest(turl,req);
+  });
 
   webserver.begin();
   xdrMappings.begin();

From c292b82635755acd5bffa9c17d339e6a32b67729 Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Thu, 14 Nov 2024 18:33:18 +0100
Subject: [PATCH 56/58] correct some js errors in example

---
 lib/exampletask/index.js | 173 +++++++++++++++++++--------------------
 1 file changed, 83 insertions(+), 90 deletions(-)

diff --git a/lib/exampletask/index.js b/lib/exampletask/index.js
index 155991c..b2c6f9d 100644
--- a/lib/exampletask/index.js
+++ b/lib/exampletask/index.js
@@ -5,104 +5,97 @@
     //on our case this is "testboard"
     //so we only start any action when we receive the init event
     //and we successfully checked that our requested capability is there
-    let isActive=false;
     const tabName="example";
     const configName="exampleBDSel";
     const infoUrl='https://github.com/wellenvogel/esp32-nmea2000/tree/master/lib/exampletask';
     let boatItemName;
     let boatItemElement;
-    api.registerListener((id,data)=>{
-        if (isActive){
-            console.log("exampletask status listener",data);
-        }
-    },api.EVENTS.status)
-    api.registerListener((id,data)=>{
-        if (id === api.EVENTS.init){
-            //data is capabilities
-            //check if our requested capability is there (see GwExampleTask.h)
-            if (data.testboard) isActive=true;
-            if (isActive){
-                //add a simple additional tab page
-                //you will have to build the content of the page dynamically
-                //using normal dom manipulation methods
-                //you can use the helper addEl to create elements
-                let page=api.addTabPage(tabName,"Example");
-                api.addEl('div','hdg',page,"this is a test tab");
-                let vrow=api.addEl('div','row',page);
-                api.addEl('span','label',vrow,'loops: ');
-                let lcount=api.addEl('span','value',vrow,'0');
-                //query the loop count
-                window.setInterval(()=>{
-                    fetch('/api/user/exampleTask/data')
-                        .then((res)=>{
-                            if (! res.ok) throw Error("server error: "+res.status);
-                            return res.text();
-                        })
-                        .then((txt)=>{
-                            lcount.textContent=txt;
-                        })
-                        .catch((e)=>console.log("rq:",e));
-                },1000);
-                api.addEl('button','',page,'Info').addEventListener('click',function(ev){
-                    window.open(infoUrl,'info');
+    api.registerListener((id, data) => {
+        //data is capabilities
+        //check if our requested capability is there (see GwExampleTask.h)
+        if (!data.testboard) return; //do nothing if we are not active
+        //add a simple additional tab page
+        //you will have to build the content of the page dynamically
+        //using normal dom manipulation methods
+        //you can use the helper addEl to create elements
+        let page = api.addTabPage(tabName, "Example");
+        api.addEl('div', 'hdg', page, "this is a test tab");
+        let vrow = api.addEl('div', 'row', page);
+        api.addEl('span', 'label', vrow, 'loops: ');
+        let lcount = api.addEl('span', 'value', vrow, '0');
+        //query the loop count
+        window.setInterval(() => {
+            fetch('/api/user/exampleTask/data')
+                .then((res) => {
+                    if (!res.ok) throw Error("server error: " + res.status);
+                    return res.text();
                 })
-                //add a tab for an external URL
-                api.addTabPage('exhelp','Info',infoUrl);
+                .then((txt) => {
+                    //set the text content of our value element with what we received
+                    lcount.textContent = txt;
+                })
+                .catch((e) => console.log("rq:", e));
+        }, 1000);
+        api.addEl('button', '', page, 'Info').addEventListener('click', function (ev) {
+            window.open(infoUrl, 'info');
+        })
+        //add a tab for an external URL
+        api.addTabPage('exhelp', 'Info', infoUrl);
+        //now as we know we are active - register all the listeners we need
+        api.registerListener((id, data) => {
+            console.log("exampletask status listener", data);
+        }, api.EVENTS.status)
+        api.registerListener((id, data) => {
+            if (data === tabName) {
+                //maybe we need some activity when our page is being activated
+                console.log("example tab activated");
             }
-        }
-        if (isActive){
-            //console.log("exampletask listener",id,data);
-            if (id === api.EVENTS.tab){
-                if (data === tabName){
-                    //maybe we need some activity when our page is being activated
-                    console.log("example tab activated");
-                }
+        }, api.EVENTS.tab);
+
+        api.registerListener((id, data) => {
+            //we have a configuration that
+            //gives us the name of a boat data item we would like to 
+            //handle special
+            //in our case we just use an own formatter and add some
+            //css to the display field
+            //as this item can change we need to keep track of the 
+            //last item we handled
+            let nextboatItemName = data[configName];
+            console.log("value of " + configName, nextboatItemName);
+            if (nextboatItemName) {
+                //register a user formatter that will be called whenever
+                //there is a new valid value
+                //we simply add an "X:" in front
+                api.addUserFormatter(nextboatItemName, "m(x)", function (v, valid) {
+                    if (!valid) return;
+                    return "X:" + v;
+                })
+                //after this call the item will be recreated
             }
-            if (id == api.EVENTS.config){
-                //we have a configuration that
-                //gives us the name of a boat data item we would like to 
-                //handle special
-                //in our case we just use an own formatter and add some
-                //css to the display field
-                //as this item can change we need to keep track of the 
-                //last item we handled
-                let nextboatItemName=data[configName];
-                console.log("value of "+configName,nextboatItemName);
-                if (nextboatItemName){
-                    //register a user formatter that will be called whenever
-                    //there is a new valid value
-                    //we simply add an "X:" in front
-                    api.addUserFormatter(nextboatItemName,"m(x)",function(v,valid){
-                        if (!valid) return;
-                        return "X:"+v;
-                    })
-                    //after this call the item will be recreated
-                }
-                if (boatItemName !== undefined && boatItemName != nextboatItemName){
-                    //if the boat item that we handle has changed, remove
-                    //the previous user formatter (this will recreate the item)
-                    api.removeUserFormatter(boatItemName);
-                }
-                boatItemName=nextboatItemName;
-                boatItemElement=undefined;
+            if (boatItemName !== undefined && boatItemName != nextboatItemName) {
+                //if the boat item that we handle has changed, remove
+                //the previous user formatter (this will recreate the item)
+                api.removeUserFormatter(boatItemName);
             }
-            if (id == api.EVENTS.dataItemCreated){
-                //this event is called whenever a data item has
-                //been created (or recreated)
-                //if this is the item we handle, we just add a css class
-                //we could also completely rebuild the dom below the element
-                //and use our formatter to directly write/draw the data
-                //avoid direct manipulation of the element (i.e. changing the classlist)
-                //as this element remains there all the time
-                if (boatItemName && boatItemName == data.name){
-                    boatItemElement=data.element;
-                    //use the helper forEl to find elements within the dashboard item
-                    //the value element has the class "dashValue"
-                    api.forEl(".dashValue",function(el){
-                        el.classList.add("examplecss");
-                    },boatItemElement);
-                }
+            boatItemName = nextboatItemName;
+            boatItemElement = undefined;
+        }, api.EVENTS.config);
+        api.registerListener((id, data) => {
+            //this event is called whenever a data item has
+            //been created (or recreated)
+            //if this is the item we handle, we just add a css class
+            //we could also completely rebuild the dom below the element
+            //and use our formatter to directly write/draw the data
+            //avoid direct manipulation of the element (i.e. changing the classlist)
+            //as this element remains there all the time
+            if (boatItemName && boatItemName == data.name) {
+                boatItemElement = data.element;
+                //use the helper forEl to find elements within the dashboard item
+                //the value element has the class "dashValue"
+                api.forEl(".dashValue", function (el) {
+                    el.classList.add("examplecss");
+                }, boatItemElement);
             }
-        }
-    })
+        }, api.EVENTS.dataItemCreated);
+    }, api.EVENTS.init);
 })();

From 13efceeef703ea1d6ee32ddf48771428f8a63db2 Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Thu, 14 Nov 2024 19:47:10 +0100
Subject: [PATCH 57/58] extend doc for new api functions and networking options

---
 Readme.md                 |  5 ++-
 doc/network.md            | 80 +++++++++++++++++++++++++++++++++++++++
 lib/exampletask/Readme.md |  3 +-
 3 files changed, 86 insertions(+), 2 deletions(-)
 create mode 100644 doc/network.md

diff --git a/Readme.md b/Readme.md
index 6de6c6a..ebcd08b 100644
--- a/Readme.md
+++ b/Readme.md
@@ -47,10 +47,13 @@ Hardware
 --------
 The software is prepared to run on different kinds of ESP32 based modules and accessoirs. For some of them prebuild binaries are available that only need to be flashed, others would require to add some definitions of the used PINs and features and to build the binary.
 For the list of hardware set ups refer to [Hardware](doc/Hardware.md).
-For details of the usage of serial devices and the USB connection refer to [Serial and USB](doc/serial-usb.md).
 
 There is a couple of prebuild binaries that you can directly flash to your device. For other combinations of hardware there is an [online build service](doc/BuildService.md) that will allow you to select your hardware and trigger a build.
 
+Connectivity
+------------
+For details of the usage of serial devices and the USB connection refer to [Serial and USB](doc/serial-usb.md).<br>
+For details on the networking capabilities refer to [Networking](doc/network.md).
 
 Installation
 ------------
diff --git a/doc/network.md b/doc/network.md
new file mode 100644
index 0000000..6c92692
--- /dev/null
+++ b/doc/network.md
@@ -0,0 +1,80 @@
+Networking
+==========
+The gateway includes a couple of network functions to send and receive NMEA data on network connections and to use a WebBrowser for configuration and status display.
+
+To understand the networking functions you always have to consider 2 parts:
+
+The "physical" connection
+-------------------------
+For the gateway this is always a Wifi connection.
+It provides the ability to use it as an access point and/or to connect to another wifi network (wifi client).
+
+Access Point
+************
+When starting up it will create an own Wifi network (access point or AP) with the name of the system. You connect to this network like to any Wifi Hotspot.
+The initial password is esp32nmea2k. You should change this password on the configuration page in the Web UI.
+When you connect the gateway will provide you with an IP address (DHCP) - and it will also have an own IP address in this range (default: 192.168.15.1). If this IP address (range) conflicts with other devices (especially if you run multiple gateways and connect them) you can change the range at the system tab on the configuration page.<br>
+If you do not need the access point during normal operation you can set "stopApTime" so that it will shut down after this time (in minutes) if no device is connected. This will also reduce the power consumption.
+
+Wifi Client
+***********
+Beside the own access point the gateway can also connect to another Wifi network. You need to configure the SSID and password at the "wifi client" tab.
+On the status page you can see if the gateway is connected and which address it got within the network.
+
+You can use both networks to connect to the gateway. It announces it's services via [bonjour](https://developer.apple.com/bonjour/). So you normally should be able to reach your system using Name.local (name being the system name, default ESP32NMEA2K). Or you can use an app that allows for bonjour browsing.
+
+The "logical" connection
+------------------------
+After you connected a device to the gateway on either the access point or by using the same Wifi network you can easily access the Web UI with a browser - e.g. using the Name.local url.
+
+To send or receive NMEA data you additionally need to configure some connection between the gateway and your device(s).
+The gateway provides the following options for NMEA connections:
+
+TCP Server
+**********
+When using the TCP server you must configure a connection to the gateway _on the device_ that you would like to connect. The gateway listens at port 10110 (can be changed at the TCP server tab). So on your device you need to configure the address of the gateway and this port. The address depends on the network that you use to connect your device - either the address from the gateway access point (default: 192.168.15.1) - or the address that the gateway was given in the Wifi client network (check this on the status page).<br>
+If your device supports this you can also use the Name.local for the address - or let the device browse for a bonjour service.<br>
+The TCP server has a limit for the number of connections (can be configured, default: 6). As for any channel you can define what it will write and read on it's connections and apply filters.
+If you want to send NMEA2000 data via TCP you can enable "Seasmart out".
+
+TCP Client
+**********
+With this settings you can configure the gateway to establish a connection to another device that provides data via TCP. You need to know the address and port for that device. If the other device also uses bonjour (like e.g. a second gateway device) you can also use this name instead of the address.
+Like for the TCP server you can define what should be send or received with filters.
+
+UDP Reader
+**********
+UDP is distinct from TCP in that it does not establish a connection directly. Instead in sends/receives messages - without checking if they have been received by someone at all. Therefore it is also not able to ensure that really all messages are reaching their destination. But as the used protocols (NMEA0183, NMEA2000) are prepared for unreliable connections any way (for serial connections you normally have no idea if the data arrives) UDP is still a valid way of connecting devices.<br>
+One advantage of UDP is that you can send out messages as broadcast or multicast - thus allowing for multiple receivers.
+
+Small hint for __multicast__:<br>
+Normally in the environment the gateway will work you will not use multicast. If you want to send to multiple devices you will use broadcast. The major difference between them are 2 points:<br>
+  1. broadcast messages will not pass a real router (but they will be available to all devices connected to one access point)
+  2. broadcast messages will always be send to all devices - regardless whether they would like to receive them or not. This can create a bit more network traffic.
+
+Multicast requires that receivers announce their interest in receiving messages (by "joining" a so called multicast group) and this way only interested devices will receive such messages - and it is possible to configure routers in a way that they route multicast messages.
+
+To use the gateway UDP reader you must select from where you would like to receive messages. In any case you need to set up a port (default: 10110). Afterwards you need to decide on the allowed sources:
+  * _all_ (default): accept messages from both the access point and the wifi client network - both broadcast messages and directly addressed ones
+  * _ap_: only accept messages from devices that are connected to the access point
+  * _cli_: only accept messages from devices on the Wifi client network
+  * _mp-all_: you need to configure the multicast address(group) you would like to join and will receive multicast from both the access point and the wifi client network
+  * _mp-ap_: multicast only from the access point network
+  * _mp-cli_: multicast only from the wifi client network
+
+UDP Writer
+**********
+The UDP writer basically is the counterpart for the UDP reader.
+You also have to select where do you want the UDP messages to be sent to.
+  * _bc-all_ (default): Send the messages as broadcast to devices connected to the own access point and to devices on the wifi client network
+  * _bc-ap_: send the messages as broadcast only to the access point network
+  * _bc-cli_: send the messages as broadcast to the wifi client network
+  * _normal_: you need to configure a destination address (one device) that you want the messages to be send to
+  * _mc-all_: send messages as multicast to both the access point network and the wifi client network. _Hint_: Only devices that configured the same multicast address will receive such messages.
+  * _mc-ap_: multicast only to the access point network
+  * _mc-cli_: multicast only to the wifi client network.
+
+With the combination of UDP writer and UDP reader you can easily connect multiple gateway devices without a lot of configuration. Just configure one device as UDP writer (with the default settings) and configure other devices as UDP reader (also with default settings) - this way it does not matter how you connect the devices - all devices will receive the data that is sent by the first one.<br>
+__Remark:__ be carefull not to create loops when you would like to send data in both directions between 2 devices using UDP. Either use filters - or use TCP connections as they are able to send data in both directions on one connection (without creating a loop).
+
+If you want to forward NMEA2000 data from one gateway device to another, just enable "Seasmart out" at the sender side. This will encapsulate the NMEA2000 data in a NMEA0183 compatible format. The receiver will always automatically detect this format and handle the data correctly.
diff --git a/lib/exampletask/Readme.md b/lib/exampletask/Readme.md
index 2a7e42e..4c51d92 100644
--- a/lib/exampletask/Readme.md
+++ b/lib/exampletask/Readme.md
@@ -70,7 +70,8 @@ Files
  * add capabilities (since 20231105 - as an alternative to a static DECLARE_CAPABILITY )
  * add a user task (since 20231105 - as an alternative to a static DECLARE_USERTASK)
  * store or read task interface data (see below)
-
+ * add a request handler for web requests (since 202411xx) - see registerRequestHandler in the API
+ 
 
  __Interfacing between Task__
 

From 5adf48322088205a42584780680c3e3584e5c435 Mon Sep 17 00:00:00 2001
From: andreas <andreas@wellenvogel.de>
Date: Thu, 14 Nov 2024 20:01:51 +0100
Subject: [PATCH 58/58] add release doc 20241114

---
 Readme.md | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/Readme.md b/Readme.md
index ebcd08b..08ac3e3 100644
--- a/Readme.md
+++ b/Readme.md
@@ -170,6 +170,28 @@ For details refer to the [example description](lib/exampletask/Readme.md).
 
 Changelog
 ---------
+[20241114](../../releases/tag/20241114)
+**********
+* UDP writer and reader - [#79](../../issues/79)
+* extensions for [user tasks](lib/exampletask/Readme.md)
+  * extend the Web UI with js and css
+  * register handler for Web Requests
+* Naming of the config file [#87](../../issues/87)
+* MTW from PGN130311 [#83](../../issues/83)
+* USB connection on S3 stops [#81](../../issues/81)
+* remove invalid true wind calc, allow to configure some mapping - partly [#75](../../issues/75)
+* correctly parse GSV messages [#50](../../issues/50)
+* minor adaptations from new version [#66](../../issues/66) 
+* new platform version 6.8.1
+* internally restructure the channel handling
+* add docs for [networking](doc/network.md) and [serial/USB](doc/serial-usb.md)
+* allow to configure the timeout(s) for the data display
+* new library versions - nmea2000 4.22.0, nmea0183 1.10.1
+* allow for builds completely without FastLED
+* wipe the nvs partition on factory reset (to handle corrupted config)
+* do not store the wifi settings in nvs on the system level [#78](../../issues/78)
+* rename of data: HDG->HDT, MHDG->HDM
+* adapt crash decoder tool to s3
 [20240428](../../releases/tag/20240428)
 **********
 * fix build error with M5 gps kit