Merge branch 'wellenvogel:master' into master

This commit is contained in:
norbert-walter 2021-12-21 11:58:37 +01:00 committed by GitHub
commit 55862efe37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 5105 additions and 132 deletions

View File

@ -59,5 +59,5 @@ jobs:
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ steps.version.outputs.version}}
file: ./.pio/build/*/*-all.bin
file: ./.pio/build/*/*-{all,update}.bin
file_glob: true

View File

@ -8,11 +8,13 @@ Based on the work of
and a couple of other open source projects.
Many thanks for all the great work.
This project is part of [OpenBoatProjects](https://open-boat-projects.org/de/nmea2000-gateway-mit-m5stack-atom/).
Goal
----
Have a simple ready-to-go ESP32 binary that can be flashed onto a [M5 Atom CAN](https://docs.m5stack.com/en/atom/atom_can), potentially extended by an [Atom Tail485](https://shop.m5stack.com/collections/atom-series/products/atom-tail485?variant=32169041559642) for NMEA0183 connection and power supply.
But will also run on other ESP32 boards.
But will also run on other ESP32 boards see [Hardware](doc/Hardware.md).
What is included
----------------------------------
@ -38,15 +40,59 @@ The software is prepared to run on different kinds of ESP32 based modules and ac
For the list of hardware set ups refer to [Hardware](doc/Hardware.md).
Pre Build Binaries
------------------
In the [release section](https://github.com/wellenvogel/esp32-nmea2000/releases) you can find a couple of pre-build binaries that can easily be flashed on your ESP32 board using [ESPTool](https://github.com/espressif/esptool).
Installation
------------
In the [release section](../../releases) you can find a couple of pre-build binaries.<br>
They are devided into binaries for an initial flash (xxx-all.bin) and binaries for updating an existing device (xxx-update.bin).
Initial Flash
*************
To initially flash a deviceyou can use [ESPTool](https://github.com/espressif/esptool).
The flash command must be (example for m5stack-atom):
```
esptool.py --port XXXX --chip esp32 write_flash 0x1000 m5stack-atom-all.bin
esptool.py --port XXXX --chip esp32 write_flash 0x1000 m5stack-atom-20211217-all.bin
```
For the meaning of the board names have a look at [Hardware](doc/Hardware.md). For details refer to the code in [platformio.ini](platformio.ini) and look for the hardware definitions in [GwHardware.h](lib/hardware/GwHardware.h).
Additionally there is a small GUI for the esptool included here at [tools/flashtool.py](tools/flashtool.py)
__linux users__<br>
You can typically install the esptool (once you have python 3 installed) with
```
sudo pip install esptool
```
To use the flashtool just copy flashtool.py and esptool.py from [tools](tools) to an empty directory.
```
sudo pip install tkinter
sudo pip install pyserial
```
Afterwards run flashtool.py (potentially making it executable before).
__windows users__<br>
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.
After installing the driver check with your device manager for the com port that is assigned to your connected esp device.
Open a command prompt and change into the directory you downloaded the esptool.exe and the firmware binary.
Flash with the command
```
esptool.exe --port COM3 0x1000 xxxxx-xxxx-all.bin
```
Replace COM3 with the port shown in the device manager and the xxx with the name of the downloaded binary.
If you do not want to use the command line you can download the precompiled [flashtool.exe](../../raw/master/tools/flashtool.exe).
Just start the downloaded exe. Unfortunately some virus scanners seem to consider the exe a virus or trojan. There is not much I can do against this - the exe is simply build from flashtool.py - see [tools readme](tools/readme-esptool-win.txt).
Update
******
To update a device you can use the Web-UI (Update tab). In principle you could also update a device using the initial flash command (and an xxx-all.bin) firmware but this would erase all your configuration.
So for normal operation just download a xxx-update.bin from the [release](../../releases) page and use the UI to install it.
![install](doc/ota-ok1.png).
When you choose a file for the update the UI will check if it is a valid firmware file and will reject invalid ones.
To really execute the update click the "Upload" button. You will have a progress indicator and get a notification about the update result.
Please reload the page in your browser after the "connected" state is green as the new version could have changes thatv otherwise will not work.
Starting
---------
@ -59,6 +105,13 @@ To store your changes you will be asked for an admin password. The initial one i
Be careful to notice the password - you can only recover from a lost password with a factory reset of the device (long press the led button until it goes blue->red->green).
On the data page you will have a small dashboard for the currently received data.
On the status page you can check the number of messages flowing in and out.
To help you recover lost passwords the Wifi access point passowrd and the admin password will be output at the USB port when the device starts up. So by connecting a terminal program you can retrieve those passwords.
Security Hints
--------------
You should only connect the Wifi of the device to trusted networks. There is only some very limited protection against network sniffing of denial of service attacks. Never connect the device directly to the internet without a firewall in between (like e.g. your Wifi or LTE router). Especially be careful when connecting to open port networks.
When making changes you will be asked for the admin password - and this one is always send somehow encrypted. But when you change the Wifi access point password or the Wifi client password it will be sent in clear text.
When you enable the "remember me" for the admin password it will be stored in clear text in your browser (use ForgetPassword to remove it from there).
Conversion from and to NMEA0183 XDR
-----------------------------------
@ -74,6 +127,22 @@ Extending the Software
To give room for adding own software and still being able to keep in sync with this master part there is a concept of user tasks that will allow you to add your own hardware definitions and to add code that should be executed without the need to change parts of the existing software.
For details refer to the [example description](lib/exampletask/Readme.md).
Changelog
---------
[20211218](../../releases/tag/20211218)
********
* 1st real release
* use the initial flash if you had the pre-release installed
* most of the N2K <-> 0183 conversions working, see [Conversions](doc/Conversions.pdf)
* display of received data
* xdr record mapping (see [XdrMappings](doc/XdrMappings.md))
* OTA update included in the UI
* description updated
* extension API
[20211113](../../releases/tag/20211113)
********
* Pre-release
* basic functions are working

View File

@ -32,7 +32,7 @@ M5 Atom CAN with M5 Tail485
* Build Define: BOARD_M5ATOM
* Power: 12V via Tail485
![M5 AtomCAn with Tail485](tail485-front.jpg)
![M5 AtomCAN with Tail485](tail485-front.jpg)
With this set up you can use the device as a gateway between NMEA2000 and NMEA0183. The NMEA0183 connection can only work as either a sender or receiver. Additionally you can connect other devices via Wifi.
This way you get a simple 12V powered NMEA2000-NMEA0183 Wifi gateway.
@ -52,17 +52,29 @@ With this set up you get basically all the features from the plain AtomCAN and t
M5 Stack Atom Canunit
---------------------
* Hardware:
* Hardware: [M5_ATOM](http://docs.m5stack.com/en/core/atom_lite) + [CAN Unit](http://docs.m5stack.com/en/unit/can)
* Prebuild Binary: m5stack-atom-canunit-all.bin
* Build Define: BOARD_M5ATOM_CANUNIT
* Power:
* Power: Via USB
![M5_Atom_with CAN unit](atom_can.jpg)
Can be used e.g. as an NMEA2000 Adapter for a laptop running e.g. OpenCPN with the NMEA2000 Data converted to NMEA0183.
![OpenCPN on Laptop via USB and MFD on Android via WiFi](in_action1.jpg)
![OpenCPN on Laptop via USB and AvNav on Android via WiFi](in_action2.jpg)
M5 Stick C Canunit
------------------
* Hardware:
* Hardware: [M5_StickC+](http://docs.m5stack.com/en/core/m5stickc_plus) + [CAN Unit](http://docs.m5stack.com/en/unit/can)
* Prebuild Binary: m5stickc-atom-canunit-all.bin
* Build Define: BOARD_M5STICK_CANUNIT
* Power:
* Power: Via USB
* LCD: not yet implemented
![M5StickC+ with CAN unit](stickc_can.jpg)
Can be used e.g. as an NMEA2000 Adapter for a laptop running e.g. OpenCPN with the NMEA2000 Data converted to NMEA0183.
Node MCU32s - [Homberger Board](https://github.com/AK-Homberger/NMEA2000WifiGateway-with-ESP32)
--------------------------------------

BIN
doc/atom_can.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

BIN
doc/in_action1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

BIN
doc/in_action2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

BIN
doc/ota-ok1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
doc/ota-ok2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
doc/stickc_can.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View File

@ -86,10 +86,11 @@ def writeFileIfChanged(fileName,data):
old=ih.read()
ih.close()
if old == data:
return
return False
print("#generating %s"%fileName)
with open(fileName,"w") as oh:
oh.write(data)
return True
def mergeConfig(base,other):
for bdir in other:
@ -242,6 +243,10 @@ def prebuild(env):
if not checkDir():
sys.exit(1)
userTaskDirs=getUserTaskDirs()
mergedConfig=os.path.join(outPath(),os.path.basename(CFG_FILE))
generateMergedConfig(os.path.join(basePath(),CFG_FILE),mergedConfig,userTaskDirs)
compressFile(mergedConfig,mergedConfig+".gz")
generateCfg(mergedConfig,os.path.join(outPath(),CFG_INCLUDE))
embedded=getEmbeddedFiles(env)
filedefs=[]
for ef in embedded:
@ -261,10 +266,6 @@ def prebuild(env):
print("#WARNING: infile %s for %s not found"%(inFile,ef))
generateEmbedded(filedefs,os.path.join(outPath(),EMBEDDED_INCLUDE))
genereateUserTasks(os.path.join(outPath(), TASK_INCLUDE))
mergedConfig=os.path.join(outPath(),os.path.basename(CFG_FILE))
generateMergedConfig(os.path.join(basePath(),CFG_FILE),mergedConfig,userTaskDirs)
compressFile(mergedConfig,mergedConfig+".gz")
generateCfg(mergedConfig,os.path.join(outPath(),CFG_INCLUDE))
generateFile(os.path.join(basePath(),XDR_FILE),os.path.join(outPath(),XDR_INCLUDE),generateXdrMappings)
version="dev"+datetime.now().strftime("%Y%m%d")
env.Append(CPPDEFINES=[('GWDEVVERSION',version)])

View File

@ -15,6 +15,7 @@ class GwApi{
public:
double value=0;
bool valid=false;
bool changed=false; //will be set by getBoatDataValues
BoatValue(){}
BoatValue(const String &n):name(n){
}

18
lib/appinfo/GwAppInfo.h Normal file
View File

@ -0,0 +1,18 @@
#pragma once
#define GWSTR(x) #x
#define GWSTRINGIFY(x) GWSTR(x)
#ifdef GWRELEASEVERSION
#define VERSION GWSTRINGIFY(GWRELEASEVERSION)
#define LOGLEVEL GwLog::ERROR
#else
#ifdef GWDEVVERSION
#define VERSION GWSTRINGIFY(GWDEVVERSION)
#define LOGLEVEL GwLog::DEBUG
#endif
#ifndef VERSION
#define VERSION "0.9.9"
#define LOGLEVEL GwLog::DEBUG
#endif
#endif
#define FIRMWARE_TYPE GWSTRINGIFY(PIO_ENV_BUILD)

View File

@ -193,7 +193,7 @@ class GwBoatData{
GWBOATDATA(double,Altitude,4000,formatFixed0)
GWBOATDATA(double,WaterDepth,4000,formatDepth)
GWBOATDATA(double,DepthTransducer,4000,formatDepth)
GWBOATDATA(double,SecondsSinceMidnight,4000,formatTime)
GWBOATDATA(double,GpsTime,4000,formatTime)
GWBOATDATA(double,WaterTemperature,4000,kelvinToC)
GWBOATDATA(double,XTE,4000,formatXte)
GWBOATDATA(double,DTW,4000,mtr2nm)
@ -202,7 +202,7 @@ class GwBoatData{
GWBOATDATA(double,WPLongitude,4000,formatLongitude)
GWBOATDATA(uint32_t,Log,16000,mtr2nm)
GWBOATDATA(uint32_t,TripLog,16000,mtr2nm)
GWBOATDATA(uint32_t,DaysSince1970,4000,formatDate)
GWBOATDATA(uint32_t,GpsDate,4000,formatDate)
GWBOATDATA(int16_t,Timezone,8000,formatFixed0)
GWSPECBOATDATA(GwBoatDataSatList,SatInfo,GwSatInfoList::lifeTime,formatFixed0);
public:

View File

@ -39,7 +39,7 @@ String GwConfigHandler::toJson() const{
}
}
serializeJson(jdoc,rt);
logger->logString("configJson: %s",rt.c_str());
LOG_DEBUG(GwLog::DEBUG,"configJson: %s",rt.c_str());
return rt;
}
@ -71,11 +71,11 @@ bool GwConfigHandler::saveConfig(){
if (it != changedValues.end()){
val=it->second;
}
logger->logString("saving %s=%s",configs[i]->getName().c_str(),val.c_str());
LOG_DEBUG(GwLog::LOG,"saving %s=%s",configs[i]->getName().c_str(),val.c_str());
prefs.putString(configs[i]->getName().c_str(),val);
}
prefs.end();
logger->logString("saved config");
LOG_DEBUG(GwLog::LOG,"saved config");
return true;
}
@ -92,7 +92,7 @@ bool GwConfigHandler::updateValue(String name, String value){
return true;
}
bool GwConfigHandler::reset(bool save){
logger->logString("reset config");
LOG_DEBUG(GwLog::LOG,"reset config");
for (int i=0;i<getNumConfig();i++){
changedValues[configs[i]->getName()]=configs[i]->getDefault();
}

View File

@ -88,8 +88,6 @@ void exampleTask(GwApi *api){
GwApi::BoatValue *latitude=new GwApi::BoatValue(F("Latitude"));
GwApi::BoatValue *testValue=new GwApi::BoatValue(boatItemName);
GwApi::BoatValue *valueList[]={longitude,latitude,testValue};
double lastTestValue=0;
bool lastTestValueValid=false;
while(true){
delay(1000);
/*
@ -155,16 +153,13 @@ void exampleTask(GwApi *api){
}
}
if (testValue->valid){
if (! lastTestValueValid || lastTestValue != testValue->value){
if (testValue->changed){
LOG_DEBUG(GwLog::LOG,"%s new value %s",testValue->getName().c_str(),formatValue(testValue).c_str());
lastTestValueValid=true;
lastTestValue=testValue->value;
}
}
else{
if (lastTestValueValid){
if (testValue->changed){
LOG_DEBUG(GwLog::LOG,"%s now invalid",testValue->getName().c_str());
lastTestValueValid=false;
}
}

View File

@ -365,23 +365,23 @@ private:
}
void convertRMC(const SNMEA0183Msg &msg)
{
double SecondsSinceMidnight=0, Latitude=0, Longitude=0, COG=0, SOG=0, Variation=0;
unsigned long DaysSince1970=0;
double GpsTime=0, Latitude=0, Longitude=0, COG=0, SOG=0, Variation=0;
unsigned long GpsDate=0;
time_t DateTime;
char status;
if (!NMEA0183ParseRMC_nc(msg, SecondsSinceMidnight, status, Latitude, Longitude, COG, SOG, DaysSince1970, Variation, &DateTime))
if (!NMEA0183ParseRMC_nc(msg, GpsTime, status, Latitude, Longitude, COG, SOG, GpsDate, Variation, &DateTime))
{
LOG_DEBUG(GwLog::DEBUG, "failed to parse RMC %s", msg.line);
return;
}
tN2kMsg n2kMsg;
if (
UD(SecondsSinceMidnight) &&
UI(DaysSince1970)
UD(GpsTime) &&
UI(GpsDate)
)
{
SetN2kSystemTime(n2kMsg, 1, DaysSince1970, SecondsSinceMidnight);
SetN2kSystemTime(n2kMsg, 1, GpsDate, GpsTime);
send(n2kMsg);
}
if (UD(Latitude) &&
@ -395,7 +395,7 @@ private:
}
if (UD(Variation)){
SetN2kMagneticVariation(n2kMsg,1,N2kmagvar_Calc,
getUint32(boatData->DaysSince1970), Variation);
getUint32(boatData->GpsDate), Variation);
send(n2kMsg);
}
@ -739,9 +739,9 @@ private:
uint32_t DaysSince1970=tNMEA0183Msg::elapsedDaysSince1970(DateTime);
tmElements_t parts;
tNMEA0183Msg::breakTime(DateTime,parts);
double SecondsSinceMidnight=parts.tm_sec+60*parts.tm_min+3600*parts.tm_hour;
if (! boatData->DaysSince1970->update(DaysSince1970,msg.sourceId)) return;
if (! boatData->SecondsSinceMidnight->update(SecondsSinceMidnight,msg.sourceId)) return;
double GpsTime=parts.tm_sec+60*parts.tm_min+3600*parts.tm_hour;
if (! boatData->GpsDate->update(DaysSince1970,msg.sourceId)) return;
if (! boatData->GpsTime->update(GpsTime,msg.sourceId)) return;
bool timezoneValid=false;
if (msg.FieldLen(4) > 0 && msg.FieldLen(5)>0){
Timezone=Timezone/60; //N2K has offset in minutes
@ -750,10 +750,10 @@ private:
}
tN2kMsg n2kMsg;
if (timezoneValid){
SetN2kLocalOffset(n2kMsg,DaysSince1970,SecondsSinceMidnight,Timezone);
SetN2kLocalOffset(n2kMsg,DaysSince1970,GpsTime,Timezone);
send(n2kMsg);
}
SetN2kSystemTime(n2kMsg,1,DaysSince1970,SecondsSinceMidnight);
SetN2kSystemTime(n2kMsg,1,DaysSince1970,GpsTime);
send(n2kMsg);
}
void convertGGA(const SNMEA0183Msg &msg){
@ -773,16 +773,16 @@ private:
LOG_DEBUG(GwLog::DEBUG, "failed to parse GGA %s", msg.line);
return;
}
if (! updateDouble(boatData->SecondsSinceMidnight,GPSTime,msg.sourceId)) return;
if (! updateDouble(boatData->GpsTime,GPSTime,msg.sourceId)) return;
if (! updateDouble(boatData->Latitude,Latitude,msg.sourceId)) return;
if (! updateDouble(boatData->Longitude,Longitude,msg.sourceId)) return;
if (! updateDouble(boatData->Altitude,Altitude,msg.sourceId)) return;
if (! updateDouble(boatData->HDOP,HDOP,msg.sourceId)) return;
if (! boatData->DaysSince1970->isValid()) return;
if (! boatData->GpsDate->isValid()) return;
tN2kMsg n2kMsg;
tN2kGNSSmethod method=N2kGNSSm_noGNSS;
if (GPSQualityIndicator <=5 ) method= (tN2kGNSSmethod)GPSQualityIndicator;
SetN2kGNSS(n2kMsg,1, boatData->DaysSince1970->getData(),
SetN2kGNSS(n2kMsg,1, boatData->GpsDate->getData(),
GPSTime, Latitude, Longitude, Altitude,
N2kGNSSt_GPS, method,
SatelliteCount, HDOP, boatData->PDOP->getDataWithDefault(N2kDoubleNA), 0,
@ -882,7 +882,7 @@ private:
if (GLL.status != 'A') return;
if (! updateDouble(boatData->Latitude,GLL.latitude,msg.sourceId)) return;
if (! updateDouble(boatData->Longitude,GLL.longitude,msg.sourceId)) return;
if (! updateDouble(boatData->SecondsSinceMidnight,GLL.GPSTime,msg.sourceId)) return;
if (! updateDouble(boatData->GpsTime,GLL.GPSTime,msg.sourceId)) return;
tN2kMsg n2kMsg;
SetN2kLatLonRapid(n2kMsg,GLL.latitude,GLL.longitude);
send(n2kMsg);

View File

@ -142,7 +142,7 @@ private:
virtual unsigned long *handledPgns()
{
logger->logString("CONV: # %d handled PGNS", converters.numConverters());
LOG_DEBUG(GwLog::LOG,"CONV: # %d handled PGNS", converters.numConverters());
return converters.handledPgns();
}
virtual String handledKeys(){
@ -242,7 +242,7 @@ private:
ParseN2kMagneticVariation(N2kMsg, SID, Source, DaysSince1970, Variation);
updateDouble(boatData->Variation, Variation);
if (DaysSince1970 != N2kUInt16NA && DaysSince1970 != 0)
boatData->DaysSince1970->update(DaysSince1970,sourceId);
boatData->GpsDate->update(DaysSince1970,sourceId);
}
//*****************************************************************************
@ -377,23 +377,23 @@ private:
double Longitude;
double Altitude;
uint16_t DaysSince1970;
double SecondsSinceMidnight;
if (ParseN2kGNSS(N2kMsg, SID, DaysSince1970, SecondsSinceMidnight, Latitude, Longitude, Altitude, GNSStype, GNSSmethod,
double GpsTime;
if (ParseN2kGNSS(N2kMsg, SID, DaysSince1970, GpsTime, Latitude, Longitude, Altitude, GNSStype, GNSSmethod,
nSatellites, HDOP, PDOP, GeoidalSeparation,
nReferenceStations, ReferenceStationType, ReferenceSationID, AgeOfCorrection))
{
updateDouble(boatData->Latitude, Latitude);
updateDouble(boatData->Longitude, Longitude);
updateDouble(boatData->Altitude, Altitude);
updateDouble(boatData->SecondsSinceMidnight, SecondsSinceMidnight);
updateDouble(boatData->GpsTime, GpsTime);
updateDouble(boatData->HDOP,HDOP);
updateDouble(boatData->PDOP,PDOP);
if (DaysSince1970 != N2kUInt16NA && DaysSince1970 != 0)
boatData->DaysSince1970->update(DaysSince1970,sourceId);
boatData->GpsDate->update(DaysSince1970,sourceId);
int quality=0;
if ((int)GNSSmethod <= 5) quality=(int)GNSSmethod;
tNMEA0183AISMsg nmeaMsg;
if (NMEA0183SetGGA(nmeaMsg,SecondsSinceMidnight,Latitude,Longitude,
if (NMEA0183SetGGA(nmeaMsg,GpsTime,Latitude,Longitude,
quality,nSatellites,HDOP,Altitude,GeoidalSeparation,AgeOfCorrection,
ReferenceSationID,talkerId)){
SendMessage(nmeaMsg);
@ -555,12 +555,12 @@ private:
tNMEA0183Msg NMEA0183Msg;
if (NMEA0183SetRMC(NMEA0183Msg,
boatData->SecondsSinceMidnight->getDataWithDefault(NMEA0183DoubleNA),
boatData->GpsTime->getDataWithDefault(NMEA0183DoubleNA),
boatData->Latitude->getDataWithDefault(NMEA0183DoubleNA),
boatData->Longitude->getDataWithDefault(NMEA0183DoubleNA),
boatData->COG->getDataWithDefault(NMEA0183DoubleNA),
boatData->SOG->getDataWithDefault(NMEA0183DoubleNA),
boatData->DaysSince1970->getDataWithDefault(NMEA0183UInt32NA),
boatData->GpsDate->getDataWithDefault(NMEA0183UInt32NA),
boatData->Variation->getDataWithDefault(NMEA0183DoubleNA),
talkerId))
{
@ -574,16 +574,16 @@ private:
void HandleLog(const tN2kMsg &N2kMsg)
{
uint16_t DaysSince1970;
double SecondsSinceMidnight;
double GpsTime;
uint32_t Log, TripLog;
if (ParseN2kDistanceLog(N2kMsg, DaysSince1970, SecondsSinceMidnight, Log, TripLog))
if (ParseN2kDistanceLog(N2kMsg, DaysSince1970, GpsTime, Log, TripLog))
{
if (Log != N2kUInt32NA)
boatData->Log->update(Log,sourceId);
if (TripLog != N2kUInt32NA)
boatData->TripLog->update(TripLog,sourceId);
if (DaysSince1970 != N2kUInt16NA && DaysSince1970 != 0)
boatData->DaysSince1970->update(DaysSince1970,sourceId);
boatData->GpsDate->update(DaysSince1970,sourceId);
tNMEA0183Msg NMEA0183Msg;
if (!NMEA0183Msg.Init("VLW", talkerId))
@ -996,27 +996,27 @@ private:
void HandleSystemTime(const tN2kMsg &msg){
unsigned char sid=-1;
uint16_t DaysSince1970=N2kUInt16NA;
double SecondsSinceMidnight=N2kDoubleNA;
double GpsTime=N2kDoubleNA;
tN2kTimeSource TimeSource;
if (! ParseN2kSystemTime(msg,sid,DaysSince1970,SecondsSinceMidnight,TimeSource)){
if (! ParseN2kSystemTime(msg,sid,DaysSince1970,GpsTime,TimeSource)){
LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN);
return;
}
updateDouble(boatData->SecondsSinceMidnight,SecondsSinceMidnight);
if (DaysSince1970 != N2kUInt16NA) boatData->DaysSince1970->update(DaysSince1970,sourceId);
if (boatData->DaysSince1970->isValid() && boatData->SecondsSinceMidnight->isValid()){
updateDouble(boatData->GpsTime,GpsTime);
if (DaysSince1970 != N2kUInt16NA) boatData->GpsDate->update(DaysSince1970,sourceId);
if (boatData->GpsDate->isValid() && boatData->GpsTime->isValid()){
tNMEA0183Msg nmeaMsg;
nmeaMsg.Init("ZDA",talkerId);
char utc[7];
double seconds=boatData->SecondsSinceMidnight->getData();
double seconds=boatData->GpsTime->getData();
int hours=floor(seconds/3600.0);
int minutes=floor(seconds/60) - hours *60;
int sec=floor(seconds)-60*minutes-3600*hours;
snprintf(utc,7,"%02d%02d%02d",hours,minutes,sec);
nmeaMsg.AddStrField(utc);
tmElements_t timeParts;
tNMEA0183Msg::breakTime(tNMEA0183Msg::daysToTime_t(boatData->DaysSince1970->getData()),timeParts);
tNMEA0183Msg::breakTime(tNMEA0183Msg::daysToTime_t(boatData->GpsDate->getData()),timeParts);
nmeaMsg.AddUInt32Field(tNMEA0183Msg::GetDay(timeParts));
nmeaMsg.AddUInt32Field(tNMEA0183Msg::GetMonth(timeParts));
nmeaMsg.AddUInt32Field(tNMEA0183Msg::GetYear(timeParts));
@ -1036,14 +1036,14 @@ private:
void HandleTimeOffset(const tN2kMsg &msg){
uint16_t DaysSince1970 =N2kUInt16NA;
double SecondsSinceMidnight=N2kDoubleNA;
double GpsTime=N2kDoubleNA;
int16_t LocalOffset=N2kInt16NA;
if (!ParseN2kLocalOffset(msg,DaysSince1970,SecondsSinceMidnight,LocalOffset)){
if (!ParseN2kLocalOffset(msg,DaysSince1970,GpsTime,LocalOffset)){
LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN);
return;
}
updateDouble(boatData->SecondsSinceMidnight,SecondsSinceMidnight);
if (DaysSince1970 != N2kUInt16NA) boatData->DaysSince1970->update(DaysSince1970,sourceId);
updateDouble(boatData->GpsTime,GpsTime);
if (DaysSince1970 != N2kUInt16NA) boatData->GpsDate->update(DaysSince1970,sourceId);
if (LocalOffset != N2kInt16NA) boatData->Timezone->update(LocalOffset,sourceId);
}
void HandleROT(const tN2kMsg &msg){

View File

@ -163,7 +163,7 @@ void GwSocketServer::begin(){
}
server=new WiFiServer(config->getInt(config->serverPort),maxClients+1);
server->begin();
logger->logString("Socket server created, port=%d",
LOG_DEBUG(GwLog::LOG,"Socket server created, port=%d",
config->getInt(config->serverPort));
MDNS.addService("_nmea-0183","_tcp",config->getInt(config->serverPort));
@ -175,7 +175,7 @@ void GwSocketServer::loop(bool handleRead,bool handleWrite)
if (client)
{
logger->logString("new client connected from %s",
LOG_DEBUG(GwLog::LOG,"new client connected from %s",
client.remoteIP().toString().c_str());
fcntl(client.fd(), F_SETFL, O_NONBLOCK);
bool canHandle = false;
@ -184,7 +184,7 @@ void GwSocketServer::loop(bool handleRead,bool handleWrite)
if (!clients[i]->hasClient())
{
clients[i]->setClient(wiFiClientPtr(new WiFiClient(client)));
logger->logString("set client as number %d", i);
LOG_DEBUG(GwLog::LOG,"set client as number %d", i);
canHandle = true;
break;
}
@ -219,7 +219,7 @@ void GwSocketServer::loop(bool handleRead,bool handleWrite)
if (!client->client->connected())
{
logger->logString("client %d disconnect %s", i, client->remoteIp.c_str());
LOG_DEBUG(GwLog::LOG,"client %d disconnect %s", i, client->remoteIp.c_str());
client->client->stop();
client->setClient(NULL);
}

View File

@ -11,7 +11,7 @@ GwWifi::GwWifi(const GwConfigHandler *config,GwLog *log, bool fixedApPass){
this->fixedApPass=fixedApPass;
}
void GwWifi::setup(){
logger->logString("Wifi setup");
LOG_DEBUG(GwLog::LOG,"Wifi setup");
IPAddress AP_local_ip(192, 168, 15, 1); // Static address for AP
IPAddress AP_gateway(192, 168, 15, 1);
@ -26,7 +26,7 @@ void GwWifi::setup(){
}
delay(100);
WiFi.softAPConfig(AP_local_ip, AP_gateway, AP_subnet);
logger->logString("WifiAP created: ssid=%s,adress=%s",
LOG_DEBUG(GwLog::LOG,"WifiAP created: ssid=%s,adress=%s",
ssid,
WiFi.softAPIP().toString().c_str()
);
@ -34,7 +34,7 @@ void GwWifi::setup(){
lastApAccess=millis();
apShutdownTime=config->getConfigItem(config->stopApTime)->asInt() * 60;
if (apShutdownTime < 120 && apShutdownTime != 0) apShutdownTime=120; //min 2 minutes
logger->logString("GWWIFI: AP auto shutdown %s (%ds)",apShutdownTime> 0?"enabled":"disabled",apShutdownTime);
LOG_DEBUG(GwLog::LOG,"GWWIFI: AP auto shutdown %s (%ds)",apShutdownTime> 0?"enabled":"disabled",apShutdownTime);
apShutdownTime=apShutdownTime*1000; //ms
clientIsConnected=false;
connectInternal();
@ -42,7 +42,7 @@ void GwWifi::setup(){
bool GwWifi::connectInternal(){
if (wifiClient->asBoolean()){
clientIsConnected=false;
logger->logString("creating wifiClient ssid=%s",wifiSSID->asString().c_str());
LOG_DEBUG(GwLog::LOG,"creating wifiClient ssid=%s",wifiSSID->asString().c_str());
wl_status_t rt=WiFi.begin(wifiSSID->asCString(),wifiPass->asCString());
LOG_DEBUG(GwLog::LOG,"wifiClient connect returns %d",(int)rt);
lastConnectStart=millis();
@ -76,7 +76,7 @@ void GwWifi::loop(){
lastApAccess=millis();
}
if ((lastApAccess + apShutdownTime) < millis()){
logger->logString("GWWIFI: shutdown AP");
LOG_DEBUG(GwLog::LOG,"GWWIFI: shutdown AP");
WiFi.softAPdisconnect(true);
apActive=false;
}

View File

@ -1,6 +1,5 @@
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x140000,
app1, app, ota_1, 0x150000,0x130000,
nvs, data, nvs, 0x280000, 0x10000,
spiffs, data, spiffs, 0x290000,0x170000,
app0, app, ota_0, 0x10000, 0x1f0000,
app1, app, ota_1, 0x200000,0x1f0000,

1 # Name # Name, Type, SubType, Offset, Size, Flags Type SubType Offset Size Flags
2 nvs, data, nvs, 0x9000, 0x5000
3 otadata otadata, data, ota, 0xe000, 0x2000, data ota 0xe000 0x2000
4 app0 app0, app, ota_0, 0x10000, 0x1f0000, app ota_0 0x10000 0x140000
5 app1 app1, app, ota_1, 0x200000,0x1f0000, app ota_1 0x150000 0x130000
nvs data nvs 0x280000 0x10000
spiffs data spiffs 0x290000 0x170000

42
post.py
View File

@ -1,7 +1,35 @@
Import("env", "projenv")
import os
import glob
import shutil
print("##post script running")
HDROFFSET=288
VERSIONOFFSET=16
NAMEOFFSET=48
MINSIZE=HDROFFSET+NAMEOFFSET+32
CHECKBYTES={
0: 0xe9, #image magic
288: 0x32, #app header magic
289: 0x54,
290: 0xcd,
291: 0xab
}
def getString(buffer,offset,len):
return buffer[offset:offset+len].rstrip(b'\0').decode('utf-8')
def getFirmwareInfo(imageFile):
with open(imageFile,"rb") as ih:
buffer=ih.read(MINSIZE)
if len(buffer) != MINSIZE:
raise Exception("invalid image file %s, to short",imageFile)
for k,v in CHECKBYTES.items():
if buffer[k] != v:
raise Exception("invalid magic in %s at %d, expected %d got %d"
%(imageFile,k,v,buffer[k]))
name=getString(buffer,HDROFFSET+NAMEOFFSET,32)
version=getString(buffer,HDROFFSET+VERSIONOFFSET,32)
return (name,version)
def post(source,target,env):
#print(env.Dump())
@ -10,6 +38,8 @@ def post(source,target,env):
base=env.subst("$PIOENV")
appoffset=env.subst("$ESP32_APP_OFFSET")
firmware=env.subst("$BUILD_DIR/${PROGNAME}.bin")
(fwname,version)=getFirmwareInfo(firmware)
print("found fwname=%s, fwversion=%s"%(fwname,version))
python=env.subst("$PYTHONEXE")
print("base=%s,esptool=%s,appoffset=%s,uploaderflags=%s"%(base,esptool,appoffset,uploaderflags))
uploadparts=uploaderflags.split(" ")
@ -23,7 +53,17 @@ def post(source,target,env):
print("file %s for combine not found"%uploadfiles[i])
return
offset=uploadfiles[0]
outfile=os.path.join(env.subst("$BUILD_DIR"),base+"-all.bin")
#cleanup old versioned files
outdir=env.subst("$BUILD_DIR")
for f in glob.glob(os.path.join(outdir,base+"*.bin")):
print("removing old file %s"%f)
os.unlink(f)
ofversion=''
if not version.startswith('dev'):
ofversion="-"+version
versionedFile=os.path.join(outdir,"%s%s-update.bin"%(base,ofversion))
shutil.copyfile(firmware,versionedFile)
outfile=os.path.join(outdir,"%s%s-all.bin"%(base,ofversion))
cmd=[python,esptool,"--chip","esp32","merge_bin","--target-offset",offset,"-o",outfile]
cmd+=uploadfiles
cmd+=[appoffset,firmware]

View File

@ -11,21 +11,7 @@
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#define GWSTR(x) #x
#define GWSTRINGIFY(x) GWSTR(x)
#ifdef GWRELEASEVERSION
#define VERSION GWSTRINGIFY(GWRELEASEVERSION)
#define LOGLEVEL GwLog::ERROR
#endif
#ifdef GWDEVVERSION
#define VERSION GWSTRINGIFY(GWDEVVERSION)
#define LOGLEVEL GwLog::DEBUG
#endif
#ifndef VERSION
#define VERSION "0.7.0"
#define LOGLEVEL GwLog::DEBUG
#endif
#include "GwAppInfo.h"
// #define GW_MESSAGE_DEBUG_ENABLED
//#define FALLBACK_SERIAL
const unsigned long HEAP_REPORT_TIME=2000; //set to 0 to disable heap reporting
@ -76,9 +62,6 @@ const unsigned long HEAP_REPORT_TIME=2000; //set to 0 to disable heap reporting
#include "GwStatistics.h"
#include "GwUpdate.h"
#ifndef FIRMWARE_TYPE
#define FIRMWARE_TYPE PIO_ENV_BUILD
#endif
//NMEA message channels
#define N2K_CHANNEL_ID 0
@ -95,7 +78,7 @@ const unsigned long HEAP_REPORT_TIME=2000; //set to 0 to disable heap reporting
#define _impl_PASTE(a,b) a##b
#define _impl_CASSERT_LINE(predicate, line) typedef char _impl_PASTE(assertion_failed_CASSERT_,line)[(predicate)?1:-1];
//assert length of firmware name and version
CASSERT(strlen(GWSTRINGIFY(FIRMWARE_TYPE)) <= 32, "environment name (FIRMWARE_TYPE) must not exceed 32 chars");
CASSERT(strlen(FIRMWARE_TYPE) <= 32, "environment name (FIRMWARE_TYPE) must not exceed 32 chars");
CASSERT(strlen(VERSION) <= 32, "VERSION must not exceed 32 chars");
//https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/app_image_format.html
//and removed the bugs in the doc...
@ -104,7 +87,7 @@ __attribute__((section(".rodata_custom_desc"))) esp_app_desc_t custom_app_desc =
1,
{0,0},
VERSION,
GWSTRINGIFY(FIRMWARE_TYPE),
FIRMWARE_TYPE,
"00:00:00",
"2021/12/13",
"0000",
@ -113,7 +96,7 @@ __attribute__((section(".rodata_custom_desc"))) esp_app_desc_t custom_app_desc =
};
String firmwareType(GWSTRINGIFY(FIRMWARE_TYPE));
String firmwareType(FIRMWARE_TYPE);
typedef std::map<String,String> StringMap;
@ -412,12 +395,20 @@ public:
virtual void getBoatDataValues(int numValues,BoatValue **list){
for (int i=0;i<numValues;i++){
GwBoatItemBase *item=boatData.getBase(list[i]->getName());
list[i]->changed=false;
if (item){
list[i]->valid=item->isValid();
if (list[i]->valid) list[i]->value=item->getDoubleValue();
bool newValid=item->isValid();
if (newValid != list[i]->valid) list[i]->changed=true;
list[i]->valid=newValid;
if (newValid){
double newValue=item->getDoubleValue();
if (newValue != list[i]->value) list[i]->changed=true;
list[i]->value=newValue;
}
list[i]->setFormat(item->getFormat());
}
else{
if (list[i]->valid) list[i]->changed=true;
list[i]->valid=false;
}
}
@ -604,7 +595,7 @@ protected:
bool rt = config.updateValue(it->first, it->second);
if (!rt)
{
logger.logString("ERR: unable to update %s to %s", it->first.c_str(), it->second.c_str());
logger.logDebug(GwLog::ERROR,"ERR: unable to update %s to %s", it->first.c_str(), it->second.c_str());
ok = false;
error += it->first;
error += "=";
@ -615,7 +606,7 @@ protected:
if (ok)
{
result = JSON_OK;
logger.logString("update config and restart");
logger.logDebug(GwLog::ERROR,"update config and restart");
config.saveConfig();
delayedRestart();
}
@ -643,7 +634,7 @@ protected:
return;
}
config.reset(true);
logger.logString("reset config, restart");
logger.logDebug(GwLog::ERROR,"reset config, restart");
result = JSON_OK;
delayedRestart();
}

BIN
tools/esptool.exe Normal file

Binary file not shown.

4582
tools/esptool.py Executable file

File diff suppressed because it is too large Load Diff

BIN
tools/flashtool.exe Normal file

Binary file not shown.

237
tools/flashtool.py Executable file
View File

@ -0,0 +1,237 @@
#! /usr/bin/env python3
import tkinter as tk
from tkinter import ttk
import tkinter.font as tkFont
import os
import serial.tools.list_ports
from tkinter import filedialog as FileDialog
import builtins
VERSION="1.0, esptool 3.2"
oldprint=builtins.print
def print(*args, **kwargs):
app.addText(*args,**kwargs)
builtins.print=print
import esptool
class App:
def __init__(self, root):
root.title("ESP32 NMEA2000 Flash Tool")
root.geometry("800x600")
root.resizable(width=True, height=True)
root.configure(background='lightgrey')
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
frame=tk.Frame(root)
row=0
frame.grid(row=0,column=0,sticky='news')
#frame.configure(background='lightblue')
frame.columnconfigure(0,weight=1)
frame.columnconfigure(1, weight=3)
tk.Label(frame,text="ESP32 NMEA2000 Flash Tool").grid(row=row,column=0,columnspan=2,sticky='ew')
row+=1
tk.Label(frame, text=VERSION).grid(row=row,column=0,columnspan=2,sticky="ew",pady=10)
row+=1
self.mode=tk.IntVar()
self.mode.set(1)
rdFrame=tk.Frame(frame)
rdFrame.grid(row=row,column=1,sticky="ew",pady=20)
tk.Radiobutton(rdFrame,text="Initial Flash",value=1,variable=self.mode,command=self.changeMode).grid(row=0,column=0)
tk.Radiobutton(rdFrame, text="Update Flash", value=2, variable=self.mode,command=self.changeMode).grid(row=0,column=1)
row+=1
tk.Label(frame, text="Com Port").grid(row=row,column=0,sticky='ew')
self.port=ttk.Combobox(frame)
self.port.grid(row=row,column=1,sticky="ew")
row+=1
tk.Label(frame,text="Select Firmware").grid(row=row,column=0,sticky='ew')
self.filename=tk.StringVar()
fn=tk.Entry(frame,textvariable=self.filename)
fn.grid(row=row,column=1,sticky='ew')
fn.bind("<1>",self.fileDialog)
row+=1
self.fileInfo=tk.StringVar()
tk.Label(frame,textvariable=self.fileInfo).grid(row=row,column=0,columnspan=2,sticky="ew")
row+=1
self.flashInfo=tk.StringVar()
self.flashInfo.set("Address 0x1000")
tk.Label(frame,textvariable=self.flashInfo).grid(row=row,column=0,columnspan=2,sticky='ew',pady=10)
row+=1
btFrame=tk.Frame(frame)
btFrame.grid(row=row,column=0,columnspan=2,pady=15)
self.actionButtons=[]
bt=tk.Button(btFrame,text="Check",command=self.buttonCheck)
bt.grid(row=0,column=0)
self.actionButtons.append(bt)
bt=tk.Button(btFrame, text="Flash", command=self.buttonFlash)
bt.grid(row=0, column=1)
self.actionButtons.append(bt)
self.cancelButton=tk.Button(btFrame,text="Cancel",state=tk.DISABLED,command=self.buttonCancel)
self.cancelButton.grid(row=0,column=2)
row+=1
self.text_widget = tk.Text(frame)
frame.rowconfigure(row,weight=1)
self.text_widget.grid(row=row,column=0,columnspan=2,sticky='news')
self.readDevices()
self.interrupt=False
def updateFlashInfo(self):
if self.mode.get() == 1:
#full
self.flashInfo.set("Address 0x1000")
else:
self.flashInfo.set("Erase(otadata): 0xe000...0xffff, Address 0x10000")
def changeMode(self):
m=self.mode.get()
self.updateFlashInfo()
self.filename.set('')
self.fileInfo.set('')
def fileDialog(self,ev):
fn=FileDialog.askopenfilename()
if fn:
self.filename.set(fn)
info=self.checkImageFile(fn,self.mode.get() == 1)
if info['error']:
self.fileInfo.set("***ERROR: %s"%info['info'])
else:
self.fileInfo.set(info['info'])
def readDevices(self):
self.serialDevices=[]
names=[]
for dev in serial.tools.list_ports.comports(False):
self.serialDevices.append(dev.device)
if dev.description != 'n/a':
label=dev.description+"("+dev.device+")"
else:
label=dev.name+"("+dev.device+")"
names.append(label)
self.port.configure(values=names)
def addText(self,*args,**kwargs):
first=True
for k in args:
self.text_widget.insert(tk.END,k)
if not first:
self.text_widget.insert(tk.END, ',')
first=False
if kwargs.get('end') is None:
self.text_widget.insert(tk.END,"\n")
else:
self.text_widget.insert(tk.END,kwargs.get('end'))
self.text_widget.see('end')
root.update()
if self.interrupt:
self.interrupt=False
raise Exception("User cancel")
FULLOFFSET=61440
HDROFFSET = 288
VERSIONOFFSET = 16
NAMEOFFSET = 48
MINSIZE = HDROFFSET + NAMEOFFSET + 32
CHECKBYTES = {
0: 0xe9, # image magic
288: 0x32, # app header magic
289: 0x54,
290: 0xcd,
291: 0xab
}
def getString(self,buffer, offset, len):
return buffer[offset:offset + len].rstrip(b'\0').decode('utf-8')
def getFirmwareInfo(self,ih,imageFile,offset):
buffer = ih.read(self.MINSIZE)
if len(buffer) != self.MINSIZE:
return self.setErr("invalid image file %s, to short"%imageFile)
for k, v in self.CHECKBYTES.items():
if buffer[k] != v:
return self.setErr("invalid magic at %d, expected %d got %d"
% (k+offset, v, buffer[k]))
name = self.getString(buffer, self.HDROFFSET + self.NAMEOFFSET, 32)
version = self.getString(buffer, self.HDROFFSET + self.VERSIONOFFSET, 32)
return {'error':False,'info':"%s:%s"%(name,version)}
def setErr(self,err):
return {'error':True,'info':err}
def checkImageFile(self,filename,isFull):
if not os.path.exists(filename):
return self.setErr("file %s not found"%filename)
with open(filename,"rb") as fh:
offset=0
if isFull:
b=fh.read(1)
if len(b) != 1:
return self.setErr("unable to read header")
if b[0] != 0xe9:
return self.setErr("invalid magic in file, expected 0xe9 got 0x%02x"%b[0])
st=fh.seek(self.FULLOFFSET)
offset=self.FULLOFFSET
return self.getFirmwareInfo(fh,filename,offset)
def runCheck(self):
self.text_widget.delete("1.0", "end")
idx = self.port.current()
isFull = self.mode.get() == 1
if idx < 0:
self.addText("ERROR: no com port selected")
return
port = self.serialDevices[idx]
fn = self.filename.get()
if fn is None or fn == '':
self.addText("ERROR: no filename selected")
return
info = self.checkImageFile(fn, isFull)
if info['error']:
print("ERROR: %s" % info['info'])
return
return {'port':port,'isFull':isFull}
def runEspTool(self,command):
for b in self.actionButtons:
b.configure(state=tk.DISABLED)
self.cancelButton.configure(state=tk.NORMAL)
print("run esptool: %s" % " ".join(command))
root.update()
root.update_idletasks()
try:
esptool.main(command)
print("esptool done")
except Exception as e:
print("Exception in esptool %s" % e)
for b in self.actionButtons:
b.configure(state=tk.NORMAL)
self.cancelButton.configure(state=tk.DISABLED)
def buttonCheck(self):
param = self.runCheck()
if not param:
return
print("Settings OK")
command = ['--chip', 'ESP32', '--port', param['port'], 'chip_id']
self.runEspTool(command)
def buttonFlash(self):
param=self.runCheck()
if not param:
return
if param['isFull']:
command=['--chip','ESP32','--port',param['port'],'write_flash','0x1000',self.filename.get()]
self.runEspTool(command)
else:
command=['--chip','ESP32','--port',param['port'],'erase_region','0xe000','0x2000']
self.runEspTool(command)
command = ['--chip', 'ESP32', '--port', param['port'], 'write_flash', '0x10000', self.filename.get()]
self.runEspTool(command)
def buttonCancel(self):
self.interrupt=True
if __name__ == "__main__":
root = tk.Tk()
app = App(root)
root.mainloop()

View File

@ -0,0 +1,29 @@
How to build the bundled esp tool for windows
=============================================
(1) install python 3 on windows, add to path
(2) pip install pyinstaller
(3) create an empty dir, cd there
(4) get esptool.py (either pip install esptool, search for it or download from
https://github.com/espressif/esptool - just esptool.py) or copy it from here
(5) run: pyinstaller -F esptool.py
esptool.exe in dist\esptool
How to build the bundled flashtool.exe (on windows)
===================================================
(1) install python 3, add to path
(2) pip install pyinstaller
(3) pip install pyserial
(4) in this directory:
pyinstaller -F flashtool.py
will create flashtool.exe in dist
How to run flashtool on Linux
=============================
(1) have python 3 installed
(2) install python3-serial as package __or__ sudo pip3 install pyserial
(3) install python3-tk as package __or__ sudo pip3 install tkinter
(4) ./flashtool.py

View File

@ -116,12 +116,7 @@
"default": "esp32nmea2k",
"check": "checkApPass",
"description": "set the password for the Wifi access point",
"category": "system",
"capabilities": {
"hardwareReset": [
"true"
]
}
"category": "system"
},
{
"name": "useAdminPass",
@ -136,12 +131,7 @@
"default": "esp32admin",
"check": "checkAdminPass",
"description": "set the password for config modifications",
"category": "system",
"capabilities": {
"hardwareReset": [
"true"
]
}
"category": "system"
},
{
"name": "showInvalidData",
@ -455,14 +445,6 @@
"description": "connect to an external WIFI network",
"category": "wifi client"
},
{
"name": "wifiPass",
"label": "wifi client password",
"type": "password",
"default": "",
"description": "the password for an external WIFI network",
"category": "wifi client"
},
{
"name": "wifiSSID",
"label": "wifi client SSID",
@ -472,6 +454,14 @@
"description": "the SSID for an external WIFI network",
"category": "wifi client"
},
{
"name": "wifiPass",
"label": "wifi client password",
"type": "password",
"default": "",
"description": "the password for an external WIFI network",
"category": "wifi client"
},
{
"name": "XDR1",
"label": "XDR1",

View File

@ -1448,11 +1448,12 @@ function updateDashboard(data) {
});
}
}
function uploadBin(){
function uploadBin(ev){
let el=document.getElementById("uploadFile");
let progressEl=document.getElementById("uploadDone");
if (! el) return;
if ( el.files.length < 1) return;
ev.target.disabled=true;
let file=el.files[0];
checkImageFile(file)
.then(function (result) {
@ -1473,12 +1474,16 @@ function uploadBin(){
confirmText += "version in image: " + result.version;
}
}
if (!confirm(confirmText)) return;
if (!confirm(confirmText)) {
ev.target.disabled=false;
return;
}
ensurePass()
.then(function (hash) {
let len = file.size;
let req = new XMLHttpRequest();
req.onloadend = function () {
ev.target.disabled=false;
let result = "unknown error";
try {
let jresult = JSON.parse(req.responseText);
@ -1504,6 +1509,7 @@ function uploadBin(){
}
}
req.onerror = function (e) {
ev.target.disabled=false;
alert("unable to upload: " + e);
}
if (progressEl) {
@ -1520,10 +1526,13 @@ function uploadBin(){
req.open("POST", '/api/update?_hash=' + encodeURIComponent(hash));
req.send(formData);
})
.catch(function (e) { });
.catch(function (e) {
ev.target.disabled=false;
});
})
.catch(function (e) {
alert("This file is an invalid image file:\n" + e);
ev.target.disabled=false;
})
}
let HDROFFSET=288;