From 3c664b1480c341cf0bf2d00df92674e1848a6480 Mon Sep 17 00:00:00 2001 From: andreas Date: Sun, 6 Oct 2024 20:06:15 +0200 Subject: [PATCH 01/14] #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 + +//list of configs for the PGN 130306 wind references +static std::map 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 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 Date: Mon, 7 Oct 2024 19:57:19 +0200 Subject: [PATCH 02/14] 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 Date: Fri, 11 Oct 2024 10:54:00 +0200 Subject: [PATCH 03/14] 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 Date: Fri, 11 Oct 2024 12:00:39 +0200 Subject: [PATCH 04/14] 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 Date: Fri, 11 Oct 2024 13:00:41 +0200 Subject: [PATCH 05/14] 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 Date: Fri, 11 Oct 2024 15:51:05 +0200 Subject: [PATCH 06/14] 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 Date: Fri, 11 Oct 2024 15:52:28 +0200 Subject: [PATCH 07/14] 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 Date: Fri, 11 Oct 2024 16:14:22 +0200 Subject: [PATCH 08/14] 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 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 Date: Fri, 11 Oct 2024 19:23:19 +0200 Subject: [PATCH 09/14] 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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 diff --git a/doc/Conversions.pdf b/doc/Conversions.pdf index 75b2bd240efac709d17fd418b357f06432366c93..3de736be458ce876b404e60dad7c020d55feb8b1 100644 GIT binary patch delta 32931 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
Hel5hIlJe!v;3`C1+P*&J%Mrqm=ZCaUK zm?W2HajR6w7kGP{;}|xiPy&cRJ(}1{O@o9na;3ncjB5CkbN3r6=c}F-S5JVzMmrSvDCRFRq*8(3M?n+3ANnJsygJ|`axE6E=Wq&2O3t5>T zegKoi@A?^!ak$=?NYo&6pBE*GPSk*L$gR+?l%O555bA+C7nsk7LWTM z$L~u}Clp=j(s^1h9c#Hdj38&4QVi3b98kM$#2PvXA3E&bl4GwI+{ZORIdi3dGH~GC z^Dns9&d(#z^F3tnPxyiTnyOaT4$7H^p$-yXU?0l#+dHEC^*7B@{-9jKwf-y4{)K*< zg$@Y|IO5eDQHS3{eFZ=)CVnh(lGZ<|J1x4pI)?0MQ_S>UE4X+cB42f%BX5!#u^x0n zmp;mzhAo$T!3I6Rn#h+dtT|>B zlkITc!Lq3+FASp$VyVfZz_N!x_yN=gagtBvkVpfWnsH!dVkC`CvePr8?I#R6PBhKo z*(?pI=|=xk-Z@~ner~jk29cM4UhM()_R_dE)KV>Af|%b2* zLoIX>(-)w0t>wq*%6_G~Jm9zSbO1pKtexS{EKHz#h@bSn9zbAI*PPwoJm1OnsBq7# z-e#>$%sYJZE@W<|+o{h}fABNaaPYITj@`ahviAAqCmlQ3uQ!vvQ$mWLU8T1Uw?MlF zI|(q0a%`CSP_3xgd>$BN6pPo%+_!ecaSyEBq*DOrZjXnOQR=!=&JEccj(0bbSuTt} zekz@GLw7 zvG)MSTqWyn$AfCok>u^SkS86onOW)A_Bc-X6}>XT2)rAv`D^<~xL63&Ljj zdp^!NefBwo%eN#egzvGf3;7r1JrDHy9zU{zWhBTQ8R5|$+hHAXpSR+VUqG9(jj&t5 zubyOFFWyIcv+Z2nsc?!qqdGTmJsoiQzZ3yg{SE82+%2BxSDWpF_*+@$CEK7y>w=xU zs|Spib{9K7H{S92m+uVco>|xS*t0JDO%zQKO&3rF`fJQeT1#{E zM{9hXR^w8-3{+v7h8@yQl&ybZ&Ra*vCMA|@h5Yb2`TVNVDe$<9)38wfUEy8lDmfAD zE9D3Db0R*ZqF@a>R4nfkbPb$HaSd@z6yc8u1tf)KMo=zstT~Gm5>c!_1Z57hJjo9} z$v{RTc$1>G2L2p*p-D#O9u8u~v4Oz%)A&e=Bq~Gv1_Vs@X%_Ofif^#C$py0b^dLN7 zR)+ZYpzwd)&{Yq%1;J%zVCH0IXJBCGU}quVU}2==;NWCuV`gXPWM$%rkBZ=k#|~5d zA8-x+kGO2Z)&5&uoblKZ|2!`Xj(^Y?m>AiZ|4CzEWdC3mS)4vI`vT(5fQ<$Ck-<@V-{Hy6d{r|s#Ft9VTvHX+9!TB$N{I&lVH8!Sy zi-duLlY{l2!kp}k|1`qP#__)+`7ePmaI!Nq{zI0Dm6h?I`u@@Y%>Rs-iItuCpGKJ2 zm|6Z4uK!|$iH(EdpU5z?{2l)={FnA`4*u2le~w3|v$6i$lrXcg{pxOp4o+?e4ko5h%zx*T6^fKpR9+0~e*q*N6)OM$ delta 32701 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 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+='
'+ - cv.l+''+ - '
'; - }) - 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 = "

Converted entities

"; - text += "

NMEA0183 to NMEA2000:
"; - text += " " + (json.nmea0183 || "").replace(/,/g, ", "); - text += "

"; - text += "

NMEA2000 to NMEA0183:
"; - text += " " + (json.nmea2000 || "").replace(/,/g, ", "); - text += "

"; - 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 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 += '
' + + cv.l + '' + + '
'; + }) + 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 = "

Converted entities

"; + text += "

NMEA0183 to NMEA2000:
"; + text += " " + (json.nmea0183 || "").replace(/,/g, ", "); + text += "

"; + text += "

NMEA2000 to NMEA0183:
"; + text += " " + (json.nmea2000 || "").replace(/,/g, ", "); + text += "

"; + 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 Date: Fri, 11 Oct 2024 20:13:45 +0200 Subject: [PATCH 10/14] 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 = "

Converted entities

"; text += "

NMEA0183 to NMEA2000:
"; @@ -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 Date: Sun, 13 Oct 2024 16:09:17 +0200 Subject: [PATCH 11/14] 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 *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 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 *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 Date: Sun, 13 Oct 2024 16:10:03 +0200 Subject: [PATCH 12/14] 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 @@

Update
Help
-
-
-
- VERSION - --- - -
+
+
+
+
+ VERSION + --- + +
-
- Access Point IP - --- +
+ Access Point IP + --- +
+
+ wifi client connected + --- [---] +
+
+ wifi client IP + --- +
+
+ # clients + --- +
+
+ TCP client connected + --- +
+
+ TCP client error + --- +
+
+ Free heap + --- +
+
+ NMEA2000 State + [---]  + UNKNOWN +
-
- wifi client connected - --- [---] -
-
- wifi client IP - --- -
-
- # clients - --- -
-
- TCP client connected - --- -
-
- TCP client error - --- -
-
- Free heap - --- -
-
- NMEA2000 State - [---]  - UNKNOWN -
+
- -
-