Merge remote-tracking branch 'origin/master'

This commit is contained in:
norbert-walter 2023-08-10 21:28:48 +02:00
commit b8134490a9
32 changed files with 1113 additions and 165 deletions

View File

@ -54,6 +54,19 @@ They are devided into binaries for an initial flash (xxx-all.bin) and binaries f
Initial Flash 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). To initially flash a deviceyou can use [ESPTool](https://github.com/espressif/esptool).
The flash command must be (example for m5stack-atom): The flash command must be (example for m5stack-atom):
@ -77,13 +90,15 @@ Afterwards run flashtool.pyz with
``` ```
python3 flashtool.pyz python3 flashtool.pyz
``` ```
<span id="windows"/>
__windows users__<br> __windows users__<br>
You can find a prebuild executable in tools: [esptool.exe](tools/esptool.exe). 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.
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.
After installing the driver check with your device manager for the com port that is assigned to your connected esp device. 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. Open a command prompt and change into the directory you downloaded the esptool.exe and the firmware binary.
Flash with the command 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: 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. * 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. * Afterwards download [flashtool.pyz](../../raw/master/tools/flashtool.pyz) and run it with a double click.
<span id="update"/>
Update Update
****** ******
@ -145,6 +160,20 @@ For details refer to the [example description](lib/exampletask/Readme.md).
Changelog 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) [20220403](../../releases/tag/20220403)
********* *********
* add support for PGN 127257 pitch/roll/yaw * add support for PGN 127257 pitch/roll/yaw

BIN
doc/AtomCan.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Binary file not shown.

View File

