diff --git a/Readme.md b/Readme.md index 12dcade..adc8ec4 100644 --- a/Readme.md +++ b/Readme.md @@ -54,6 +54,19 @@ They are devided into binaries for an initial flash (xxx-all.bin) and binaries f Initial Flash ************* + +__Browser__ + +If you run a system with a modern Chrome or Edge Browser you can directly flash your device from within the browser. +Just go to the [Flash Page](https://wellenvogel.github.io/esp32-nmea2000/install.html) and select the "Initial" flash for your Hardware. This will install the most current software to your device. +If you are on Windows you will need to have the correct driver installed before (see below at [windows users](#windows) - only install the driver, not the flashtool). + +You can also install an update from the flash page but normally it is easier to do this from the Web Gui of the device (see [below](#update)). + +The [Flash Page](https://wellenvogel.github.io/esp32-nmea2000/install.html) will also allow you to open a console window to your ESP32. + +__Tool based__ + To initially flash a deviceyou can use [ESPTool](https://github.com/espressif/esptool). The flash command must be (example for m5stack-atom): @@ -77,13 +90,15 @@ Afterwards run flashtool.pyz with ``` python3 flashtool.pyz ``` + __windows users__
-You can find a prebuild executable in tools: [esptool.exe](tools/esptool.exe). -Just create an empty directory on your machine, download the esptool to this directory and also download the binary (xxx-all.bin) from [releases](../../releases). -Afterwards you need to install the driver for the serial port to connect your ESP32 board. For a modern windows the driver at [FTDI](https://ftdichip.com/drivers/d2xx-drivers/) should be working. +You need to install the driver for the serial port to connect your ESP32 board. For a modern windows the driver at [FTDI](https://ftdichip.com/drivers/d2xx-drivers/) should be working. After installing the driver check with your device manager for the com port that is assigned to your connected esp device. +For the flashtool you can find a prebuild executable in tools: [esptool.exe](tools/esptool.exe). +Just create an empty directory on your machine, download the esptool to this directory and also download the binary (xxx-all.bin) from [releases](../../releases). + Open a command prompt and change into the directory you downloaded the esptool.exe and the firmware binary. Flash with the command ``` @@ -97,7 +112,7 @@ There is no installation needed - just start the downloaded exe. Some Anti Virus Software may (accidently) tag this as infected. In this case you can still install the UI in two steps: * you first need to install python3 from the [download page](https://www.python.org/downloads/windows/) - use the Windows 64 Bit installer. Install using the default settings. * Afterwards download [flashtool.pyz](../../raw/master/tools/flashtool.pyz) and run it with a double click. - + Update ****** @@ -145,6 +160,20 @@ For details refer to the [example description](lib/exampletask/Readme.md). Changelog --------- +[20230317](../../releases/tag/20230317) +********** +* correctly convert bar to Pascal in XDR records + +[20230309](../../releases/tag/20230308) +********** +* use underscores in settings file names [#40](../../issues/40) +* pin platform and lib versions +* add rs232 and rs485 atom boards +* use less memory when saving new config +* correct factor for ROT [#44](../../issues/44) +* better handling of VHW - send STW (128259) even if no heading, additionally send 127250 (magnetic/true) if included in VHW [#49](../../issues/49) +* parse MTW and convert to 130310 [#49](../../issues/49) + [20220403](../../releases/tag/20220403) ********* * add support for PGN 127257 pitch/roll/yaw diff --git a/doc/AtomCan.jpg b/doc/AtomCan.jpg new file mode 100644 index 0000000..8ad86a9 Binary files /dev/null and b/doc/AtomCan.jpg differ diff --git a/doc/Conversions.odt b/doc/Conversions.odt index 97ce811..975520d 100644 Binary files a/doc/Conversions.odt and b/doc/Conversions.odt differ diff --git a/doc/Conversions.pdf b/doc/Conversions.pdf index 15109b6..75b2bd2 100644 Binary files a/doc/Conversions.pdf and b/doc/Conversions.pdf differ diff --git a/doc/Hardware.md b/doc/Hardware.md index 79209a0..988c8d6 100644 --- a/doc/Hardware.md +++ b/doc/Hardware.md @@ -50,6 +50,20 @@ M5 Atom CAN with M5 RS485 Module With this set up you get basically all the features from the plain AtomCAN and the Tal485 combined. You still can connect via USB but have the NMEA0183 connection in parallel. +M5 Atom RS485 with M5 CAN Unit +-------------------------------- +* Hardware: [ATOM RS485](https://docs.m5stack.com/en/atom/atomic485) + [CAN Unit](http://docs.m5stack.com/en/unit/can) +* Prebuild Binary: m5stack-atom-rs485-canunit-all.bin +* Build Define: BOARD_M5ATOM_RS485_CANUNIT +* Power: 12V via RS485 Module or via USB + +M5 Atom RS232 with M5 CAN Unit +-------------------------------- +* Hardware: [ATOM RS232](https://docs.m5stack.com/en/atom/atomic232) + [CAN Unit](http://docs.m5stack.com/en/unit/can) +* Prebuild Binary: m5stack-atom-rs232-canunit-all.bin +* Build Define: BOARD_M5ATOM_RS232_CANUNIT +* Power: 12V via RS232 Module or via USB + M5 Stack Atom Canunit --------------------- * Hardware: [M5_ATOM](http://docs.m5stack.com/en/core/atom_lite) + [CAN Unit](http://docs.m5stack.com/en/unit/can) diff --git a/doc/Praesi-2023-01.odp b/doc/Praesi-2023-01.odp new file mode 100644 index 0000000..adf8480 Binary files /dev/null and b/doc/Praesi-2023-01.odp differ diff --git a/doc/web-config.png b/doc/web-config.png new file mode 100644 index 0000000..7ab01e0 Binary files /dev/null and b/doc/web-config.png differ diff --git a/doc/web-status.png b/doc/web-status.png new file mode 100644 index 0000000..62fce3d Binary files /dev/null and b/doc/web-status.png differ diff --git a/docs/install.html b/docs/install.html new file mode 100644 index 0000000..6e7fa75 --- /dev/null +++ b/docs/install.html @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + +
+

Installing ESP32-NMEA2000

+

On this page you can install the latest release of ESP32-NMEA2000 + directly to your device.

+

Before installing just check carefully which of the devices is the correct one for you. Refer to the documentation.

+
+
+
+
+ \ No newline at end of file diff --git a/lib/boatData/GwBoatData.h b/lib/boatData/GwBoatData.h index 5ddebde..71742cf 100644 --- a/lib/boatData/GwBoatData.h +++ b/lib/boatData/GwBoatData.h @@ -7,6 +7,10 @@ #define GW_BOAT_VALUE_LEN 32 #define GWSC(name) static constexpr const __FlashStringHelper* name=F(#name) +//see https://github.com/wellenvogel/esp32-nmea2000/issues/44 +//factor to convert from N2k/SI rad/s to current NMEA rad/min +#define ROT_WA_FACTOR 60 + class GwJsonDocument; class GwBoatItemBase{ public: diff --git a/lib/buttons/GwButtons.cpp b/lib/buttons/GwButtons.cpp index cf64329..c169e48 100644 --- a/lib/buttons/GwButtons.cpp +++ b/lib/buttons/GwButtons.cpp @@ -14,7 +14,7 @@ class FactoryResetRequest: public GwMessage{ protected: virtual void processImpl(){ api->getLogger()->logDebug(GwLog::LOG,"reset request processing"); - api->getConfig()->reset(true); + api->getConfig()->reset(); xTaskCreate([](void *p){ delay(500); ESP.restart(); diff --git a/lib/config/GWConfig.cpp b/lib/config/GWConfig.cpp index f7316df..cdbfdbe 100644 --- a/lib/config/GWConfig.cpp +++ b/lib/config/GWConfig.cpp @@ -1,6 +1,7 @@ #include "GWConfig.h" #include #include +#include #define B(v) (v?"true":"false") @@ -53,6 +54,7 @@ GwConfigInterface * GwConfigHandler::getConfigItem(const String name, bool dummy #define PREF_NAME "gwprefs" GwConfigHandler::GwConfigHandler(GwLog *logger): GwConfigDefinitions(){ this->logger=logger; + saltBase=esp_random(); } bool GwConfigHandler::loadConfig(){ prefs.begin(PREF_NAME,true); @@ -63,18 +65,6 @@ bool GwConfigHandler::loadConfig(){ prefs.end(); return true; } -bool GwConfigHandler::saveConfig(){ - prefs.begin(PREF_NAME,false); - for (int i=0;ihasChangedValue){ - LOG_DEBUG(GwLog::LOG,"saving %s=%s",configs[i]->getName().c_str(),configs[i]->changedValue.c_str()); - prefs.putString(configs[i]->getName().c_str(),configs[i]->changedValue); - } - } - prefs.end(); - LOG_DEBUG(GwLog::LOG,"saved config"); - return true; -} bool GwConfigHandler::updateValue(String name, String value){ GwConfigInterface *i=getConfigItem(name); @@ -83,18 +73,24 @@ bool GwConfigHandler::updateValue(String name, String value){ LOG_DEBUG(GwLog::LOG,"skip empty password %s",name.c_str()); } else{ + if (i->asString() == value){ + return false; + } LOG_DEBUG(GwLog::LOG,"update config %s=>%s",name.c_str(),i->isSecret()?"***":value.c_str()); - i->updateValue(value); + prefs.begin(PREF_NAME,false); + prefs.putString(i->getName().c_str(),value); + prefs.end(); } return true; } -bool GwConfigHandler::reset(bool save){ +bool GwConfigHandler::reset(){ LOG_DEBUG(GwLog::LOG,"reset config"); + prefs.begin(PREF_NAME,false); for (int i=0;iupdateValue(configs[i]->getDefault()); + prefs.putString(configs[i]->getName().c_str(),configs[i]->getDefault()); } - if (!save) return true; - return saveConfig(); + prefs.end(); + return true; } String GwConfigHandler::getString(const String name, String defaultv) const{ GwConfigInterface *i=getConfigItem(name,false); @@ -122,6 +118,47 @@ bool GwConfigHandler::setValue(String name,String value){ return true; } +bool GwConfigHandler::checkPass(String hash){ + if (! getBool(useAdminPass)) return true; + String pass=getString(adminPassword); + unsigned long now=millis()/1000UL & ~0x7UL; + MD5Builder builder; + char buffer[2*sizeof(now)+1]; + for (int i=0;i< 5 ;i++){ + unsigned long base=saltBase+now; + toHex(base,buffer,2*sizeof(now)+1); + builder.begin(); + builder.add(buffer); + builder.add(pass); + builder.calculate(); + String md5=builder.toString(); + bool rt=hash == md5; + logger->logDebug(GwLog::DEBUG,"checking pass %s, base=%ld, hash=%s, res=%d", + hash.c_str(),base,md5.c_str(),(int)rt); + if (rt) return true; + now -= 8; + } + return false; +} +static char hv(uint8_t nibble){ + nibble=nibble&0xf; + if (nibble < 10) return (char)('0'+nibble); + return (char)('A'+nibble-10); +} +void GwConfigHandler::toHex(unsigned long v, char *buffer, size_t bsize) +{ + uint8_t *bp = (uint8_t *)&v; + size_t i = 0; + for (; i < sizeof(v) && (2 * i + 1) < bsize; i++) + { + buffer[2 * i] = hv((*bp) >> 4); + buffer[2 * i + 1] = hv(*bp); + bp++; + } + if ((2 * i) < bsize) + buffer[2 * i] = 0; +} + void GwNmeaFilter::handleToken(String token, int index){ switch(index){ case 0: diff --git a/lib/config/GWConfig.h b/lib/config/GWConfig.h index 1dcb28c..4d0b105 100644 --- a/lib/config/GWConfig.h +++ b/lib/config/GWConfig.h @@ -18,22 +18,25 @@ class GwConfigHandler: public GwConfigDefinitions{ public: GwConfigHandler(GwLog *logger); bool loadConfig(); - bool saveConfig(); void stopChanges(); bool updateValue(String name, String value); - bool reset(bool save); + bool reset(); String toString() const; String toJson() const; String getString(const String name,const String defaultv="") const; bool getBool(const String name,bool defaultv=false) const ; int getInt(const String name,int defaultv=0) const; GwConfigInterface * getConfigItem(const String name, bool dummy=false) const; + bool checkPass(String hash); /** * change the value of a config item * will become a noop after stopChanges has been called * !use with care! no checks of the value */ bool setValue(String name, String value); + static void toHex(unsigned long v,char *buffer,size_t bsize); + unsigned long getSaltBase(){return saltBase;} private: + unsigned long saltBase=0; }; #endif \ No newline at end of file diff --git a/lib/config/GwConfigItem.h b/lib/config/GwConfigItem.h index 44f8a06..adfb161 100644 --- a/lib/config/GwConfigItem.h +++ b/lib/config/GwConfigItem.h @@ -10,18 +10,6 @@ class GwConfigInterface{ const char * initialValue; String value; bool secret=false; - String changedValue; - bool hasChangedValue=false; - void updateValue(String value) - { - hasChangedValue = false; - if (value != this->value) - { - changedValue = value; - hasChangedValue = true; - } - } - public: GwConfigInterface(const String &name, const char * initialValue, bool secret=false){ this->name=name; diff --git a/lib/hardware/GwHardware.h b/lib/hardware/GwHardware.h index 08a6a8f..8b6dd9f 100644 --- a/lib/hardware/GwHardware.h +++ b/lib/hardware/GwHardware.h @@ -57,6 +57,52 @@ //brightness 0...255 #define GWLED_BRIGHTNESS 64 #endif + +#ifdef BOARD_M5ATOM_RS232_CANUNIT +#define ESP32_CAN_TX_PIN GPIO_NUM_26 +#define ESP32_CAN_RX_PIN GPIO_NUM_32 +//if using rs232 +#define GWSERIAL_TX 19 +#define GWSERIAL_RX 22 +#define GWSERIAL_MODE "BI" +#define GWBUTTON_PIN GPIO_NUM_39 +#define GWBUTTON_ACTIVE LOW +//if GWBUTTON_PULLUPDOWN we enable a pulup/pulldown +#define GWBUTTON_PULLUPDOWN +//led handling +//if we define GWLED_FASTNET the arduino fastnet lib is used +#define GWLED_FASTLED +#define GWLED_TYPE SK6812 +//color schema for fastled +#define GWLED_SCHEMA GRB +#define GWLED_PIN GPIO_NUM_27 +//brightness 0...255 +#define GWLED_BRIGHTNESS 64 +#endif + +#ifdef BOARD_M5ATOM_RS485_CANUNIT +#define ESP32_CAN_TX_PIN GPIO_NUM_26 +#define ESP32_CAN_RX_PIN GPIO_NUM_32 +//if using rs232 +#define GWSERIAL_TX 19 +#define GWSERIAL_RX 22 +#define GWSERIAL_MODE "UNI" +#define GWBUTTON_PIN GPIO_NUM_39 +#define GWBUTTON_ACTIVE LOW +//if GWBUTTON_PULLUPDOWN we enable a pulup/pulldown +#define GWBUTTON_PULLUPDOWN +//led handling +//if we define GWLED_FASTNET the arduino fastnet lib is used +#define GWLED_FASTLED +#define GWLED_TYPE SK6812 +//color schema for fastled +#define GWLED_SCHEMA GRB +#define GWLED_PIN GPIO_NUM_27 +//brightness 0...255 +#define GWLED_BRIGHTNESS 64 +#endif + + #ifdef BOARD_M5STICK_CANUNIT #define ESP32_CAN_TX_PIN GPIO_NUM_32 #define ESP32_CAN_RX_PIN GPIO_NUM_33 diff --git a/lib/log/GWLog.cpp b/lib/log/GWLog.cpp index 80af69b..6601f2a 100644 --- a/lib/log/GWLog.cpp +++ b/lib/log/GWLog.cpp @@ -21,6 +21,7 @@ void GwLog::logString(const char *fmt,...){ va_list args; va_start(args,fmt); xSemaphoreTake(locker, portMAX_DELAY); + recordCounter++; vsnprintf(buffer,bufferSize-1,fmt,args); buffer[bufferSize-1]=0; if (! writer) { @@ -40,6 +41,7 @@ void GwLog::logDebug(int level,const char *fmt,...){ va_list args; va_start(args,fmt); xSemaphoreTake(locker, portMAX_DELAY); + recordCounter++; vsnprintf(buffer,bufferSize-1,fmt,args); buffer[bufferSize-1]=0; if (! writer) { diff --git a/lib/log/GwLog.h b/lib/log/GwLog.h index 7aea6f8..e04c7b5 100644 --- a/lib/log/GwLog.h +++ b/lib/log/GwLog.h @@ -15,6 +15,7 @@ class GwLog{ int logLevel=1; GwLogWriter *writer; SemaphoreHandle_t locker; + long long recordCounter=0; public: static const int LOG=1; static const int ERROR=0; @@ -29,6 +30,7 @@ class GwLog{ int isActive(int level){return level <= logLevel;}; void flush(); void setLevel(int level){this->logLevel=level;} + long long getRecordCounter(){return recordCounter;} }; #define LOG_DEBUG(level,...){ if (logger != NULL && logger->isActive(level)) logger->logDebug(level,__VA_ARGS__);} diff --git a/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp b/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp index 0258f8c..9d66e08 100644 --- a/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp +++ b/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp @@ -526,7 +526,7 @@ private: boatData->VAR->getDataWithDefault(N2kDoubleNA), boatData->DEV->getDataWithDefault(N2kDoubleNA) ); - send(n2kMsg,msg.sourceId); + send(n2kMsg,msg.sourceId,"127250M"); } void convertHDT(const SNMEA0183Msg &msg){ @@ -570,7 +570,7 @@ private: UD(DEV); tN2kMsg n2kMsg; SetN2kMagneticHeading(n2kMsg,1,MHDG,DEV,VAR); - send(n2kMsg,msg.sourceId); + send(n2kMsg,msg.sourceId,"127250M"); } void convertDPT(const SNMEA0183Msg &msg){ @@ -692,10 +692,19 @@ private: LOG_DEBUG(GwLog::DEBUG, "failed to parse VHW %s", msg.line); return; } - if (! updateDouble(boatData->STW,STW,msg.sourceId)) return; - if (! updateDouble(boatData->HDG,TrueHeading,msg.sourceId)) return; - if (MagneticHeading == NMEA0183DoubleNA) MagneticHeading=N2kDoubleNA; tN2kMsg n2kMsg; + if (updateDouble(boatData->HDG,TrueHeading,msg.sourceId)){ + SetN2kTrueHeading(n2kMsg,1,TrueHeading); + send(n2kMsg,msg.sourceId); + } + if(updateDouble(boatData->MHDG,MagneticHeading,msg.sourceId)){ + SetN2kMagneticHeading(n2kMsg,1,MagneticHeading, + boatData->DEV->getDataWithDefault(N2kDoubleNA), + boatData->VAR->getDataWithDefault(N2kDoubleNA) + ); + send(n2kMsg,msg.sourceId,"127250M"); //ensure both mag and true are sent + } + if (! updateDouble(boatData->STW,STW,msg.sourceId)) return; SetN2kBoatSpeed(n2kMsg,1,STW); send(n2kMsg,msg.sourceId); @@ -887,6 +896,7 @@ private: LOG_DEBUG(GwLog::DEBUG,"unable to parse ROT %s",msg.line); return; } + ROT=ROT / ROT_WA_FACTOR; if (! updateDouble(boatData->ROT,ROT,msg.sourceId)) return; tN2kMsg n2kMsg; SetN2kRateOfTurn(n2kMsg,1,ROT); @@ -912,6 +922,23 @@ private: send(n2kMsg,msg.sourceId); } + void convertMTW(const SNMEA0183Msg &msg){ + if (msg.FieldCount() < 2){ + LOG_DEBUG(GwLog::DEBUG,"unable to parse MTW %s",msg.line); + return; + } + if (msg.Field(1)[0] != 'C'){ + LOG_DEBUG(GwLog::DEBUG,"invalid temp unit in MTW %s",msg.line); + return; + } + if (msg.FieldLen(0) < 1) return; + double WTemp=CToKelvin(atof(msg.Field(0))); + UD(WTemp); + tN2kMsg n2kMsg; + SetN2kPGN130310(n2kMsg,1,WTemp); + send(n2kMsg,msg.sourceId); + } + //shortcut for lambda converters #define CVL [](const SNMEA0183Msg &msg, NMEA0183DataToN2KFunctions *p) -> void void registerConverters() @@ -980,7 +1007,10 @@ private: String(F("ROT")), &NMEA0183DataToN2KFunctions::convertROT); converters.registerConverter( 129283UL, - String(F("XTE")), &NMEA0183DataToN2KFunctions::convertXTE); + String(F("XTE")), &NMEA0183DataToN2KFunctions::convertXTE); + converters.registerConverter( + 130310UL, + String(F("MTW")), &NMEA0183DataToN2KFunctions::convertMTW); unsigned long *xdrpgns=new unsigned long[8]{127505UL,127508UL,130312UL,130313UL,130314UL,127489UL,127488UL,127257UL}; converters.registerConverter( 8, diff --git a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp index 3e900fd..1e61d8e 100644 --- a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp +++ b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp @@ -32,7 +32,6 @@ - N2kDataToNMEA0183::N2kDataToNMEA0183(GwLog * logger, GwBoatData *boatData, SendNMEA0183MessageCallback callback, String talkerId) { @@ -1055,7 +1054,7 @@ private: } if (!updateDouble(boatData->ROT,ROT)) return; tNMEA0183Msg nmeamsg; - if (NMEA0183SetROT(nmeamsg,ROT,talkerId)){ + if (NMEA0183SetROT(nmeamsg,ROT * ROT_WA_FACTOR,talkerId)){ SendMessage(nmeamsg); } } diff --git a/lib/webserver/GwWebServer.cpp b/lib/webserver/GwWebServer.cpp index eb4dcef..2b3a79a 100644 --- a/lib/webserver/GwWebServer.cpp +++ b/lib/webserver/GwWebServer.cpp @@ -118,4 +118,12 @@ bool GwWebServer::registerMainHandler(const char *url,RequestCreator creator){ return true; } +bool GwWebServer::registerPostHandler(const char *url, ArRequestHandlerFunction requestHandler, + ArBodyHandlerFunction bodyHandler){ + server->on(url,HTTP_POST,requestHandler, + [](AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final){}, + bodyHandler); + return true; +} + diff --git a/lib/webserver/GwWebServer.h b/lib/webserver/GwWebServer.h index 35b5cba..795988e 100644 --- a/lib/webserver/GwWebServer.h +++ b/lib/webserver/GwWebServer.h @@ -1,6 +1,7 @@ #ifndef _GWWEBSERVER_H #define _GWWEBSERVER_H #include +#include #include "GwMessage.h" #include "GwLog.h" class GwWebServer{ @@ -14,6 +15,7 @@ class GwWebServer{ ~GwWebServer(); void begin(); bool registerMainHandler(const char *url,RequestCreator creator); + bool registerPostHandler(const char *url, ArRequestHandlerFunction requestHandler, ArBodyHandlerFunction bodyHandler); void handleAsyncWebRequest(AsyncWebServerRequest *request, GwRequestMessage *msg); AsyncWebServer * getServer(){return server;} }; diff --git a/lib/xdrmappings/GwXDRMappings.cpp b/lib/xdrmappings/GwXDRMappings.cpp index 04b4f3d..25d403f 100644 --- a/lib/xdrmappings/GwXDRMappings.cpp +++ b/lib/xdrmappings/GwXDRMappings.cpp @@ -41,19 +41,20 @@ double ps2ph(double v) GwXDRType *types[] = { new GwXDRType(GwXDRType::PRESS, "P", "P"), new GwXDRType(GwXDRType::PRESS, "P", "B", + BarToP, PtoBar, - BarToP), + "P"), new GwXDRType(GwXDRType::VOLT, "U", "V"), new GwXDRType(GwXDRType::AMP, "I", "A"), new GwXDRType(GwXDRType::TEMP, "C", "K"), - new GwXDRType(GwXDRType::TEMP, "C", "C", CToKelvin, KelvinToC), + new GwXDRType(GwXDRType::TEMP, "C", "C", CToKelvin, KelvinToC,"K"), new GwXDRType(GwXDRType::HUMID, "H", "P"), //percent new GwXDRType(GwXDRType::VOLPERCENT, "V", "P"), - new GwXDRType(GwXDRType::VOLUME, "V", "M", m3ToL, ltrTom3), + new GwXDRType(GwXDRType::VOLUME, "V", "M", m3ToL, ltrTom3,"L"), new GwXDRType(GwXDRType::FLOW, "R", "I", ps2ph, ph2ps), new GwXDRType(GwXDRType::GENERIC, "G", ""), new GwXDRType(GwXDRType::DISPLACEMENT, "A", "P"), - new GwXDRType(GwXDRType::DISPLACEMENTD, "A", "D",DegToRad,RadToDeg), + new GwXDRType(GwXDRType::DISPLACEMENTD, "A", "D",DegToRad,RadToDeg,"rd"), new GwXDRType(GwXDRType::RPM,"T","R"), //important to have 2x NULL! NULL, diff --git a/lib/xdrmappings/GwXDRMappings.h b/lib/xdrmappings/GwXDRMappings.h index da1db2d..44d1599 100644 --- a/lib/xdrmappings/GwXDRMappings.h +++ b/lib/xdrmappings/GwXDRMappings.h @@ -44,14 +44,24 @@ class GwXDRType{ TypeCode code; String xdrtype; String xdrunit; + String boatDataUnit; convert tonmea=NULL; convert fromnmea=NULL; - GwXDRType(TypeCode tc,String xdrtype,String xdrunit,convert fromnmea=NULL,convert tonmea=NULL){ + GwXDRType(TypeCode tc,String xdrtype,String xdrunit){ + this->code=tc; + this->xdrtype=xdrtype; + this->xdrunit=xdrunit; + this->boatDataUnit=xdrunit; + this->fromnmea=fromnmea; + this->tonmea=tonmea; + } + GwXDRType(TypeCode tc,String xdrtype,String xdrunit,convert fromnmea,convert tonmea,String boatDataUnit=String()){ this->code=tc; this->xdrtype=xdrtype; this->xdrunit=xdrunit; this->fromnmea=fromnmea; this->tonmea=tonmea; + this->boatDataUnit=boatDataUnit.isEmpty()?xdrunit:boatDataUnit; } }; class GwXDRTypeMapping{ @@ -183,7 +193,7 @@ class GwXDRFoundMapping : public GwBoatItemNameProvider{ return String("xdr")+getTransducerName(); }; virtual String getBoatItemFormat(){ - return "formatXdr:"+type->xdrtype+":"+type->xdrunit; + return "formatXdr:"+type->xdrtype+":"+type->boatDataUnit; }; virtual ~GwXDRFoundMapping(){} }; diff --git a/platformio.ini b/platformio.ini index d9d2305..7625c7b 100644 --- a/platformio.ini +++ b/platformio.ini @@ -59,6 +59,25 @@ build_flags = upload_port = /dev/esp32 upload_protocol = esptool +[env:m5stack-atom-rs232-canunit] +board = m5stack-atom +lib_deps = ${env.lib_deps} +build_flags = + -D BOARD_M5ATOM_RS232_CANUNIT + ${env.build_flags} +upload_port = /dev/esp32 +upload_protocol = esptool + +[env:m5stack-atom-rs485-canunit] +board = m5stack-atom +lib_deps = ${env.lib_deps} +build_flags = + -D BOARD_M5ATOM_RS485_CANUNIT + ${env.build_flags} +upload_port = /dev/esp32 +upload_protocol = esptool + + [env:m5stickc-atom-canunit] board = m5stick-c lib_deps = ${env.lib_deps} diff --git a/src/main.cpp b/src/main.cpp index becdce6..557c35a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -141,47 +141,12 @@ GwWebServer webserver(&logger,&mainQueue,80); GwCounter countNMEA2KIn("count2Kin"); GwCounter countNMEA2KOut("count2Kout"); -unsigned long saltBase=esp_random(); - -char hv(uint8_t nibble){ - nibble=nibble&0xf; - if (nibble < 10) return (char)('0'+nibble); - return (char)('A'+nibble-10); -} -void toHex(unsigned long v,char *buffer,size_t bsize){ - uint8_t *bp=(uint8_t *)&v; - size_t i=0; - for (;i> 4); - buffer[2*i+1]=hv(*bp); - bp++; - } - if ((2*i) < bsize) buffer[2*i]=0; -} bool checkPass(String hash){ - if (! config.getBool(config.useAdminPass)) return true; - String pass=config.getString(config.adminPassword); - unsigned long now=millis()/1000UL & ~0x7UL; - MD5Builder builder; - char buffer[2*sizeof(now)+1]; - for (int i=0;i< 5 ;i++){ - unsigned long base=saltBase+now; - toHex(base,buffer,2*sizeof(now)+1); - builder.begin(); - builder.add(buffer); - builder.add(pass); - builder.calculate(); - String md5=builder.toString(); - bool rt=hash == md5; - logger.logDebug(GwLog::DEBUG,"checking pass %s, base=%ld, hash=%s, res=%d", - hash.c_str(),base,md5.c_str(),(int)rt); - if (rt) return true; - now -= 8; - } - return false; + return config.checkPass(hash); } + GwUpdate updater(&logger,&webserver,&checkPass); GwConfigInterface *systemName=config.getConfigItem(config.systemName,true); @@ -398,11 +363,12 @@ protected: status["clientIP"] = WiFi.localIP().toString(); status["apIp"] = gwWifi.apIP(); size_t bsize=2*sizeof(unsigned long)+1; - unsigned long base=saltBase + ( millis()/1000UL & ~0x7UL); + unsigned long base=config.getSaltBase() + ( millis()/1000UL & ~0x7UL); char buffer[bsize]; - toHex(base,buffer,bsize); + GwConfigHandler::toHex(base,buffer,bsize); status["salt"] = buffer; status["fwtype"]= firmwareType; + status["heap"]=(long)xPortGetFreeHeapSize(); //nmea0183Converter->toJson(status); countNMEA2KIn.toJson(status); countNMEA2KOut.toJson(status); @@ -476,71 +442,7 @@ protected: } }; -class SetConfigRequest : public GwRequestMessage -{ -public: - //we rely on the message living not longer then the request - AsyncWebServerRequest *request; - SetConfigRequest(AsyncWebServerRequest *rq) : GwRequestMessage(F("application/json"),F("setConfig")), - request(rq) - {}; - virtual int getTimeout(){return 4000;} -protected: - virtual void processRequest() - { - bool ok = true; - const char * hashArg="_hash"; - String error; - String hash; - if (request->hasArg(hashArg)){ - hash=request->arg(hashArg); - } - if (! checkPass(hash)){ - result=JSON_INVALID_PASS; - return; - } - logger.logDebug(GwLog::DEBUG,"Heap free=%ld, minFree=%ld", - (long)xPortGetFreeHeapSize(), - (long)xPortGetMinimumEverFreeHeapSize() - ); - for (int i = 0; i < request->args(); i++){ - String name=request->argName(i); - String value=request->arg(i); - if (name.indexOf("_")>= 0) continue; - if (name == GwConfigDefinitions::apPassword && fixedApPass) continue; - bool rt = config.updateValue(name, value); - if (!rt) - { - logger.logDebug(GwLog::ERROR,"ERR: unable to update %s to %s", name.c_str(), value.c_str()); - ok = false; - error += name; - error += "="; - error += value; - error += ","; - } - logger.flush(); - } - if (ok) - { - result = JSON_OK; - logger.logDebug(GwLog::ERROR,"update config and restart"); - config.saveConfig(); - logger.flush(); - logger.logDebug(GwLog::DEBUG,"Heap free=%ld, minFree=%ld", - (long)xPortGetFreeHeapSize(), - (long)xPortGetMinimumEverFreeHeapSize() - ); - logger.flush(); - delayedRestart(); - } - else - { - GwJsonDocument rt(100); - rt["status"] = error; - serializeJson(rt, result); - } - } -}; + class ResetConfigRequest : public GwRequestMessage { String hash; @@ -557,7 +459,7 @@ protected: result=JSON_INVALID_PASS; return; } - config.reset(true); + config.reset(); logger.logDebug(GwLog::ERROR,"reset config, restart"); result = JSON_OK; delayedRestart(); @@ -626,6 +528,134 @@ protected: }; +void handleConfigRequestData(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total){ + typedef struct{ + char notFirst; + char hashChecked; + char parsingValue; + int bName; + char name[33]; + int bValue; + char value[512]; + }RequestNV; + long long lastRecords=logger.getRecordCounter(); + logger.logDebug(GwLog::DEBUG,"handleConfigRequestData len=%d,idx=%d,total=%d",(int)len,(int)index,(int)total); + if (request->_tempObject == NULL){ + logger.logDebug(GwLog::DEBUG,"handleConfigRequestData create receive struct"); + //we cannot use new here as it will be deleted with free + request->_tempObject=malloc(sizeof(RequestNV)); + memset(request->_tempObject,0,sizeof(RequestNV)); + } + RequestNV *nv=(RequestNV*)(request->_tempObject); + if (nv->notFirst && ! nv->hashChecked){ + return; //ignore data + } + int parsed=0; + while (parsed < len) + { + if (!nv->parsingValue) + { + int maxSize = sizeof(RequestNV::name) - 1; + if (nv->bName >= maxSize) + { + nv->name[maxSize] = 0; + logger.logDebug(GwLog::DEBUG, "parse error name too long %s", nv->name); + nv->bName = 0; + } + while (nv->bName < maxSize && parsed < len) + { + bool endName = *data == '='; + nv->name[nv->bName] = endName ? 0 : *data; + nv->bName++; + parsed++; + data++; + if (endName) + { + nv->parsingValue = 1; + break; + } + } + } + bool valueDone = false; + if (nv->parsingValue) + { + int maxSize = sizeof(RequestNV::value) - 1; + if (nv->bValue >= maxSize) + { + nv->value[maxSize] = 0; + logger.logDebug(GwLog::DEBUG, "parse error value too long %s:%s", nv->name, nv->value); + nv->bValue = 0; + } + while (nv->bValue < maxSize && parsed < len) + { + valueDone = *data == '&'; + nv->value[nv->bValue] = valueDone ? 0 : *data; + nv->bValue++; + parsed++; + data++; + if (valueDone) break; + } + if (! valueDone){ + if (parsed >= len && (len+index) >= total){ + //request ends here + nv->value[nv->bValue]=0; + valueDone=true; + } + } + if (valueDone){ + String name(nv->name); + String value(nv->value); + if (! nv->notFirst){ + nv->notFirst=1; + //we expect the _hash as first parameter + if (name != String("_hash")){ + logger.logDebug(GwLog::ERROR,"missing first parameter _hash in setConfig"); + request->send(200,"application/json","{\"status\":\"missing _hash\"}"); + return; + } + if (! config.checkPass(request->urlDecode(value))){ + request->send(200,"application/json",JSON_INVALID_PASS); + return; + } + else{ + nv->hashChecked=1; + } + } + else{ + if (nv->hashChecked){ + logger.logDebug(GwLog::DEBUG,"value ns=%d,n=%s,vs=%d,v=%s",nv->bName,nv->name,nv->bValue,nv->value); + if ((logger.getRecordCounter() - lastRecords) > 20){ + logger.flush(); + lastRecords=logger.getRecordCounter(); + } + config.updateValue(request->urlDecode(name),request->urlDecode(value)); + } + } + nv->parsingValue=0; + nv->bName=0; + nv->bValue=0; + } + } + } + if (parsed >= len && (len+index)>= total){ + if (nv->notFirst){ + if (nv->hashChecked){ + request->send(200,"application/json",JSON_OK); + logger.flush(); + logger.logDebug(GwLog::DEBUG,"Heap free=%ld, minFree=%ld", + (long)xPortGetFreeHeapSize(), + (long)xPortGetMinimumEverFreeHeapSize() + ); + logger.flush(); + delayedRestart(); + } + } + else{ + request->send(200,"application/json","{\"status\":\"missing _hash\"}"); + } + } +} + void setup() { mainLock=xSemaphoreCreateMutex(); @@ -666,12 +696,6 @@ void setup() { { return new StatusRequest(); }); webserver.registerMainHandler("/api/config", [](AsyncWebServerRequest *request)->GwRequestMessage * { return new ConfigRequest(); }); - webserver.registerMainHandler("/api/setConfig", - [](AsyncWebServerRequest *request)->GwRequestMessage * - { - SetConfigRequest *msg = new SetConfigRequest(request); - return msg; - }); webserver.registerMainHandler("/api/resetConfig", [](AsyncWebServerRequest *request)->GwRequestMessage * { return new ResetConfigRequest(request->arg("_hash")); }); webserver.registerMainHandler("/api/boatData", [](AsyncWebServerRequest *request)->GwRequestMessage * @@ -690,7 +714,12 @@ void setup() { { String hash=request->arg("hash"); return new CheckPassRequest(hash); - }); + }); + webserver.registerPostHandler("/api/setConfig", + [](AsyncWebServerRequest *request){ + + }, + handleConfigRequestData); webserver.begin(); xdrMappings.begin(); diff --git a/web/index.html b/web/index.html index a607101..2ecf096 100644 --- a/web/index.html +++ b/web/index.html @@ -54,6 +54,10 @@
TCP client error --- +
+
+ Free heap + ---
diff --git a/web/index.js b/web/index.js index 509d7a6..28ea82f 100644 --- a/web/index.js +++ b/web/index.js @@ -236,16 +236,24 @@ function changeConfig() { ensurePass() .then(function (pass) { let newAdminPass; - let url = "/api/setConfig?_hash="+encodeURIComponent(pass)+"&"; + 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]; } - url += name + "=" + encodeURIComponent(allValues[name]) + "&"; + body += encodeURIComponent(name) + "=" + encodeURIComponent(allValues[name]) + "&"; } - getJson(url) + 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) { diff --git a/webinstall/install.css b/webinstall/install.css new file mode 100644 index 0000000..972b865 --- /dev/null +++ b/webinstall/install.css @@ -0,0 +1,24 @@ +.item { + margin: 0.5em; +} +.itemTitle { + margin-top: 0.5em; + margin-bottom: 0.2em; +} +button.installButton, button.showConsole, button.hideConsole { + font-size: 1em; + margin-left: 0.5em; +} +select.consoleBaud { + display: inline-block; + max-width: 10em; + font-size: 1em; +} +.console { + margin-bottom: 1em; +} +body { + font-size: 16px; + font-family: system-ui; + line-height: 1.5em; +} \ No newline at end of file diff --git a/webinstall/install.html b/webinstall/install.html new file mode 100644 index 0000000..e578a82 --- /dev/null +++ b/webinstall/install.html @@ -0,0 +1,17 @@ + + + + + + + + + + + + +
+
+
+
+ \ No newline at end of file diff --git a/webinstall/install.js b/webinstall/install.js new file mode 100644 index 0000000..0281ff4 --- /dev/null +++ b/webinstall/install.js @@ -0,0 +1,151 @@ +import {XtermOutputHandler} from "./installUtil.js"; +import ESPInstaller from "./installUtil.js"; +(function(){ + let espLoaderTerminal; + let espInstaller; + let releaseData={}; + const addEl=ESPInstaller.addEl; //shorter typing + let showConsole; + let hideConsole; + const enableConsole=(enable,disableBoth)=>{ + if (showConsole) showConsole.disabled=!enable || disableBoth; + if (hideConsole) hideConsole.disabled=enable || disableBoth; + } + const showError=(txt)=>{ + let hFrame=document.querySelector('.heading'); + if (hFrame){ + hFrame.textContent=txt; + hFrame.classList.add("error"); + } + else{ + alert(txt); + } + } + const buildHeading=(user,repo,element)=>{ + let hFrame=document.querySelector(element||'.heading'); + if (! hFrame) return; + hFrame.textContent=''; + let h=addEl('h2',undefined,hFrame,`ESP32 Install ${user}:${repo}`) + } + const checkChip=(chipFamily,assetName)=>{ + //for now only ESP32 + if (chipFamily != "ESP32"){ + throw new Error(`unexpected chip family ${chipFamily}, expected ESP32`); + } + return assetName; + } + const baudRates=[1200, + 2400, + 4800, + 9600, + 14400, + 19200, + 28800, + 38400, + 57600, + 115200, + 230400, + 460800]; + const buildConsoleButtons=(element)=>{ + let bFrame=document.querySelector(element||'.console'); + if (! bFrame) return; + bFrame.textContent=''; + let cLine=addEl('div','buttons',bFrame); + let bSelect=addEl('select','consoleBaud',cLine); + baudRates.forEach((baud)=>{ + let v=addEl('option',undefined,bSelect,baud+''); + v.setAttribute('value',baud); + }); + bSelect.value=115200; + showConsole=addEl('button','showConsole',cLine,'ShowConsole'); + showConsole.addEventListener('click',async()=>{ + enableConsole(false); + await espInstaller.startConsole(bSelect.value); + }) + hideConsole=addEl('button','hideConsole',cLine,'HideConsole'); + hideConsole.addEventListener('click',async()=>{ + await espInstaller.stopConsole(); + enableConsole(true); + }) + } + const buildButtons=(user,repo,element)=>{ + let bFrame=document.querySelector(element||'.content'); + if (! bFrame) return; + bFrame.textContent=''; + if (!releaseData.assets) return; + let version=releaseData.name; + if (! version){ + alert("no version found in release data"); + return; + } + addEl('div','version',bFrame,`Version: ${version}`); + let items={}; + releaseData.assets.forEach((asset)=>{ + let name=asset.name; + let base=name.replace(/-all\.bin/,'').replace(/-update\.bin/,''); + if (items[base] === undefined){ + items[base]={}; + } + let item=items[base]; + item.label=base.replace(/-[0-9][0-9]*/,''); + if (name.match(/-update\./)){ + item.update=name; + } + else{ + item.basic=name; + } + }); + for (let k in items){ + let item=items[k]; + let line=addEl('div','item',bFrame); + addEl('div','itemTitle',line,item.label); + let btLine=addEl('div','buttons',line); + let tb=addEl('button','installButton',line,'Initial'); + tb.addEventListener('click',async ()=>{ + enableConsole(false,true); + await espInstaller.installClicked( + true, + user, + repo, + version, + 4096, + (chip)=>checkChip(chip,item.basic) + ) + enableConsole(true); + }); + tb=addEl('button','installButton',line,'Update'); + tb.addEventListener('click',async ()=>{ + enableConsole(false,true); + await espInstaller.installClicked( + false, + user, + repo, + version, + 65536, + (chip)=>checkChip(chip,item.update) + ) + enableConsole(true); + }); + } + + } + window.onload = async () => { + if (! ESPInstaller.checkAvailable()){ + showError("your browser does not support the ESP flashing (no serial)"); + return; + } + let user = window.gitHubUser||ESPInstaller.getParam('user'); + let repo = window.gitHubRepo || ESPInstaller.getParam('repo'); + if (!user || !repo) { + alert("missing parameter user or repo"); + } + try { + espLoaderTerminal = new XtermOutputHandler('terminal'); + espInstaller = new ESPInstaller(espLoaderTerminal); + buildHeading(user, repo); + buildConsoleButtons(); + releaseData = await espInstaller.getReleaseInfo(user, repo); + buildButtons(user, repo); + } catch(error){alert("unable to query release info for user "+user+", repo "+repo+": "+error)}; + } +})(); \ No newline at end of file diff --git a/webinstall/install.php b/webinstall/install.php new file mode 100644 index 0000000..e429bec --- /dev/null +++ b/webinstall/install.php @@ -0,0 +1,155 @@ + array('wellenvogel'), + 'repo'=> array('esp32-nmea2000') + ); + if (!function_exists('getallheaders')) { + function getallheaders() + { + $headers = []; + foreach ($_SERVER as $name => $value) { + if (substr($name, 0, 5) == 'HTTP_') { + $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; + } + } + return $headers; + } + } + function safeName($name){ + return preg_replace('[^0-9_a-zA-Z.-]','',$name); + } + function replaceVars($str,$vars){ + foreach ($vars as $n => &$v){ + $str=str_replace("#".$n."#",$v,$str); + } + return $str; + } + + function fillUserAndRepo($vars=null){ + global $allowed; + if ($vars == null) { + $vars=array(); + } + foreach (array('user','repo') as $n){ + if (! isset($_REQUEST[$n])){ + die("missing parameter $n"); + } + $v=$_REQUEST[$n]; + $av=$allowed[$n]; + if (! in_array($v,$av)){ + die("value $v for $n not allowed"); + } + $vars[$n]=$v; + } + return $vars; + } + function addVars($vars,$names){ + foreach ($names as $n){ + if (! isset($_REQUEST[$n])){ + die("missing parameter $n"); + } + $safe=safeName($_REQUEST[$n]); + $vars[$n]=$safe; + } + return $vars; + } + + function curl_exec_follow(/*resource*/ $ch, /*int*/ &$maxredirect = null) { + $mr = $maxredirect === null ? 5 : intval($maxredirect); + if (ini_get('open_basedir') == '' && ini_get('safe_mode' == 'Off') && false) { + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, $mr > 0); + curl_setopt($ch, CURLOPT_MAXREDIRS, $mr); + } else { + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); + if ($mr > 0) { + $newurl = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL); + $rch = curl_copy_handle($ch); + curl_setopt($rch, CURLOPT_HEADER, true); + curl_setopt($rch, CURLOPT_NOBODY, true); + curl_setopt($rch, CURLOPT_FORBID_REUSE, false); + curl_setopt($rch, CURLOPT_RETURNTRANSFER, true); + do { + curl_setopt($rch, CURLOPT_URL, $newurl); + $header = curl_exec($rch); + if (curl_errno($rch)) { + $code = 0; + } else { + $code = curl_getinfo($rch, CURLINFO_HTTP_CODE); + if ($code == 301 || $code == 302) { + preg_match('/Location:(.*?)\n/', $header, $matches); + $newurl = trim(array_pop($matches)); + } else { + $code = 0; + } + } + } while ($code && --$mr); + curl_close($rch); + if (!$mr) { + if ($maxredirect === null) { + trigger_error('Too many redirects. When following redirects, libcurl hit the maximum amount.', E_USER_WARNING); + } else { + $maxredirect = 0; + } + return false; + } + curl_setopt($ch, CURLOPT_URL, $newurl); + } + } + curl_setopt( + $ch, + CURLOPT_HEADERFUNCTION, + function ($curl, $header) { + header($header); + return strlen($header); + } + ); + curl_setopt( + $ch, + CURLOPT_WRITEFUNCTION, + function ($curl, $body) { + echo $body; + return strlen($body); + } + ); + header('Access-Control-Allow-Origin:*'); + return curl_exec($ch); + } + function proxy($url) + { + $headers=getallheaders(); + $ch = curl_init($url); + curl_setopt_array( + $ch, + [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CONNECTTIMEOUT => 30, + ] + ); + $FWHDR = ['User-Agent']; + $outHeaders = array(); + foreach ($FWHDR as $k) { + if (isset($headers[$k])) { + array_push($outHeaders, "$k: $headers[$k]"); + } + } + curl_setopt($ch, CURLOPT_HTTPHEADER, $outHeaders); + $response = curl_exec_follow($ch); + curl_close($ch); + } + + if (isset($_REQUEST['api'])) { + $vars=fillUserAndRepo(); + proxy(replaceVars($api,$vars)); + exit(0); + } + if (isset($_REQUEST['dlName'])){ + $vars=fillUserAndRepo(); + $vars=addVars($vars,array('dlName','dlVersion')); + proxy(replaceVars($download,$vars)); + exit(0); + } + die("invalid request"); + ?> \ No newline at end of file diff --git a/webinstall/installUtil.js b/webinstall/installUtil.js new file mode 100644 index 0000000..87f91c3 --- /dev/null +++ b/webinstall/installUtil.js @@ -0,0 +1,338 @@ +import {ESPLoader,Transport} from "https://cdn.jsdelivr.net/npm/esptool-js@0.2.1/bundle.js"; +/** + * write all messages to the console + */ +class ConsoleOutputHandler{ + clean() { + } + writeLine(data) { + console.log("ESPInstaller:",data); + } + write(data) { + console.log(data); + } +} + +/** + * write messages to an instance of xterm + * to use this, include in your html + + + * and create a div element +
+ * provide the id of this div to the constructor + */ +class XtermOutputHandler { + constructor(termId) { + let termElement = document.getElementById(termId); + if (termElement) { + this.term = new Terminal({ cols: 120, rows: 40 , convertEol: true }); + this.term.open(termElement); + } + this.clean=this.clean.bind(this); + this.writeLine=this.writeLine.bind(this); + this.write=this.write.bind(this); + } + clean() { + if (!this.term) return; + this.term.clear(); + } + writeLine(data) { + if (!this.term) { + console.log("TERM:", data); + return; + }; + this.term.writeln(data); + } + write(data) { + if (!this.term) { + console.log("TERM:", data); + return; + }; + this.term.write(data) + } +}; +class ESPInstaller{ + constructor(outputHandler){ + this.espLoaderTerminal=outputHandler|| new ConsoleOutputHandler(); + this.transport=undefined; + this.esploader=undefined; + this.chipFamily=undefined; + this.base=import.meta.url.replace(/[^/]*$/,"install.php"); + this.consoleDevice=undefined; + this.consoleReader=undefined; + } + /** + * get an URL query parameter + * @param key + * @returns + */ + static getParam(key){ + let value=RegExp(""+key+"[^&]+").exec(window.location.search); + // Return the unescaped value minus everything starting from the equals sign or an empty string + return decodeURIComponent(!!value ? value.toString().replace(/^[^=]+./,"") : ""); + }; + /** + * add an HTML element + * @param {*} type + * @param {*} clazz + * @param {*} parent + * @param {*} text + * @returns + */ + static addEl(type, clazz, parent, text) { + let el = document.createElement(type); + if (clazz) { + if (!(clazz instanceof Array)) { + clazz = clazz.split(/ */); + } + clazz.forEach(function (ce) { + el.classList.add(ce); + }); + } + if (text) el.textContent = text; + if (parent) parent.appendChild(el); + return el; + } + /** + * call a function for each matching element + * @param {*} selector + * @param {*} cb + */ + static forEachEl(selector,cb){ + let arr=document.querySelectorAll(selector); + for (let i=0;i resolve(reader.result)); + reader.readAsBinaryString(blob); + }); + this.espLoaderTerminal.writeLine(`successfully loaded ${data.length} bytes`); + return data; + } + /** + * handle the click of an install button + * @param {*} isFull + * @param {*} user + * @param {*} repo + * @param {*} version + * @param {*} address + * @param {*} assetName the name of the asset file. + * can be a function - will be called with the chip family + * and must return the asset file name + * @returns + */ + async installClicked(isFull, user, repo, version, address, assetName) { + try { + await this.connect(); + let assetFileName = assetName; + if (typeof (assetName) === 'function') { + assetFileName = assetName(this.getChipFamily()); + } + let imageData = await this.getReleaseAsset(user, repo, version, assetFileName); + if (!imageData || imageData.length == 0) { + throw new Error(`no image data fetched`); + } + let fileList = [ + { data: imageData, address: address } + ]; + let txt = isFull ? "baseImage (all data will be erased)" : "update"; + if (!confirm(`ready to install ${version}\n${txt}`)) { + this.espLoaderTerminal.writeLine("aborted by user..."); + await this.disconnect(); + return; + } + await this.writeFlash(fileList); + await this.disconnect(); + } catch (e) { + this.espLoaderTerminal.writeLine(`Error: ${e}`); + alert(`Error: ${e}`); + } + } + /** + * fetch the release info from the github API + * @param {*} user + * @param {*} repo + * @returns + */ + async getReleaseInfo(user,repo){ + let url=this.base+"?api=1&user="+encodeURIComponent(user)+"&repo="+encodeURIComponent(repo) + let resp=await fetch(url); + if (! resp.ok){ + throw new Error(`unable to query release info from ${url}: ${resp.status}`); + } + return await resp.json(); + } + /** + * get the release info in a parsed form + * @param {*} user + * @param {*} repo + * @returns an object: {version:nnn, assets:[name1,name2,...]} + */ + async getParsedReleaseInfo(user,repo){ + let raw=await this.getReleaseInfo(user,repo); + let rt={ + version:raw.name, + assets:[] + }; + if (! raw.assets) return rt; + raw.assets.forEach((asset)=>{ + rt.assets.push(asset.name); + }) + return rt; + } +}; +export {ConsoleOutputHandler, XtermOutputHandler}; +export default ESPInstaller; \ No newline at end of file