@ -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. 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 M5 Stack Atom Canunit
--------------------- ---------------------
* Hardware: [M5_ATOM](http://docs.m5stack.com/en/core/atom_lite) + [CAN Unit](http://docs.m5stack.com/en/unit/can) * Hardware: [M5_ATOM](http://docs.m5stack.com/en/core/atom_lite) + [CAN Unit](http://docs.m5stack.com/en/unit/can)

BIN
doc/Praesi-2023-01.odp Normal file

Binary file not shown.

BIN
doc/web-config.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
doc/web-status.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

28
docs/install.html Normal file
View File

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<script src="https://cdn.jsdelivr.net/npm/xterm@4.19.0/lib/xterm.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@4.19.0/css/xterm.css">
<script type="module" src="https://wellenvogel.de/software/esp32/install.js"></script>
<link rel="stylesheet" href="https://wellenvogel.de/software/esp32/install.css"/>
<script type="text/javascript">
window.gitHubUser="wellenvogel";
window.gitHubRepo="esp32-nmea2000";
</script>
</head>
<body>
</body>
<div class="head">
<h1>Installing ESP32-NMEA2000</h1>
<p>On this page you can install the latest release of <a href="https://github.com/wellenvogel/esp32-nmea2000">ESP32-NMEA2000</a>
directly to your device.</p>
<p>Before installing just check carefully which of the devices is the correct one for you. Refer to the <a href="https://github.com/wellenvogel/esp32-nmea2000/blob/master/doc/Hardware.md">documentation</a>.</p>
</div>
<div class="console"></div>
<div class="content"></div>
<div id="terminal"></div>
</html>

View File

@ -7,6 +7,10 @@
#define GW_BOAT_VALUE_LEN 32 #define GW_BOAT_VALUE_LEN 32
#define GWSC(name) static constexpr const __FlashStringHelper* name=F(#name) #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 GwJsonDocument;
class GwBoatItemBase{ class GwBoatItemBase{
public: public:

View File

@ -14,7 +14,7 @@ class FactoryResetRequest: public GwMessage{
protected: protected:
virtual void processImpl(){ virtual void processImpl(){
api->getLogger()->logDebug(GwLog::LOG,"reset request processing"); api->getLogger()->logDebug(GwLog::LOG,"reset request processing");
api->getConfig()->reset(true); api->getConfig()->reset();
xTaskCreate([](void *p){ xTaskCreate([](void *p){
delay(500); delay(500);
ESP.restart(); ESP.restart();

View File

@ -1,6 +1,7 @@
#include "GWConfig.h" #include "GWConfig.h"
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <string.h> #include <string.h>
#include <MD5Builder.h>
#define B(v) (v?"true":"false") #define B(v) (v?"true":"false")
@ -53,6 +54,7 @@ GwConfigInterface * GwConfigHandler::getConfigItem(const String name, bool dummy
#define PREF_NAME "gwprefs" #define PREF_NAME "gwprefs"
GwConfigHandler::GwConfigHandler(GwLog *logger): GwConfigDefinitions(){ GwConfigHandler::GwConfigHandler(GwLog *logger): GwConfigDefinitions(){
this->logger=logger; this->logger=logger;
saltBase=esp_random();
} }
bool GwConfigHandler::loadConfig(){ bool GwConfigHandler::loadConfig(){
prefs.begin(PREF_NAME,true); prefs.begin(PREF_NAME,true);
@ -63,18 +65,6 @@ bool GwConfigHandler::loadConfig(){
prefs.end(); prefs.end();
return true; return true;
} }
bool GwConfigHandler::saveConfig(){
prefs.begin(PREF_NAME,false);
for (int i=0;i<getNumConfig();i++){
if (configs[i]->hasChangedValue){
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){ bool GwConfigHandler::updateValue(String name, String value){
GwConfigInterface *i=getConfigItem(name); 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()); LOG_DEBUG(GwLog::LOG,"skip empty password %s",name.c_str());
} }
else{ else{
if (i->asString() == value){
return false;
}
LOG_DEBUG(GwLog::LOG,"update config %s=>%s",name.c_str(),i->isSecret()?"***":value.c_str()); 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; return true;
} }
bool GwConfigHandler::reset(bool save){ bool GwConfigHandler::reset(){
LOG_DEBUG(GwLog::LOG,"reset config"); LOG_DEBUG(GwLog::LOG,"reset config");
prefs.begin(PREF_NAME,false);
for (int i=0;i<getNumConfig();i++){ for (int i=0;i<getNumConfig();i++){
configs[i]->updateValue(configs[i]->getDefault()); prefs.putString(configs[i]->getName().c_str(),configs[i]->getDefault());
} }
if (!save) return true; prefs.end();
return saveConfig(); return true;
} }
String GwConfigHandler::getString(const String name, String defaultv) const{ String GwConfigHandler::getString(const String name, String defaultv) const{
GwConfigInterface *i=getConfigItem(name,false); GwConfigInterface *i=getConfigItem(name,false);
@ -122,6 +118,47 @@ bool GwConfigHandler::setValue(String name,String value){
return true; 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){ void GwNmeaFilter::handleToken(String token, int index){
switch(index){ switch(index){
case 0: case 0:

View File

@ -18,22 +18,25 @@ class GwConfigHandler: public GwConfigDefinitions{
public: public:
GwConfigHandler(GwLog *logger); GwConfigHandler(GwLog *logger);
bool loadConfig(); bool loadConfig();
bool saveConfig();
void stopChanges(); void stopChanges();
bool updateValue(String name, String value); bool updateValue(String name, String value);
bool reset(bool save); bool reset();
String toString() const; String toString() const;
String toJson() const; String toJson() const;
String getString(const String name,const String defaultv="") const; String getString(const String name,const String defaultv="") const;
bool getBool(const String name,bool defaultv=false) const ; bool getBool(const String name,bool defaultv=false) const ;
int getInt(const String name,int defaultv=0) const; int getInt(const String name,int defaultv=0) const;
GwConfigInterface * getConfigItem(const String name, bool dummy=false) const; GwConfigInterface * getConfigItem(const String name, bool dummy=false) const;
bool checkPass(String hash);
/** /**
* change the value of a config item * change the value of a config item
* will become a noop after stopChanges has been called * will become a noop after stopChanges has been called
* !use with care! no checks of the value * !use with care! no checks of the value
*/ */
bool setValue(String name, String value); bool setValue(String name, String value);
static void toHex(unsigned long v,char *buffer,size_t bsize);
unsigned long getSaltBase(){return saltBase;}
private: private:
unsigned long saltBase=0;
}; };
#endif #endif

View File

@ -10,18 +10,6 @@ class GwConfigInterface{
const char * initialValue; const char * initialValue;
String value; String value;
bool secret=false; bool secret=false;
String changedValue;
bool hasChangedValue=false;
void updateValue(String value)
{
hasChangedValue = false;
if (value != this->value)
{
changedValue = value;
hasChangedValue = true;
}
}
public: public:
GwConfigInterface(const String &name, const char * initialValue, bool secret=false){ GwConfigInterface(const String &name, const char * initialValue, bool secret=false){
this->name=name; this->name=name;

View File

@ -57,6 +57,52 @@
//brightness 0...255 //brightness 0...255
#define GWLED_BRIGHTNESS 64 #define GWLED_BRIGHTNESS 64
#endif #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 #ifdef BOARD_M5STICK_CANUNIT
#define ESP32_CAN_TX_PIN GPIO_NUM_32 #define ESP32_CAN_TX_PIN GPIO_NUM_32
#define ESP32_CAN_RX_PIN GPIO_NUM_33 #define ESP32_CAN_RX_PIN GPIO_NUM_33

View File

@ -21,6 +21,7 @@ void GwLog::logString(const char *fmt,...){
va_list args; va_list args;
va_start(args,fmt); va_start(args,fmt);
xSemaphoreTake(locker, portMAX_DELAY); xSemaphoreTake(locker, portMAX_DELAY);
recordCounter++;
vsnprintf(buffer,bufferSize-1,fmt,args); vsnprintf(buffer,bufferSize-1,fmt,args);
buffer[bufferSize-1]=0; buffer[bufferSize-1]=0;
if (! writer) { if (! writer) {
@ -40,6 +41,7 @@ void GwLog::logDebug(int level,const char *fmt,...){
va_list args; va_list args;
va_start(args,fmt); va_start(args,fmt);
xSemaphoreTake(locker, portMAX_DELAY); xSemaphoreTake(locker, portMAX_DELAY);
recordCounter++;
vsnprintf(buffer,bufferSize-1,fmt,args); vsnprintf(buffer,bufferSize-1,fmt,args);
buffer[bufferSize-1]=0; buffer[bufferSize-1]=0;
if (! writer) { if (! writer) {

View File

@ -15,6 +15,7 @@ class GwLog{
int logLevel=1; int logLevel=1;
GwLogWriter *writer; GwLogWriter *writer;
SemaphoreHandle_t locker; SemaphoreHandle_t locker;
long long recordCounter=0;
public: public:
static const int LOG=1; static const int LOG=1;
static const int ERROR=0; static const int ERROR=0;
@ -29,6 +30,7 @@ class GwLog{
int isActive(int level){return level <= logLevel;}; int isActive(int level){return level <= logLevel;};
void flush(); void flush();
void setLevel(int level){this->logLevel=level;} 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__);} #define LOG_DEBUG(level,...){ if (logger != NULL && logger->isActive(level)) logger->logDebug(level,__VA_ARGS__);}

View File

@ -526,7 +526,7 @@ private:
boatData->VAR->getDataWithDefault(N2kDoubleNA), boatData->VAR->getDataWithDefault(N2kDoubleNA),
boatData->DEV->getDataWithDefault(N2kDoubleNA) boatData->DEV->getDataWithDefault(N2kDoubleNA)
); );
send(n2kMsg,msg.sourceId); send(n2kMsg,msg.sourceId,"127250M");
} }
void convertHDT(const SNMEA0183Msg &msg){ void convertHDT(const SNMEA0183Msg &msg){
@ -570,7 +570,7 @@ private:
UD(DEV); UD(DEV);
tN2kMsg n2kMsg; tN2kMsg n2kMsg;
SetN2kMagneticHeading(n2kMsg,1,MHDG,DEV,VAR); SetN2kMagneticHeading(n2kMsg,1,MHDG,DEV,VAR);
send(n2kMsg,msg.sourceId); send(n2kMsg,msg.sourceId,"127250M");
} }
void convertDPT(const SNMEA0183Msg &msg){ void convertDPT(const SNMEA0183Msg &msg){
@ -692,10 +692,19 @@ private:
LOG_DEBUG(GwLog::DEBUG, "failed to parse VHW %s", msg.line); LOG_DEBUG(GwLog::DEBUG, "failed to parse VHW %s", msg.line);
return; return;
} }
if (! updateDouble(boatData->STW,STW,msg.sourceId)) return;
if (! updateDouble(boatData->HDG,TrueHeading,msg.sourceId)) return;
if (MagneticHeading == NMEA0183DoubleNA) MagneticHeading=N2kDoubleNA;
tN2kMsg n2kMsg; 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); SetN2kBoatSpeed(n2kMsg,1,STW);
send(n2kMsg,msg.sourceId); send(n2kMsg,msg.sourceId);
@ -887,6 +896,7 @@ private:
LOG_DEBUG(GwLog::DEBUG,"unable to parse ROT %s",msg.line); LOG_DEBUG(GwLog::DEBUG,"unable to parse ROT %s",msg.line);
return; return;
} }
ROT=ROT / ROT_WA_FACTOR;
if (! updateDouble(boatData->ROT,ROT,msg.sourceId)) return; if (! updateDouble(boatData->ROT,ROT,msg.sourceId)) return;
tN2kMsg n2kMsg; tN2kMsg n2kMsg;
SetN2kRateOfTurn(n2kMsg,1,ROT); SetN2kRateOfTurn(n2kMsg,1,ROT);
@ -912,6 +922,23 @@ private:
send(n2kMsg,msg.sourceId); 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 //shortcut for lambda converters
#define CVL [](const SNMEA0183Msg &msg, NMEA0183DataToN2KFunctions *p) -> void #define CVL [](const SNMEA0183Msg &msg, NMEA0183DataToN2KFunctions *p) -> void
void registerConverters() void registerConverters()
@ -981,6 +1008,9 @@ private:
converters.registerConverter( converters.registerConverter(
129283UL, 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}; unsigned long *xdrpgns=new unsigned long[8]{127505UL,127508UL,130312UL,130313UL,130314UL,127489UL,127488UL,127257UL};
converters.registerConverter( converters.registerConverter(
8, 8,

View File

@ -32,7 +32,6 @@
N2kDataToNMEA0183::N2kDataToNMEA0183(GwLog * logger, GwBoatData *boatData, N2kDataToNMEA0183::N2kDataToNMEA0183(GwLog * logger, GwBoatData *boatData,
SendNMEA0183MessageCallback callback, String talkerId) SendNMEA0183MessageCallback callback, String talkerId)
{ {
@ -1055,7 +1054,7 @@ private:
} }
if (!updateDouble(boatData->ROT,ROT)) return; if (!updateDouble(boatData->ROT,ROT)) return;
tNMEA0183Msg nmeamsg; tNMEA0183Msg nmeamsg;
if (NMEA0183SetROT(nmeamsg,ROT,talkerId)){ if (NMEA0183SetROT(nmeamsg,ROT * ROT_WA_FACTOR,talkerId)){
SendMessage(nmeamsg); SendMessage(nmeamsg);
} }
} }

View File

@ -118,4 +118,12 @@ bool GwWebServer::registerMainHandler(const char *url,RequestCreator creator){
return true; 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;
}

View File

@ -1,6 +1,7 @@
#ifndef _GWWEBSERVER_H #ifndef _GWWEBSERVER_H
#define _GWWEBSERVER_H #define _GWWEBSERVER_H
#include <ESPAsyncWebServer.h> #include <ESPAsyncWebServer.h>
#include <functional>
#include "GwMessage.h" #include "GwMessage.h"
#include "GwLog.h" #include "GwLog.h"
class GwWebServer{ class GwWebServer{
@ -14,6 +15,7 @@ class GwWebServer{
~GwWebServer(); ~GwWebServer();
void begin(); void begin();
bool registerMainHandler(const char *url,RequestCreator creator); bool registerMainHandler(const char *url,RequestCreator creator);
bool registerPostHandler(const char *url, ArRequestHandlerFunction requestHandler, ArBodyHandlerFunction bodyHandler);
void handleAsyncWebRequest(AsyncWebServerRequest *request, GwRequestMessage *msg); void handleAsyncWebRequest(AsyncWebServerRequest *request, GwRequestMessage *msg);
AsyncWebServer * getServer(){return server;} AsyncWebServer * getServer(){return server;}
}; };

View File

@ -41,19 +41,20 @@ double ps2ph(double v)
GwXDRType *types[] = { GwXDRType *types[] = {
new GwXDRType(GwXDRType::PRESS, "P", "P"), new GwXDRType(GwXDRType::PRESS, "P", "P"),
new GwXDRType(GwXDRType::PRESS, "P", "B", new GwXDRType(GwXDRType::PRESS, "P", "B",
BarToP,
PtoBar, PtoBar,
BarToP), "P"),
new GwXDRType(GwXDRType::VOLT, "U", "V"), new GwXDRType(GwXDRType::VOLT, "U", "V"),
new GwXDRType(GwXDRType::AMP, "I", "A"), new GwXDRType(GwXDRType::AMP, "I", "A"),
new GwXDRType(GwXDRType::TEMP, "C", "K"), 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::HUMID, "H", "P"), //percent
new GwXDRType(GwXDRType::VOLPERCENT, "V", "P"), 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::FLOW, "R", "I", ps2ph, ph2ps),
new GwXDRType(GwXDRType::GENERIC, "G", ""), new GwXDRType(GwXDRType::GENERIC, "G", ""),
new GwXDRType(GwXDRType::DISPLACEMENT, "A", "P"), 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"), new GwXDRType(GwXDRType::RPM,"T","R"),
//important to have 2x NULL! //important to have 2x NULL!
NULL, NULL,

View File

@ -44,14 +44,24 @@ class GwXDRType{
TypeCode code; TypeCode code;
String xdrtype; String xdrtype;
String xdrunit; String xdrunit;
String boatDataUnit;
convert tonmea=NULL; convert tonmea=NULL;
convert fromnmea=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->code=tc;
this->xdrtype=xdrtype; this->xdrtype=xdrtype;
this->xdrunit=xdrunit; this->xdrunit=xdrunit;
this->fromnmea=fromnmea; this->fromnmea=fromnmea;
this->tonmea=tonmea; this->tonmea=tonmea;
this->boatDataUnit=boatDataUnit.isEmpty()?xdrunit:boatDataUnit;
} }
}; };
class GwXDRTypeMapping{ class GwXDRTypeMapping{
@ -183,7 +193,7 @@ class GwXDRFoundMapping : public GwBoatItemNameProvider{
return String("xdr")+getTransducerName(); return String("xdr")+getTransducerName();
}; };
virtual String getBoatItemFormat(){ virtual String getBoatItemFormat(){
return "formatXdr:"+type->xdrtype+":"+type->xdrunit; return "formatXdr:"+type->xdrtype+":"+type->boatDataUnit;
}; };
virtual ~GwXDRFoundMapping(){} virtual ~GwXDRFoundMapping(){}
}; };

View File

@ -59,6 +59,25 @@ build_flags =
upload_port = /dev/esp32 upload_port = /dev/esp32
upload_protocol = esptool 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] [env:m5stickc-atom-canunit]
board = m5stick-c board = m5stick-c
lib_deps = ${env.lib_deps} lib_deps = ${env.lib_deps}

View File

@ -141,47 +141,12 @@ GwWebServer webserver(&logger,&mainQueue,80);
GwCounter<unsigned long> countNMEA2KIn("count2Kin"); GwCounter<unsigned long> countNMEA2KIn("count2Kin");
GwCounter<unsigned long> countNMEA2KOut("count2Kout"); GwCounter<unsigned long> 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<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;
}
bool checkPass(String hash){ bool checkPass(String hash){
if (! config.getBool(config.useAdminPass)) return true; return config.checkPass(hash);
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;
} }
GwUpdate updater(&logger,&webserver,&checkPass); GwUpdate updater(&logger,&webserver,&checkPass);
GwConfigInterface *systemName=config.getConfigItem(config.systemName,true); GwConfigInterface *systemName=config.getConfigItem(config.systemName,true);
@ -398,11 +363,12 @@ protected:
status["clientIP"] = WiFi.localIP().toString(); status["clientIP"] = WiFi.localIP().toString();
status["apIp"] = gwWifi.apIP(); status["apIp"] = gwWifi.apIP();
size_t bsize=2*sizeof(unsigned long)+1; 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]; char buffer[bsize];
toHex(base,buffer,bsize); GwConfigHandler::toHex(base,buffer,bsize);
status["salt"] = buffer; status["salt"] = buffer;
status["fwtype"]= firmwareType; status["fwtype"]= firmwareType;
status["heap"]=(long)xPortGetFreeHeapSize();
//nmea0183Converter->toJson(status); //nmea0183Converter->toJson(status);
countNMEA2KIn.toJson(status); countNMEA2KIn.toJson(status);
countNMEA2KOut.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 class ResetConfigRequest : public GwRequestMessage
{ {
String hash; String hash;
@ -557,7 +459,7 @@ protected:
result=JSON_INVALID_PASS; result=JSON_INVALID_PASS;
return; return;
} }
config.reset(true); config.reset();
logger.logDebug(GwLog::ERROR,"reset config, restart"); logger.logDebug(GwLog::ERROR,"reset config, restart");
result = JSON_OK; result = JSON_OK;
delayedRestart(); 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() { void setup() {
mainLock=xSemaphoreCreateMutex(); mainLock=xSemaphoreCreateMutex();
@ -666,12 +696,6 @@ void setup() {
{ return new StatusRequest(); }); { return new StatusRequest(); });
webserver.registerMainHandler("/api/config", [](AsyncWebServerRequest *request)->GwRequestMessage * webserver.registerMainHandler("/api/config", [](AsyncWebServerRequest *request)->GwRequestMessage *
{ return new ConfigRequest(); }); { return new ConfigRequest(); });
webserver.registerMainHandler("/api/setConfig",
[](AsyncWebServerRequest *request)->GwRequestMessage *
{
SetConfigRequest *msg = new SetConfigRequest(request);
return msg;
});
webserver.registerMainHandler("/api/resetConfig", [](AsyncWebServerRequest *request)->GwRequestMessage * webserver.registerMainHandler("/api/resetConfig", [](AsyncWebServerRequest *request)->GwRequestMessage *
{ return new ResetConfigRequest(request->arg("_hash")); }); { return new ResetConfigRequest(request->arg("_hash")); });
webserver.registerMainHandler("/api/boatData", [](AsyncWebServerRequest *request)->GwRequestMessage * webserver.registerMainHandler("/api/boatData", [](AsyncWebServerRequest *request)->GwRequestMessage *
@ -691,6 +715,11 @@ void setup() {
String hash=request->arg("hash"); String hash=request->arg("hash");
return new CheckPassRequest(hash); return new CheckPassRequest(hash);
}); });
webserver.registerPostHandler("/api/setConfig",
[](AsyncWebServerRequest *request){
},
handleConfigRequestData);
webserver.begin(); webserver.begin();
xdrMappings.begin(); xdrMappings.begin();

View File

@ -55,6 +55,10 @@
<span class="label">TCP client error</span> <span class="label">TCP client error</span>
<span class="value" id="clientErr">---</span> <span class="value" id="clientErr">---</span>
</div> </div>
<div class="row">
<span class="label">Free heap</span>
<span class="value" id="heap">---</span>
</div>
</div> </div>
<button id="reset">Reset</button> <button id="reset">Reset</button>
</div> </div>

View File

@ -236,16 +236,24 @@ function changeConfig() {
ensurePass() ensurePass()
.then(function (pass) { .then(function (pass) {
let newAdminPass; let newAdminPass;
let url = "/api/setConfig?_hash="+encodeURIComponent(pass)+"&"; let url = "/api/setConfig"
let body="_hash="+encodeURIComponent(pass)+"&";
let allValues=getAllConfigs(); let allValues=getAllConfigs();
if (!allValues) return; if (!allValues) return;
for (let name in allValues){ for (let name in allValues){
if (name == 'adminPassword'){ if (name == 'adminPassword'){
newAdminPass=allValues[name]; 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) { .then(function (status) {
if (status.status == 'OK') { if (status.status == 'OK') {
if (newAdminPass !== undefined) { if (newAdminPass !== undefined) {

24
webinstall/install.css Normal file
View File

@ -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;
}

17
webinstall/install.html Normal file
View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/xterm@4.19.0/lib/xterm.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@4.19.0/css/xterm.css">
<script type="module" src="install.js"></script>
<link rel="stylesheet" href="install.css"/>
</head>
<body>
</body>
<div class="heading"></div>
<div class="console"></div>
<div class="content"></div>
<div id="terminal"></div>
</html>

151
webinstall/install.js Normal file
View File

@ -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)};
}
})();

155
webinstall/install.php Normal file
View File

@ -0,0 +1,155 @@
<?php
$api="https://api.github.com/repos/#user#/#repo#/releases/latest";
$download="https://github.com/#user#/#repo#/releases/download/#dlVersion#/#dlName#";
$manifest="?dlName=#mName#&dlVersion=#mVersion#&user=#user#&repo=#repo#";
$allowed=array(
'user'=> 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");
?>

338
webinstall/installUtil.js Normal file
View File

@ -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
<script src="https://cdn.jsdelivr.net/npm/xterm@4.19.0/lib/xterm.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@4.19.0/css/xterm.css">
* and create a div element
<div id="terminal"/>
* 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<arr.length;i++){
cb(arr[i]);
}
}
static checkAvailable(){
if (! navigator.serial || ! navigator.serial.requestPort) return false;
return true;
}
/**
* execute a reset on the connected device
*/
async resetTransport() {
if (!this.transport) {
throw new Error("not connected");
}
this.espLoaderTerminal.writeLine("Resetting...");
await this.transport.device.setSignals({
dataTerminalReady: false,
requestToSend: true,
});
await this.transport.device.setSignals({
dataTerminalReady: false,
requestToSend: false,
});
};
async disconnect(){
if (this.consoleDevice){
try{
if (this.consoleReader){
await this.consoleReader.cancel();
this.consoleReader=undefined;
}
await this.consoleDevice.close();
}catch(e){
console.log(`error cancel serial read ${e}`);
}
this.consoleDevice=undefined;
}
if (this.transport){
try{
await this.transport.disconnect();
await this.transport.waitForUnlock(1500);
}catch (e){}
this.transport=undefined;
}
this.esploader=undefined;
}
async connect() {
this.espLoaderTerminal.clean();
await this.disconnect();
let device = await navigator.serial.requestPort({});
if (!device) {
return;
}
try {
this.transport = new Transport(device);
this.esploader = new ESPLoader(this.transport, 115200, this.espLoaderTerminal);
let foundChip = await this.esploader.main_fn();
if (!foundChip) {
throw new Error("unable to read chip id");
}
this.espLoaderTerminal.writeLine(`chip: ${foundChip}`);
await this.esploader.flash_id();
this.chipFamily = this.esploader.chip.CHIP_NAME;
this.espLoaderTerminal.writeLine(`chipFamily: ${this.chipFamily}`);
} catch (e) {
this.disconnect();
throw e;
}
}
async startConsole(baud) {
await this.disconnect();
try {
let device = await navigator.serial.requestPort({});
if (!device) {
return;
}
this.consoleDevice=device;
let br=baud || 115200;
await device.open({
baudRate: br
});
this.consoleReader=device.readable.getReader();
this.espLoaderTerminal.clean();
this.espLoaderTerminal.writeLine(`Console at ${br}:`);
while (this.consoleReader) {
let {value:val,done:done} = await this.consoleReader.read();
if (typeof val !== 'undefined') {
this.espLoaderTerminal.write(val);
}
if (done){
console.log("Console reader stopped");
break;
}
}
} catch (e) { this.espLoaderTerminal.writeLine(`Error: ${e}`) }
this.espLoaderTerminal.writeLine("Console reader stopped");
}
async stopConsole(){
await this.disconnect();
}
isConnected(){
return this.transport !== undefined;
}
checkConnected(){
if (! this.isConnected){
throw new Error("not connected");
}
}
getChipFamily(){
this.checkConnected();
return this.chipFamily;
}
/**
* flass the device
* @param {*} fileList : an array of entries {data:blob,address:number}
*/
async writeFlash(fileList){
this.checkConnected();
this.espLoaderTerminal.writeLine(`Flashing....`);
await this.esploader.write_flash(
fileList,
"keep",
"keep",
"keep",
false
)
await this.resetTransport();
this.espLoaderTerminal.writeLine(`Done.`);
}
/**
* fetch a release asset from github
* @param {*} user
* @param {*} repo
* @param {*} version
* @param {*} name
* @returns
*/
async getReleaseAsset(user,repo,version,name){
const url=this.base+"?dlName="+encodeURIComponent(name)+
"&dlVersion="+encodeURIComponent(version)+
"&user="+encodeURIComponent(user)+
"&repo="+encodeURIComponent(repo);
this.espLoaderTerminal.writeLine(`downloading image from ${url}`);
const resp=await fetch(url);
if (! resp.ok){
throw new Error(`unable to download image from ${url}: ${resp.status}`);
}
const reader=new FileReader();
const blob= await resp.blob();
let data=await new Promise((resolve)=>{
reader.addEventListener("load",() => 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;