diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2562875..c373801 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,8 @@ jobs: os: [ubuntu-latest] runs-on: ${{ matrix.os }} + env: + PIP_BREAK_SYSTEM_PACKAGES: 1 steps: - uses: actions/checkout@v2 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f7a26bc..2aeae17 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,6 +18,9 @@ jobs: # The type of runner that the job will run on runs-on: ubuntu-latest + env: + PIP_BREAK_SYSTEM_PACKAGES: 1 + # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it diff --git a/Readme.md b/Readme.md index 40f8d9b..08ac3e3 100644 --- a/Readme.md +++ b/Readme.md @@ -50,6 +50,10 @@ For the list of hardware set ups refer to [Hardware](doc/Hardware.md). There is a couple of prebuild binaries that you can directly flash to your device. For other combinations of hardware there is an [online build service](doc/BuildService.md) that will allow you to select your hardware and trigger a build. +Connectivity +------------ +For details of the usage of serial devices and the USB connection refer to [Serial and USB](doc/serial-usb.md).
+For details on the networking capabilities refer to [Networking](doc/network.md). Installation ------------ @@ -166,6 +170,28 @@ For details refer to the [example description](lib/exampletask/Readme.md). Changelog --------- +[20241114](../../releases/tag/20241114) +********** +* UDP writer and reader - [#79](../../issues/79) +* extensions for [user tasks](lib/exampletask/Readme.md) + * extend the Web UI with js and css + * register handler for Web Requests +* Naming of the config file [#87](../../issues/87) +* MTW from PGN130311 [#83](../../issues/83) +* USB connection on S3 stops [#81](../../issues/81) +* remove invalid true wind calc, allow to configure some mapping - partly [#75](../../issues/75) +* correctly parse GSV messages [#50](../../issues/50) +* minor adaptations from new version [#66](../../issues/66) +* new platform version 6.8.1 +* internally restructure the channel handling +* add docs for [networking](doc/network.md) and [serial/USB](doc/serial-usb.md) +* allow to configure the timeout(s) for the data display +* new library versions - nmea2000 4.22.0, nmea0183 1.10.1 +* allow for builds completely without FastLED +* wipe the nvs partition on factory reset (to handle corrupted config) +* do not store the wifi settings in nvs on the system level [#78](../../issues/78) +* rename of data: HDG->HDT, MHDG->HDM +* adapt crash decoder tool to s3 [20240428](../../releases/tag/20240428) ********** * fix build error with M5 gps kit diff --git a/doc/Conversions.odt b/doc/Conversions.odt index 975520d..9ca2adb 100644 Binary files a/doc/Conversions.odt and b/doc/Conversions.odt differ diff --git a/doc/Conversions.pdf b/doc/Conversions.pdf index 75b2bd2..3de736b 100644 Binary files a/doc/Conversions.pdf and b/doc/Conversions.pdf differ diff --git a/doc/network.md b/doc/network.md new file mode 100644 index 0000000..6c92692 --- /dev/null +++ b/doc/network.md @@ -0,0 +1,80 @@ +Networking +========== +The gateway includes a couple of network functions to send and receive NMEA data on network connections and to use a WebBrowser for configuration and status display. + +To understand the networking functions you always have to consider 2 parts: + +The "physical" connection +------------------------- +For the gateway this is always a Wifi connection. +It provides the ability to use it as an access point and/or to connect to another wifi network (wifi client). + +Access Point +************ +When starting up it will create an own Wifi network (access point or AP) with the name of the system. You connect to this network like to any Wifi Hotspot. +The initial password is esp32nmea2k. You should change this password on the configuration page in the Web UI. +When you connect the gateway will provide you with an IP address (DHCP) - and it will also have an own IP address in this range (default: 192.168.15.1). If this IP address (range) conflicts with other devices (especially if you run multiple gateways and connect them) you can change the range at the system tab on the configuration page.
+If you do not need the access point during normal operation you can set "stopApTime" so that it will shut down after this time (in minutes) if no device is connected. This will also reduce the power consumption. + +Wifi Client +*********** +Beside the own access point the gateway can also connect to another Wifi network. You need to configure the SSID and password at the "wifi client" tab. +On the status page you can see if the gateway is connected and which address it got within the network. + +You can use both networks to connect to the gateway. It announces it's services via [bonjour](https://developer.apple.com/bonjour/). So you normally should be able to reach your system using Name.local (name being the system name, default ESP32NMEA2K). Or you can use an app that allows for bonjour browsing. + +The "logical" connection +------------------------ +After you connected a device to the gateway on either the access point or by using the same Wifi network you can easily access the Web UI with a browser - e.g. using the Name.local url. + +To send or receive NMEA data you additionally need to configure some connection between the gateway and your device(s). +The gateway provides the following options for NMEA connections: + +TCP Server +********** +When using the TCP server you must configure a connection to the gateway _on the device_ that you would like to connect. The gateway listens at port 10110 (can be changed at the TCP server tab). So on your device you need to configure the address of the gateway and this port. The address depends on the network that you use to connect your device - either the address from the gateway access point (default: 192.168.15.1) - or the address that the gateway was given in the Wifi client network (check this on the status page).
+If your device supports this you can also use the Name.local for the address - or let the device browse for a bonjour service.
+The TCP server has a limit for the number of connections (can be configured, default: 6). As for any channel you can define what it will write and read on it's connections and apply filters. +If you want to send NMEA2000 data via TCP you can enable "Seasmart out". + +TCP Client +********** +With this settings you can configure the gateway to establish a connection to another device that provides data via TCP. You need to know the address and port for that device. If the other device also uses bonjour (like e.g. a second gateway device) you can also use this name instead of the address. +Like for the TCP server you can define what should be send or received with filters. + +UDP Reader +********** +UDP is distinct from TCP in that it does not establish a connection directly. Instead in sends/receives messages - without checking if they have been received by someone at all. Therefore it is also not able to ensure that really all messages are reaching their destination. But as the used protocols (NMEA0183, NMEA2000) are prepared for unreliable connections any way (for serial connections you normally have no idea if the data arrives) UDP is still a valid way of connecting devices.
+One advantage of UDP is that you can send out messages as broadcast or multicast - thus allowing for multiple receivers. + +Small hint for __multicast__:
+Normally in the environment the gateway will work you will not use multicast. If you want to send to multiple devices you will use broadcast. The major difference between them are 2 points:
+ 1. broadcast messages will not pass a real router (but they will be available to all devices connected to one access point) + 2. broadcast messages will always be send to all devices - regardless whether they would like to receive them or not. This can create a bit more network traffic. + +Multicast requires that receivers announce their interest in receiving messages (by "joining" a so called multicast group) and this way only interested devices will receive such messages - and it is possible to configure routers in a way that they route multicast messages. + +To use the gateway UDP reader you must select from where you would like to receive messages. In any case you need to set up a port (default: 10110). Afterwards you need to decide on the allowed sources: + * _all_ (default): accept messages from both the access point and the wifi client network - both broadcast messages and directly addressed ones + * _ap_: only accept messages from devices that are connected to the access point + * _cli_: only accept messages from devices on the Wifi client network + * _mp-all_: you need to configure the multicast address(group) you would like to join and will receive multicast from both the access point and the wifi client network + * _mp-ap_: multicast only from the access point network + * _mp-cli_: multicast only from the wifi client network + +UDP Writer +********** +The UDP writer basically is the counterpart for the UDP reader. +You also have to select where do you want the UDP messages to be sent to. + * _bc-all_ (default): Send the messages as broadcast to devices connected to the own access point and to devices on the wifi client network + * _bc-ap_: send the messages as broadcast only to the access point network + * _bc-cli_: send the messages as broadcast to the wifi client network + * _normal_: you need to configure a destination address (one device) that you want the messages to be send to + * _mc-all_: send messages as multicast to both the access point network and the wifi client network. _Hint_: Only devices that configured the same multicast address will receive such messages. + * _mc-ap_: multicast only to the access point network + * _mc-cli_: multicast only to the wifi client network. + +With the combination of UDP writer and UDP reader you can easily connect multiple gateway devices without a lot of configuration. Just configure one device as UDP writer (with the default settings) and configure other devices as UDP reader (also with default settings) - this way it does not matter how you connect the devices - all devices will receive the data that is sent by the first one.
+__Remark:__ be carefull not to create loops when you would like to send data in both directions between 2 devices using UDP. Either use filters - or use TCP connections as they are able to send data in both directions on one connection (without creating a loop). + +If you want to forward NMEA2000 data from one gateway device to another, just enable "Seasmart out" at the sender side. This will encapsulate the NMEA2000 data in a NMEA0183 compatible format. The receiver will always automatically detect this format and handle the data correctly. diff --git a/doc/serial-usb.md b/doc/serial-usb.md new file mode 100644 index 0000000..428e607 --- /dev/null +++ b/doc/serial-usb.md @@ -0,0 +1,51 @@ +Serial and USB +============== +The gateway software uses the [arduino layer](https://github.com/espressif/arduino-esp32) on top of the [espressif IDF framework](https://github.com/espressif/esp-idf). +The handling of serial devices is mainly done by the implementations in the arduino-espressif layer. +The gateway code adds some buffering on top of this implementation and ensures that normally only full nmea records are sent. +If the external connection is to slow the gateway will drop complete records. +All handling of the serial channels is done in the main task of the gateway. + +Serial Devices +-------------- +THe arduino espressif layer provides the serial devices as [Streams](https://github.com/espressif/arduino-esp32/blob/master/cores/esp32/Stream.h#L48). +Main implementations are [HardwareSerial](https://github.com/espressif/arduino-esp32/blob/3670e2bf2aca822f2e1225fdb0e0796e490005a8/cores/esp32/HardwareSerial.h#L71) - for the UARTS and [HWCDC](https://github.com/espressif/arduino-esp32/blob/3670e2bf2aca822f2e1225fdb0e0796e490005a8/cores/esp32/HWCDC.h#L43)(C3/S3 only) - for the USB CDC hardware device. + +For the github versions: arduino-espressif 3.20009 maps to github tag 2.0.9. + +The arduino espressif layer creates a couple of global instances for the serial devices (potentially depending on some defines). +The important defines for C3/S3 are: + + * ARDUINO_USB_MODE - the gateway always expects this to be 1 + * ARDUINO_USB_CDC_ON_BOOT - 0 or 1 (CB in the table below) + +The created devices for framework 3.20009: + +| Device(Variable) | Type(ESP32) | C3/S3 CB=0 | C3/S3 CB=1 | +| ------------ | ------- | ------ | -----| +| Serial0 | --- | --- | HardwareSerial(0) | +| Serial1 | HardwareSerial(1) | HardwareSerial(1) | HardwareSerial(1) | +| Serial2 | HardwareSerial(2) | HardwareSerial(2) | HardwareSerial(2) | +| USBSerial | --- | HWCDC | ---- | +| Serial | HardwareSerial(0) | HardwareSerial(0) | HWCDC | + +Unfortunately it seems that in newer versions of the framework the devices could change. + +The gateway will use the following serial devices: + +* USBserial:
+ For debug output and NMEA as USB connection. If you do not use an S3/C3 with ARDUINO_USB_CDC_ON_BOOT=0 you need to add a
_define USBSerial Serial_ somewhere in your build flags or in your task header.
+ Currently the gateway does not support setting the pins for the USB channel (that would be possible in principle only if an external PHY device is used and the USB is connected to a normal UART). +* Serial1:
+ If you define GWSERIAL_TYPE (1,2,3,4) - see [defines](../lib/hardware/GwHardware.h#23) or GWSERIAL_MODE ("UNI","BI","TX","RX") it will be used for the first serial channel. +* Serial2:
+ If you define GWSERIAL2_TYPE (1,2,3,4) - see [defines](../lib/hardware/GwHardware.h#23) or GWSERIAL2_MODE ("UNI","BI","TX","RX") it will be used for the second serial channel. + +Hints +----- +For normal ESP32 chips you need to set
_define USBSerial Serial_
and you can use up to 2 serial channels beside the USB channel. +For C3/S3 chips you can go for 2 options: +1. set ARDUINO_USB_CDC_ON_BOOT=1: in This case you still need to set
_define USBSerial Serial_
You can use up to 2 serial channels in the gateway core - but you still have Serial0 available for a third channel (not yet supported by the core - but can be used in your user code) +2. set ARDUINO_USB_CDC_ON_BOOT=0: in this case USBSerial is already defined as the USB channel. You can use 2 channels in the gateway core and optional you can use Serial in your user code. + +If you do not set any of the GWSERIAL* defines (and they are not set by the core hardware definitions) you can freely use Serial1 and / or Serial2 in your user code. diff --git a/extra_script.py b/extra_script.py index 7713451..ed62c64 100644 --- a/extra_script.py +++ b/extra_script.py @@ -18,6 +18,8 @@ OWN_FILE="extra_script.py" GEN_DIR='lib/generated' CFG_FILE='web/config.json' XDR_FILE='web/xdrconfig.json' +INDEXJS="index.js" +INDEXCSS="index.css" CFG_INCLUDE='GwConfigDefinitions.h' CFG_INCLUDE_IMPL='GwConfigDefImpl.h' XDR_INCLUDE='GwXdrTypeMappings.h' @@ -66,6 +68,7 @@ def isCurrent(infile,outfile): def compressFile(inFile,outfile): if isCurrent(inFile,outfile): return + print("compressing %s"%inFile) with open(inFile, 'rb') as f_in: with gzip.open(outfile, 'wb') as f_out: shutil.copyfileobj(f_in, f_out) @@ -372,6 +375,32 @@ def getLibs(): rt.append(e) return rt + + +def joinFiles(target,pattern,dirlist): + flist=[] + for dir in dirlist: + fn=os.path.join(dir,pattern) + if os.path.exists(fn): + flist.append(fn) + current=False + if os.path.exists(target): + current=True + for f in flist: + if not isCurrent(f,target): + current=False + break + if current: + print("%s is up to date"%target) + return + print("creating %s"%target) + with gzip.open(target,"wb") as oh: + for fn in flist: + print("adding %s to %s"%(fn,target)) + with open(fn,"rb") as rh: + shutil.copyfileobj(rh,oh) + + OWNLIBS=getLibs()+["FS","WiFi"] GLOBAL_INCLUDES=[] @@ -440,6 +469,8 @@ def prebuild(env): compressFile(mergedConfig,mergedConfig+".gz") generateCfg(mergedConfig,os.path.join(outPath(),CFG_INCLUDE),False) generateCfg(mergedConfig,os.path.join(outPath(),CFG_INCLUDE_IMPL),True) + joinFiles(os.path.join(outPath(),INDEXJS+".gz"),INDEXJS,["web"]+userTaskDirs) + joinFiles(os.path.join(outPath(),INDEXCSS+".gz"),INDEXCSS,["web"]+userTaskDirs) embedded=getEmbeddedFiles(env) filedefs=[] for ef in embedded: @@ -453,7 +484,6 @@ def prebuild(env): filedefs.append((pureName,usname,ct)) inFile=os.path.join(basePath(),"web",pureName) if os.path.exists(inFile): - print("compressing %s"%inFile) compressFile(inFile,ef) else: print("#WARNING: infile %s for %s not found"%(inFile,ef)) diff --git a/lib/api/GwApi.h b/lib/api/GwApi.h index d4d0716..9d79ef3 100644 --- a/lib/api/GwApi.h +++ b/lib/api/GwApi.h @@ -6,7 +6,9 @@ #include "GWConfig.h" #include "GwBoatData.h" #include "GwXDRMappings.h" +#include "GwSynchronized.h" #include +#include class GwApi; typedef void (*GwUserTaskFunction)(GwApi *); //API to be used for additional tasks @@ -95,6 +97,8 @@ class GwApi{ unsigned long ser2Tx=0; unsigned long tcpSerRx=0; unsigned long tcpSerTx=0; + unsigned long udpwTx=0; + unsigned long udprRx=0; int tcpClients=0; unsigned long tcpClRx=0; unsigned long tcpClTx=0; @@ -169,6 +173,20 @@ class GwApi{ virtual void remove(int idx){} virtual TaskInterfaces * taskInterfaces()=0; + /** + * register handler for web URLs + * Please be aware that this handler function will always be called from a separate + * task. So you must ensure proper synchronization! + */ + using HandlerFunction=std::function; + /** + * @param url: the url of that will trigger the handler. + * it will be prefixed with /api/user/ + * taskname is the name that you used in addUserTask + * @param handler: the handler function (see remark above about thread synchronization) + */ + virtual void registerRequestHandler(const String &url,HandlerFunction handler)=0; + /** * only allowed during init methods */ diff --git a/lib/boatData/GwBoatData.cpp b/lib/boatData/GwBoatData.cpp index df61ddf..39d22c8 100644 --- a/lib/boatData/GwBoatData.cpp +++ b/lib/boatData/GwBoatData.cpp @@ -1,6 +1,7 @@ #include "GwBoatData.h" #include #include +#include "GWConfig.h" #define GWTYPE_DOUBLE 1 #define GWTYPE_UINT32 2 #define GWTYPE_UINT16 3 @@ -35,8 +36,48 @@ GwBoatItemBase::GwBoatItemBase(String name, String format, unsigned long invalid this->format = format; this->type = 0; this->lastUpdateSource = -1; + this->toType=TOType::user; +} +GwBoatItemBase::GwBoatItemBase(String name, String format, GwBoatItemBase::TOType toType) +{ + lastSet = 0; + this->invalidTime = INVALID_TIME; + this->toType=toType; + this->name = name; + this->format = format; + this->type = 0; + this->lastUpdateSource = -1; +} +void GwBoatItemBase::setInvalidTime(GwConfigHandler *cfg){ + if (toType != TOType::user){ + unsigned long timeout=GwBoatItemBase::INVALID_TIME; + switch(getToType()){ + case GwBoatItemBase::TOType::ais: + timeout=cfg->getInt(GwConfigDefinitions::timoAis); + break; + case GwBoatItemBase::TOType::def: + timeout=cfg->getInt(GwConfigDefinitions::timoDefault); + break; + case GwBoatItemBase::TOType::lng: + timeout=cfg->getInt(GwConfigDefinitions::timoLong); + break; + case GwBoatItemBase::TOType::sensor: + timeout=cfg->getInt(GwConfigDefinitions::timoSensor); + break; + case GwBoatItemBase::TOType::keep: + timeout=0; + break; + } + invalidTime=timeout; + } } size_t GwBoatItemBase::getJsonSize() { return JSON_OBJECT_SIZE(10); } + +void GwBoatItemBase::GwBoatItemMap::add(const String &name,GwBoatItemBase *item){ + boatData->setInvalidTime(item); + (*this)[name]=item; +} + #define STRING_SIZE 40 GwBoatItemBase::StringWriter::StringWriter() { @@ -110,7 +151,17 @@ GwBoatItem::GwBoatItem(String name, String formatInfo, unsigned long invalidT this->type = GwBoatItemTypes::getType(dummy); if (map) { - (*map)[name] = this; + map->add(name,this); + } +} +template +GwBoatItem::GwBoatItem(String name, String formatInfo, GwBoatItemBase::TOType toType, GwBoatItemMap *map) : GwBoatItemBase(name, formatInfo, toType) +{ + T dummy; + this->type = GwBoatItemTypes::getType(dummy); + if (map) + { + map->add(name,this); } } @@ -246,14 +297,13 @@ void GwSatInfoList::houseKeeping(unsigned long ts) sats.end(), [ts, this](const GwSatInfo &info) { - return (info.timestamp + lifeTime) < ts; + return info.validTill < ts; }), sats.end()); } -void GwSatInfoList::update(GwSatInfo entry) +void GwSatInfoList::update(GwSatInfo entry, unsigned long validTill) { - unsigned long now = millis(); - entry.timestamp = now; + entry.validTill = validTill; for (auto it = sats.begin(); it != sats.end(); it++) { if (it->PRN == entry.PRN) @@ -267,7 +317,7 @@ void GwSatInfoList::update(GwSatInfo entry) sats.push_back(entry); } -GwBoatDataSatList::GwBoatDataSatList(String name, String formatInfo, unsigned long invalidTime, GwBoatItemMap *map) : GwBoatItem(name, formatInfo, invalidTime, map) {} +GwBoatDataSatList::GwBoatDataSatList(String name, String formatInfo, GwBoatItemBase::TOType toType, GwBoatItemMap *map) : GwBoatItem(name, formatInfo, toType, map) {} bool GwBoatDataSatList::update(GwSatInfo info, int source) { @@ -284,7 +334,7 @@ bool GwBoatDataSatList::update(GwSatInfo info, int source) } lastUpdateSource = source; uls(now); - data.update(info); + data.update(info,now+invalidTime); return true; } void GwBoatDataSatList::toJsonDoc(GwJsonDocument *doc, unsigned long minTime) @@ -293,9 +343,15 @@ void GwBoatDataSatList::toJsonDoc(GwJsonDocument *doc, unsigned long minTime) GwBoatItem::toJsonDoc(doc, minTime); } -GwBoatData::GwBoatData(GwLog *logger) +GwBoatData::GwBoatData(GwLog *logger, GwConfigHandler *cfg) { this->logger = logger; + this->config = cfg; +} +void GwBoatData::begin(){ + for (auto &&it : values){ + it.second->setInvalidTime(config); + } } GwBoatData::~GwBoatData() { @@ -326,7 +382,7 @@ GwBoatItem *GwBoatData::getOrCreate(T initial, GwBoatItemNameProvider *provid provider->getBoatItemFormat(), provider->getInvalidTime(), &values); - rt->update(initial); + rt->update(initial,-1); LOG_DEBUG(GwLog::LOG, "creating boatItem %s, type %d", name.c_str(), rt->getCurrentType()); return rt; @@ -408,6 +464,10 @@ double GwBoatData::getDoubleValue(String name, double defaultv) return defaultv; return it->second->getDoubleValue(); } + +void GwBoatData::setInvalidTime(GwBoatItemBase *item){ + if (config != nullptr) item->setInvalidTime(config); +} double formatCourse(double cv) { double rt = cv * 180.0 / M_PI; diff --git a/lib/boatData/GwBoatData.h b/lib/boatData/GwBoatData.h index 6d3ab19..a4df3b4 100644 --- a/lib/boatData/GwBoatData.h +++ b/lib/boatData/GwBoatData.h @@ -2,6 +2,7 @@ #define _GWBOATDATA_H #include "GwLog.h" +#include "GWConfig.h" #include #include #include @@ -13,8 +14,18 @@ #define ROT_WA_FACTOR 60 class GwJsonDocument; +class GwBoatData; + class GwBoatItemBase{ public: + using TOType=enum{ + def=1, + ais=2, + sensor=3, + lng=4, + user=5, + keep=6 + }; class StringWriter{ uint8_t *buffer =NULL; uint8_t *wp=NULL; @@ -31,7 +42,7 @@ class GwBoatItemBase{ bool baseFilled(); void reset(); }; - static const unsigned long INVALID_TIME=60000; + static const long INVALID_TIME=60000; //the formatter names that must be known in js GWSC(formatCourse); GWSC(formatKnots); @@ -47,14 +58,14 @@ class GwBoatItemBase{ GWSC(formatRot); GWSC(formatDate); GWSC(formatTime); - typedef std::map GwBoatItemMap; protected: int type; unsigned long lastSet=0; - unsigned long invalidTime=INVALID_TIME; + long invalidTime=INVALID_TIME; String name; String format; StringWriter writer; + TOType toType=TOType::def; void uls(unsigned long ts=0){ if (ts) lastSet=ts; else lastSet=millis(); @@ -65,7 +76,8 @@ class GwBoatItemBase{ int getCurrentType(){return type;} unsigned long getLastSet() const {return lastSet;} bool isValid(unsigned long now=0) const ; - GwBoatItemBase(String name,String format,unsigned long invalidTime=INVALID_TIME); + GwBoatItemBase(String name,String format,TOType toType); + GwBoatItemBase(String name,String format,unsigned long invalidTime); virtual ~GwBoatItemBase(){} void invalidate(){ lastSet=0; @@ -82,17 +94,25 @@ class GwBoatItemBase{ virtual double getDoubleValue()=0; String getName(){return name;} const String & getFormat() const{return format;} + virtual void setInvalidTime(GwConfigHandler *cfg); + TOType getToType(){return toType;} + class GwBoatItemMap : public std::map{ + GwBoatData *boatData; + public: + GwBoatItemMap(GwBoatData *bd):boatData(bd){} + void add(const String &name,GwBoatItemBase *item); + }; }; -class GwBoatData; template class GwBoatItem : public GwBoatItemBase{ protected: T data; bool lastStringValid=false; public: GwBoatItem(String name,String formatInfo,unsigned long invalidTime=INVALID_TIME,GwBoatItemMap *map=NULL); + GwBoatItem(String name,String formatInfo,TOType toType,GwBoatItemMap *map=NULL); virtual ~GwBoatItem(){} - bool update(T nv, int source=-1); - bool updateMax(T nv,int sourceId=-1); + bool update(T nv, int source); + bool updateMax(T nv,int sourceId); T getData(){ return data; } @@ -118,14 +138,14 @@ class GwSatInfo{ uint32_t Elevation; uint32_t Azimut; uint32_t SNR; - unsigned long timestamp; + unsigned long validTill; }; class GwSatInfoList{ public: - static const unsigned long lifeTime=32000; + static const GwBoatItemBase::TOType toType=GwBoatItemBase::TOType::lng; std::vector sats; void houseKeeping(unsigned long ts=0); - void update(GwSatInfo entry); + void update(GwSatInfo entry, unsigned long validTill); int getNumSats() const{ return sats.size(); } @@ -139,7 +159,7 @@ class GwSatInfoList{ class GwBoatDataSatList : public GwBoatItem { public: - GwBoatDataSatList(String name, String formatInfo, unsigned long invalidTime = INVALID_TIME, GwBoatItemMap *map = NULL); + GwBoatDataSatList(String name, String formatInfo, GwBoatItemBase::TOType toType, GwBoatItemMap *map = NULL); bool update(GwSatInfo info, int source); virtual void toJsonDoc(GwJsonDocument *doc, unsigned long minTime); GwSatInfo *getAt(int idx){ @@ -164,59 +184,65 @@ public: virtual unsigned long getInvalidTime(){ return GwBoatItemBase::INVALID_TIME;} virtual ~GwBoatItemNameProvider() {} }; -#define GWBOATDATA(type,name,time,fmt) \ +#define GWBOATDATAT(type,name,toType,fmt) \ static constexpr const char* _##name=#name; \ - GwBoatItem *name=new GwBoatItem(#name,GwBoatItemBase::fmt,time,&values) ; -#define GWSPECBOATDATA(clazz,name,time,fmt) \ - clazz *name=new clazz(#name,GwBoatItemBase::fmt,time,&values) ; + GwBoatItem *name=new GwBoatItem(#name,GwBoatItemBase::fmt,toType,&values) ; +#define GWBOATDATA(type,name,fmt) GWBOATDATAT(type,name,GwBoatItemBase::TOType::def,fmt) +#define GWSPECBOATDATA(clazz,name,toType,fmt) \ + clazz *name=new clazz(#name,GwBoatItemBase::fmt,toType,&values) ; class GwBoatData{ private: - GwLog *logger; - GwBoatItemBase::GwBoatItemMap values; + GwLog *logger=nullptr; + GwConfigHandler *config=nullptr; + GwBoatItemBase::GwBoatItemMap values{this}; public: - GWBOATDATA(double,COG,4000,formatCourse) - GWBOATDATA(double,TWD,4000,formatCourse) - GWBOATDATA(double,SOG,4000,formatKnots) - GWBOATDATA(double,STW,4000,formatKnots) - GWBOATDATA(double,TWS,4000,formatKnots) - GWBOATDATA(double,AWS,4000,formatKnots) - GWBOATDATA(double,MaxTws,0,formatKnots) - GWBOATDATA(double,MaxAws,0,formatKnots) - GWBOATDATA(double,AWA,4000,formatWind) - GWBOATDATA(double,HDG,4000,formatCourse) //true heading - GWBOATDATA(double,MHDG,4000,formatCourse) //magnetic heading - GWBOATDATA(double,ROT,4000,formatRot) - GWBOATDATA(double,VAR,4000,formatCourse) //Variation - GWBOATDATA(double,DEV,4000,formatCourse) //Deviation - GWBOATDATA(double,HDOP,4000,formatDop) - GWBOATDATA(double,PDOP,4000,formatDop) - GWBOATDATA(double,VDOP,4000,formatDop) - GWBOATDATA(double,RPOS,4000,formatWind) //RudderPosition - GWBOATDATA(double,PRPOS,4000,formatWind) //second rudder pos - GWBOATDATA(double,LAT,4000,formatLatitude) - GWBOATDATA(double,LON,4000,formatLongitude) - GWBOATDATA(double,ALT,4000,formatFixed0) //altitude - GWBOATDATA(double,DBS,4000,formatDepth) //waterDepth (below surface) - GWBOATDATA(double,DBT,4000,formatDepth) //DepthTransducer - GWBOATDATA(double,GPST,4000,formatTime) //GpsTime - GWBOATDATA(double,WTemp,4000,kelvinToC) - GWBOATDATA(double,XTE,4000,formatXte) - GWBOATDATA(double,DTW,4000,mtr2nm) //distance wp - GWBOATDATA(double,BTW,4000,formatCourse) //bearing wp - GWBOATDATA(double,WPLat,4000,formatLatitude) - GWBOATDATA(double,WPLon,4000,formatLongitude) - GWBOATDATA(uint32_t,Log,16000,mtr2nm) - GWBOATDATA(uint32_t,TripLog,16000,mtr2nm) - GWBOATDATA(uint32_t,GPSD,4000,formatDate) //Date - GWBOATDATA(int16_t,TZ,8000,formatFixed0) - GWSPECBOATDATA(GwBoatDataSatList,SatInfo,GwSatInfoList::lifeTime,formatFixed0); + GWBOATDATA(double,COG,formatCourse) // course over ground + GWBOATDATA(double,SOG,formatKnots) // speed over ground + GWBOATDATA(double,HDT,formatCourse) // true heading + GWBOATDATA(double,HDM,formatCourse) // magnetic heading + GWBOATDATA(double,STW,formatKnots) // water speed + GWBOATDATA(double,VAR,formatWind) // variation + GWBOATDATA(double,DEV,formatWind) // deviation + GWBOATDATA(double,AWA,formatWind) // apparent wind ANGLE + GWBOATDATA(double,AWS,formatKnots) // apparent wind speed + GWBOATDATAT(double,MaxAws,GwBoatItemBase::TOType::keep,formatKnots) + GWBOATDATA(double,TWD,formatCourse) // true wind DIRECTION + GWBOATDATA(double,TWA,formatWind) // true wind ANGLE + GWBOATDATA(double,TWS,formatKnots) // true wind speed + + GWBOATDATAT(double,MaxTws,GwBoatItemBase::TOType::keep,formatKnots) + GWBOATDATA(double,ROT,formatRot) // rate of turn + GWBOATDATA(double,RPOS,formatWind) // rudder position + GWBOATDATA(double,PRPOS,formatWind) // secondary rudder position + GWBOATDATA(double,LAT,formatLatitude) + GWBOATDATA(double,LON,formatLongitude) + GWBOATDATA(double,ALT,formatFixed0) //altitude + GWBOATDATA(double,HDOP,formatDop) + GWBOATDATA(double,PDOP,formatDop) + GWBOATDATA(double,VDOP,formatDop) + GWBOATDATA(double,DBS,formatDepth) //waterDepth (below surface) + GWBOATDATA(double,DBT,formatDepth) //DepthTransducer + GWBOATDATA(double,GPST,formatTime) // GPS time (seconds of day) + GWBOATDATA(uint32_t,GPSD,formatDate) // GPS date (days since 1979-01-01) + GWBOATDATAT(int16_t,TZ,GwBoatItemBase::TOType::lng,formatFixed0) + GWBOATDATA(double,WTemp,kelvinToC) + GWBOATDATAT(uint32_t,Log,GwBoatItemBase::TOType::lng,mtr2nm) + GWBOATDATAT(uint32_t,TripLog,GwBoatItemBase::TOType::lng,mtr2nm) + GWBOATDATA(double,DTW,mtr2nm) // distance to waypoint + GWBOATDATA(double,BTW,formatCourse) // bearing to waypoint + GWBOATDATA(double,XTE,formatXte) // cross track error + GWBOATDATA(double,WPLat,formatLatitude) // waypoint latitude + GWBOATDATA(double,WPLon,formatLongitude) // waypoint longitude + GWSPECBOATDATA(GwBoatDataSatList,SatInfo,GwSatInfoList::toType,formatFixed0); public: - GwBoatData(GwLog *logger); + GwBoatData(GwLog *logger, GwConfigHandler *cfg); ~GwBoatData(); + void begin(); template GwBoatItem *getOrCreate(T initial,GwBoatItemNameProvider *provider); template bool update(T value,int source,GwBoatItemNameProvider *provider); template T getDataWithDefault(T defaultv, GwBoatItemNameProvider *provider); + void setInvalidTime(GwBoatItemBase *item); bool isValid(String name); double getDoubleValue(String name,double defaultv); GwBoatItemBase *getBase(String name); diff --git a/lib/channel/GwChannel.cpp b/lib/channel/GwChannel.cpp index 93a2da3..9c79983 100644 --- a/lib/channel/GwChannel.cpp +++ b/lib/channel/GwChannel.cpp @@ -58,8 +58,6 @@ GwChannel::GwChannel(GwLog *logger, this->name=name; this->sourceId=sourceId; this->maxSourceId=maxSourceId; - this->countIn=new GwCounter(String("count")+name+String("in")); - this->countOut=new GwCounter(String("count")+name+String("out")); this->impl=NULL; this->receiver=new GwChannelMessageReceiver(logger,this); this->actisenseReader=NULL; @@ -100,6 +98,12 @@ void GwChannel::begin( actisenseReader->SetReadStream(channelStream); } } + if (nmeaIn || readActisense){ + this->countIn=new GwCounter(String("count")+name+String("in")); + } + if (nmeaOut || seaSmartOut || writeActisense){ + this->countOut=new GwCounter(String("count")+name+String("out")); + } } void GwChannel::setImpl(GwChannelInterface *impl){ this->impl=impl; @@ -135,10 +139,10 @@ void GwChannel::updateCounter(const char *msg, bool out) } if (key[0] == 0) return; if (out){ - countOut->add(key); + if (countOut) countOut->add(key); } else{ - countIn->add(key); + if (countIn) countIn->add(key); } } @@ -209,7 +213,7 @@ void GwChannel::parseActisense(N2kHandler handler){ tN2kMsg N2kMsg; while (actisenseReader->GetMessageFromStream(N2kMsg)) { - countIn->add(String(N2kMsg.PGN)); + if(countIn) countIn->add(String(N2kMsg.PGN)); handler(N2kMsg,sourceId); } } @@ -218,14 +222,23 @@ void GwChannel::sendActisense(const tN2kMsg &msg, int sourceId){ if (!enabled || ! impl || ! writeActisense || ! channelStream) return; //currently actisense only for channels with a single source id //so we can check it here - if (isOwnSource(sourceId)) return; - countOut->add(String(msg.PGN)); + if (maxSourceId < 0 && this->sourceId == sourceId) return; + if (sourceId >= this->sourceId && sourceId <= maxSourceId) return; + if(countOut) countOut->add(String(msg.PGN)); msg.SendInActisenseFormat(channelStream); } -bool GwChannel::isOwnSource(int id){ - if (maxSourceId < 0) return id == sourceId; - else return (id >= sourceId && id <= maxSourceId); +bool GwChannel::overlaps(const GwChannel *other) const{ + if (maxSourceId < 0){ + if (other->maxSourceId < 0) return sourceId == other->sourceId; + return (other->sourceId <= sourceId && other->maxSourceId >= sourceId); + } + if (other->maxSourceId < 0){ + return other->sourceId >= sourceId && other->sourceId <= maxSourceId; + } + if (other->maxSourceId < sourceId) return false; + if (other->sourceId > maxSourceId) return false; + return true; } unsigned long GwChannel::countRx(){ diff --git a/lib/channel/GwChannel.h b/lib/channel/GwChannel.h index 77af597..66fb4ae 100644 --- a/lib/channel/GwChannel.h +++ b/lib/channel/GwChannel.h @@ -50,7 +50,7 @@ class GwChannel{ ); void setImpl(GwChannelInterface *impl); - bool isOwnSource(int id); + bool overlaps(const GwChannel *) const; void enable(bool enabled){ this->enabled=enabled; } @@ -73,5 +73,11 @@ class GwChannel{ void sendActisense(const tN2kMsg &msg, int sourceId); unsigned long countRx(); unsigned long countTx(); + bool isOwnSource(int source){ + if (maxSourceId < 0) return source == sourceId; + return (source >= sourceId && source <= maxSourceId); + } + String getMode(){return impl->getMode();} + int getMinId(){return sourceId;}; }; diff --git a/lib/channel/GwChannelInterface.h b/lib/channel/GwChannelInterface.h index f9b076c..68f519b 100644 --- a/lib/channel/GwChannelInterface.h +++ b/lib/channel/GwChannelInterface.h @@ -6,4 +6,5 @@ class GwChannelInterface{ virtual void readMessages(GwMessageFetcher *writer)=0; virtual size_t sendToClients(const char *buffer, int sourceId, bool partial=false)=0; virtual Stream * getStream(bool partialWrites){ return NULL;} + virtual String getMode(){return "UNKNOWN";} }; \ No newline at end of file diff --git a/lib/channel/GwChannelList.cpp b/lib/channel/GwChannelList.cpp index a118f13..7861f3d 100644 --- a/lib/channel/GwChannelList.cpp +++ b/lib/channel/GwChannelList.cpp @@ -6,6 +6,8 @@ #include "GwSocketServer.h" #include "GwSerial.h" #include "GwTcpClient.h" +#include "GwUdpWriter.h" +#include "GwUdpReader.h" class SerInit{ public: int serial=-1; @@ -18,108 +20,110 @@ class SerInit{ }; std::vector serialInits; + +static int typeFromMode(const char *mode){ + if (strcmp(mode,"UNI") == 0) return GWSERIAL_TYPE_UNI; + if (strcmp(mode,"BI") == 0) return GWSERIAL_TYPE_BI; + if (strcmp(mode,"RX") == 0) return GWSERIAL_TYPE_RX; + if (strcmp(mode,"TX") == 0) return GWSERIAL_TYPE_TX; + return GWSERIAL_TYPE_UNK; +} + #define CFG_SERIAL(ser,...) \ __MSG("serial config " #ser); \ static GwInitializer __serial ## ser ## _init \ (serialInits,SerInit(ser,__VA_ARGS__)); #ifdef _GWI_SERIAL1 - CFG_SERIAL(1,_GWI_SERIAL1) + CFG_SERIAL(SERIAL1_CHANNEL_ID,_GWI_SERIAL1) #endif #ifdef _GWI_SERIAL2 - CFG_SERIAL(2,_GWI_SERIAL2) + CFG_SERIAL(SERIAL2_CHANNEL_ID,_GWI_SERIAL2) #endif -class GwSerialLog : public GwLogWriter -{ - static const size_t bufferSize = 4096; - char *logBuffer = NULL; - int wp = 0; - GwSerial *writer; - bool disabled = false; - long flushTimeout=200; -public: - GwSerialLog(GwSerial *writer, bool disabled,long flushTimeout=200) + // handle separate defines + // serial 1 +#ifndef GWSERIAL_TX +#define GWSERIAL_TX -1 +#endif +#ifndef GWSERIAL_RX +#define GWSERIAL_RX -1 +#endif +#ifdef GWSERIAL_TYPE + CFG_SERIAL(SERIAL1_CHANNEL_ID, GWSERIAL_RX, GWSERIAL_TX, GWSERIAL_TYPE) +#else +#ifdef GWSERIAL_MODE +CFG_SERIAL(SERIAL1_CHANNEL_ID, GWSERIAL_RX, GWSERIAL_TX, typeFromMode(GWSERIAL_MODE)) +#endif +#endif + // serial 2 +#ifndef GWSERIAL2_TX +#define GWSERIAL2_TX -1 +#endif +#ifndef GWSERIAL2_RX +#define GWSERIAL2_RX -1 +#endif +#ifdef GWSERIAL2_TYPE + CFG_SERIAL(SERIAL2_CHANNEL_ID, GWSERIAL2_RX, GWSERIAL2_TX, GWSERIAL2_TYPE) +#else +#ifdef GWSERIAL2_MODE +CFG_SERIAL(SERIAL2_CHANNEL_ID, GWSERIAL2_RX, GWSERIAL2_TX, typeFromMode(GWSERIAL2_MODE)) +#endif +#endif + class GwSerialLog : public GwLogWriter { - this->writer = writer; - this->disabled = disabled; - this->flushTimeout=flushTimeout; - logBuffer = new char[bufferSize]; - wp = 0; - } - virtual ~GwSerialLog() {} - virtual void write(const char *data) - { - if (disabled) - return; - int len = strlen(data); - if ((wp + len) >= (bufferSize - 1)) - return; - strncpy(logBuffer + wp, data, len); - wp += len; - logBuffer[wp] = 0; - } - virtual void flush() - { - size_t handled = 0; - if (!disabled) + static const size_t bufferSize = 4096; + char *logBuffer = NULL; + int wp = 0; + GwSerial *writer; + bool disabled = false; + + public: + GwSerialLog(GwSerial *writer, bool disabled) { - while (handled < wp) - { - if ( !writer->flush(flushTimeout)) break; - size_t rt = writer->sendToClients(logBuffer + handled, -1, true); - handled += rt; - } - if (handled < wp){ - if (handled > 0){ - memmove(logBuffer,logBuffer+handled,wp-handled); - wp-=handled; - logBuffer[handled]=0; - } + this->writer = writer; + this->disabled = disabled; + logBuffer = new char[bufferSize]; + wp = 0; + } + virtual ~GwSerialLog() {} + virtual void write(const char *data) + { + if (disabled) return; + int len = strlen(data); + if ((wp + len) >= (bufferSize - 1)) + return; + strncpy(logBuffer + wp, data, len); + wp += len; + logBuffer[wp] = 0; + } + virtual void flush() + { + size_t handled = 0; + if (!disabled) + { + while (handled < wp) + { + if (!writer->flush()) + break; + size_t rt = writer->sendToClients(logBuffer + handled, -1, true); + handled += rt; + } + if (handled < wp) + { + if (handled > 0) + { + memmove(logBuffer, logBuffer + handled, wp - handled); + wp -= handled; + logBuffer[wp] = 0; + } + return; + } } + wp = 0; + logBuffer[0] = 0; } - wp = 0; - logBuffer[0] = 0; - } }; -template - class SerialWrapper : public GwChannelList::SerialWrapperBase{ - private: - template - void beginImpl(C *s,unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1){} - void beginImpl(HardwareSerial *s,unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1){ - s->begin(baud,config,rxPin,txPin); - } - template - void setError(C* s, GwLog *logger){} - void setError(HardwareSerial *s,GwLog *logger){ - LOG_DEBUG(GwLog::LOG,"enable serial errors for channel %d",id); - s->onReceiveError([logger,this](hardwareSerial_error_t err){ - LOG_DEBUG(GwLog::ERROR,"serial error on id %d: %d",this->id,(int)err); - }); - } - #if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3 - void beginImpl(HWCDC *s,unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1){ - s->begin(baud); - } - #endif - T *serial; - int id; - public: - SerialWrapper(T* s,int i):serial(s),id(i){} - virtual void begin(GwLog* logger,unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1) override{ - beginImpl(serial,baud,config,rxPin,txPin); - setError(serial,logger); - }; - virtual Stream *getStream() override{ - return serial; - } - virtual int getId() override{ - return id; - } - - }; - GwChannelList::GwChannelList(GwLog *logger, GwConfigHandler *config){ this->logger=logger; @@ -139,10 +143,35 @@ typedef struct { const char *toN2K; const char *readF; const char *writeF; + const char *preventLog; + const char *readAct; + const char *writeAct; + const char *sendSeasmart; const char *name; -} SerialParam; + int maxId; + size_t rxstatus; + size_t txstatus; +} ChannelParam; -static SerialParam serialParameters[]={ +static ChannelParam channelParameters[]={ + { + .id=USB_CHANNEL_ID, + .baud=GwConfigDefinitions::usbBaud, + .receive=GwConfigDefinitions::receiveUsb, + .send=GwConfigDefinitions::sendUsb, + .direction="", + .toN2K=GwConfigDefinitions::usbToN2k, + .readF=GwConfigDefinitions::usbReadFilter, + .writeF=GwConfigDefinitions::usbWriteFilter, + .preventLog=GwConfigDefinitions::usbActisense, + .readAct=GwConfigDefinitions::usbActisense, + .writeAct=GwConfigDefinitions::usbActSend, + .sendSeasmart="", + .name="USB", + .maxId=-1, + .rxstatus=offsetof(GwApi::Status,GwApi::Status::usbRx), + .txstatus=offsetof(GwApi::Status,GwApi::Status::usbTx) + }, { .id=SERIAL1_CHANNEL_ID, .baud=GwConfigDefinitions::serialBaud, @@ -152,7 +181,14 @@ static SerialParam serialParameters[]={ .toN2K=GwConfigDefinitions::serialToN2k, .readF=GwConfigDefinitions::serialReadF, .writeF=GwConfigDefinitions::serialWriteF, - .name="Serial" + .preventLog="", + .readAct="", + .writeAct="", + .sendSeasmart="", + .name="Serial", + .maxId=-1, + .rxstatus=offsetof(GwApi::Status,GwApi::Status::serRx), + .txstatus=offsetof(GwApi::Status,GwApi::Status::serTx) }, { .id=SERIAL2_CHANNEL_ID, @@ -163,81 +199,162 @@ static SerialParam serialParameters[]={ .toN2K=GwConfigDefinitions::serial2ToN2k, .readF=GwConfigDefinitions::serial2ReadF, .writeF=GwConfigDefinitions::serial2WriteF, - .name="Serial2" + .preventLog="", + .readAct="", + .writeAct="", + .sendSeasmart="", + .name="Serial2", + .maxId=-1, + .rxstatus=offsetof(GwApi::Status,GwApi::Status::ser2Rx), + .txstatus=offsetof(GwApi::Status,GwApi::Status::ser2Tx) + }, + { + .id=MIN_TCP_CHANNEL_ID, + .baud="", + .receive=GwConfigDefinitions::readTCP, + .send=GwConfigDefinitions::sendTCP, + .direction="", + .toN2K=GwConfigDefinitions::tcpToN2k, + .readF=GwConfigDefinitions::tcpReadFilter, + .writeF=GwConfigDefinitions::tcpWriteFilter, + .preventLog="", + .readAct="", + .writeAct="", + .sendSeasmart=GwConfigDefinitions::sendSeasmart, + .name="TCPServer", + .maxId=MIN_TCP_CHANNEL_ID+10, + .rxstatus=offsetof(GwApi::Status,GwApi::Status::tcpSerRx), + .txstatus=offsetof(GwApi::Status,GwApi::Status::tcpSerTx) + }, + { + .id=TCP_CLIENT_CHANNEL_ID, + .baud="", + .receive=GwConfigDefinitions::readTCL, + .send=GwConfigDefinitions::sendTCL, + .direction="", + .toN2K=GwConfigDefinitions::tclToN2k, + .readF=GwConfigDefinitions::tclReadFilter, + .writeF=GwConfigDefinitions::tclWriteFilter, + .preventLog="", + .readAct="", + .writeAct="", + .sendSeasmart=GwConfigDefinitions::tclSeasmart, + .name="TCPClient", + .maxId=-1, + .rxstatus=offsetof(GwApi::Status,GwApi::Status::tcpClRx), + .txstatus=offsetof(GwApi::Status,GwApi::Status::tcpClTx) + }, + { + .id=UDPW_CHANNEL_ID, + .baud="", + .receive="", + .send=GwConfigDefinitions::udpwEnabled, + .direction="", + .toN2K="", + .readF="", + .writeF=GwConfigDefinitions::udpwWriteFilter, + .preventLog="", + .readAct="", + .writeAct="", + .sendSeasmart=GwConfigDefinitions::udpwSeasmart, + .name="UDPWriter", + .maxId=-1, + .rxstatus=0, + .txstatus=offsetof(GwApi::Status,GwApi::Status::udpwTx) + }, + { + .id=UDPR_CHANNEL_ID, + .baud="", + .receive=GwConfigDefinitions::udprEnabled, + .send="", + .direction="", + .toN2K=GwConfigDefinitions::udprToN2k, + .readF=GwConfigDefinitions::udprReadFilter, + .writeF="", + .preventLog="", + .readAct="", + .writeAct="", + .sendSeasmart="", + .name="UDPReader", + .maxId=-1, + .rxstatus=offsetof(GwApi::Status,GwApi::Status::udprRx), + .txstatus=0 } + + }; -static SerialParam *getSerialParam(int id){ - for (size_t idx=0;idx(&Serial1,SERIAL1_CHANNEL_ID),type,rx,tx); - return; - } - if (id == 2){ - addSerial(new SerialWrapper(&Serial2,SERIAL2_CHANNEL_ID),type,rx,tx); - return; - } - LOG_DEBUG(GwLog::ERROR,"invalid serial config with id %d",id); -} -void GwChannelList::addSerial(GwChannelList::SerialWrapperBase *stream,int type,int rx,int tx){ - const char *mode=nullptr; - switch (type) - { - case GWSERIAL_TYPE_UNI: - mode="UNI"; - break; - case GWSERIAL_TYPE_BI: - mode="BI"; - break; - case GWSERIAL_TYPE_RX: - mode="RX"; - break; - case GWSERIAL_TYPE_TX: - mode="TX"; - break; - } - if (mode == nullptr) { - LOG_DEBUG(GwLog::ERROR,"unknown serial type %d",type); - return; - } - addSerial(stream,mode,rx,tx); -} -void GwChannelList::addSerial(GwChannelList::SerialWrapperBase *serialStream,const String &mode,int rx,int tx){ - int id=serialStream->getId(); - for (auto &&it:theChannels){ - if (it->isOwnSource(id)){ - LOG_DEBUG(GwLog::ERROR,"trying to re-add serial id=%d, ignoring",id); - return; +template +GwSerial* createSerial(GwLog *logger, T* s,int id, bool canRead=true){ + return new GwSerialImpl(logger,s,id,canRead); +} + +static ChannelParam * findChannelParam(int id){ + ChannelParam *param=nullptr; + for (auto && p: channelParameters){ + if (id == p.id){ + param=&p; + break; } } - SerialParam *param=getSerialParam(id); + return param; +} + +static GwSerial * createSerialImpl(GwConfigHandler *config,GwLog *logger, int idx,int rx,int tx, bool setLog=false){ + LOG_DEBUG(GwLog::DEBUG,"create serial: channel=%d, rx=%d,tx=%d", + idx,rx,tx); + ChannelParam *param=findChannelParam(idx); if (param == nullptr){ - logger->logDebug(GwLog::ERROR,"trying to set up an unknown serial channel: %d",id); - return; + LOG_DEBUG(GwLog::ERROR,"invalid serial channel id %d",idx); + return nullptr; } - if (rx < 0 && tx < 0){ - logger->logDebug(GwLog::ERROR,"useless config for serial %d: both rx/tx undefined"); - return; + GwSerial *serialStream=nullptr; + GwLog *streamLog=setLog?nullptr:logger; + switch(param->id){ + case USB_CHANNEL_ID: + serialStream=createSerial(streamLog,&USBSerial,param->id); + break; + case SERIAL1_CHANNEL_ID: + serialStream=createSerial(streamLog,&Serial1,param->id); + break; + case SERIAL2_CHANNEL_ID: + serialStream=createSerial(streamLog,&Serial2,param->id); + break; + } + if (serialStream == nullptr){ + LOG_DEBUG(GwLog::ERROR,"invalid serial config with id %d",param->id); + return nullptr; + } + serialStream->begin(config->getInt(param->baud,115200),SERIAL_8N1,rx,tx); + if (setLog){ + logger->setWriter(new GwSerialLog(serialStream,config->getBool(param->preventLog,false))); + logger->prefix="GWSERIAL:"; + } + return serialStream; +} +static GwChannel * createChannel(GwLog *logger, GwConfigHandler *config, int id,GwChannelInterface *impl, int type=GWSERIAL_TYPE_BI){ + ChannelParam *param=findChannelParam(id); + if (param == nullptr){ + LOG_DEBUG(GwLog::ERROR,"invalid channel id %d",id); + return nullptr; } - modes[id]=String(mode); bool canRead=false; bool canWrite=false; - if (mode == "BI"){ + bool validType=false; + if (type == GWSERIAL_TYPE_BI){ canRead=config->getBool(param->receive); canWrite=config->getBool(param->send); + validType=true; } - if (mode == "TX"){ + if (type == GWSERIAL_TYPE_TX){ canWrite=true; + validType=true; } - if (mode == "RX"){ + if (type == GWSERIAL_TYPE_RX){ canRead=true; + validType=true; } - if (mode == "UNI"){ + if (type == GWSERIAL_TYPE_UNI ){ String cfgMode=config->getString(param->direction); if (cfgMode == "receive"){ canRead=true; @@ -245,138 +362,102 @@ void GwChannelList::addSerial(GwChannelList::SerialWrapperBase *serialStream,con if (cfgMode == "send"){ canWrite=true; } + validType=true; } - if (rx < 0) canRead=false; - if (tx < 0) canWrite=false; - LOG_DEBUG(GwLog::DEBUG,"serial set up: mode=%s,rx=%d,canRead=%d,tx=%d,canWrite=%d", - mode.c_str(),rx,(int)canRead,tx,(int)canWrite); - serialStream->begin(logger,config->getInt(param->baud,115200),SERIAL_8N1,rx,tx); - GwSerial *serial = new GwSerial(logger, serialStream->getStream(), id, canRead); - LOG_DEBUG(GwLog::LOG, "starting serial %d ", id); - GwChannel *channel = new GwChannel(logger, param->name, id); - channel->setImpl(serial); + if (! validType){ + LOG_DEBUG(GwLog::ERROR,"invalid type for channel %d: %d",param->id,type); + return nullptr; + } + GwChannel *channel = new GwChannel(logger, param->name,param->id,param->maxId); + bool sendSeaSmart=config->getBool(param->sendSeasmart); + bool readAct=config->getBool(param->readAct); + bool writeAct=config->getBool(param->writeAct); + channel->setImpl(impl); channel->begin( - canRead || canWrite, + canRead || canWrite || readAct || writeAct|| sendSeaSmart, canWrite, canRead, config->getString(param->readF), config->getString(param->writeF), - false, + sendSeaSmart, config->getBool(param->toN2K), - false, - false); - LOG_DEBUG(GwLog::LOG, "%s", channel->toString().c_str()); + readAct, + writeAct); + LOG_INFO("created channel %s",channel->toString().c_str()); + return channel; +} +void GwChannelList::addChannel(GwChannel * channel){ + if (channel == nullptr) return; + for (auto &&it:theChannels){ + if (it->overlaps(channel)){ + LOG_DEBUG(GwLog::ERROR,"trying to add channel with overlapping ids %s (%s), ignoring", + channel->toString().c_str(), + it->toString().c_str()); + return; + } + } + LOG_INFO("adding channel %s", channel->toString().c_str()); theChannels.push_back(channel); } void GwChannelList::preinit(){ for (auto &&init:serialInits){ + LOG_INFO("serial config found for %d",init.serial); if (init.fixedBaud >= 0){ - switch(init.serial){ - case 1: - { - LOG_DEBUG(GwLog::DEBUG,"setting fixed baud %d for serial",init.fixedBaud); - config->setValue(GwConfigDefinitions::serialBaud,String(init.fixedBaud),GwConfigInterface::READONLY); - } - break; - case 2: - { - LOG_DEBUG(GwLog::DEBUG,"setting fixed baud %d for serial2",init.fixedBaud); - config->setValue(GwConfigDefinitions::serial2Baud,String(init.fixedBaud),GwConfigInterface::READONLY); - } - break; - default: - LOG_DEBUG(GwLog::ERROR,"invalid serial definition %d found",init.serial) + ChannelParam *param=findChannelParam(init.serial); + if (! param){ + LOG_ERROR("invalid serial definition %d found",init.serial) + return; } + LOG_DEBUG(GwLog::DEBUG,"setting fixed baud %d for serial %d",init.fixedBaud,init.serial); + config->setValue(param->baud,String(init.fixedBaud),GwConfigInterface::READONLY); } } } -template -long getFlushTimeout(S &s){ - return 200; -} -template<> -long getFlushTimeout(HardwareSerial &s){ - return 2000; -} +#ifndef GWUSB_TX + #define GWUSB_TX -1 +#endif +#ifndef GWUSB_RX + #define GWUSB_RX -1 +#endif + void GwChannelList::begin(bool fallbackSerial){ LOG_DEBUG(GwLog::DEBUG,"GwChannelList::begin"); GwChannel *channel=NULL; //usb if (! fallbackSerial){ - GwSerial *usb=new GwSerial(NULL,&USBSerial,USB_CHANNEL_ID); - USBSerial.begin(config->getInt(config->usbBaud)); - logger->setWriter(new GwSerialLog(usb,config->getBool(config->usbActisense),getFlushTimeout(USBSerial))); - logger->prefix="GWSERIAL:"; - channel=new GwChannel(logger,"USB",USB_CHANNEL_ID); - channel->setImpl(usb); - channel->begin(true, - config->getBool(config->sendUsb), - config->getBool(config->receiveUsb), - config->getString(config->usbReadFilter), - config->getString(config->usbWriteFilter), - false, - config->getBool(config->usbToN2k), - config->getBool(config->usbActisense), - config->getBool(config->usbActSend) - ); - theChannels.push_back(channel); - LOG_DEBUG(GwLog::LOG,"%s",channel->toString().c_str()); + GwSerial *usbSerial=createSerialImpl(config, logger,USB_CHANNEL_ID,GWUSB_RX,GWUSB_TX,true); + if (usbSerial != nullptr){ + GwChannel *usbChannel=createChannel(logger,config,USB_CHANNEL_ID,usbSerial,GWSERIAL_TYPE_BI); + if (usbChannel != nullptr){ + addChannel(usbChannel); + } + else{ + delete usbSerial; + } + } } //TCP server sockets=new GwSocketServer(config,logger,MIN_TCP_CHANNEL_ID); sockets->begin(); - channel=new GwChannel(logger,"TCPserver",MIN_TCP_CHANNEL_ID,MIN_TCP_CHANNEL_ID+10); - channel->setImpl(sockets); - channel->begin( - true, - config->getBool(config->sendTCP), - config->getBool(config->readTCP), - config->getString(config->tcpReadFilter), - config->getString(config->tcpWriteFilter), - config->getBool(config->sendSeasmart), - config->getBool(config->tcpToN2k), - false, - false - ); - LOG_DEBUG(GwLog::LOG,"%s",channel->toString().c_str()); - theChannels.push_back(channel); + addChannel(createChannel(logger,config,MIN_TCP_CHANNEL_ID,sockets)); //new serial config handling for (auto &&init:serialInits){ - addSerial(init.serial,init.rx,init.tx,init.mode); + LOG_INFO("creating serial channel %d, rx=%d,tx=%d,type=%d",init.serial,init.rx,init.tx,init.mode); + GwSerial *ser=createSerialImpl(config,logger,init.serial,init.rx,init.tx); + if (ser != nullptr){ + channel=createChannel(logger,config,init.serial,ser,init.mode); + if (channel != nullptr){ + addChannel(channel); + } + else{ + delete ser; + } + } } - //handle separate defines - //serial 1 - #ifndef GWSERIAL_TX - #define GWSERIAL_TX -1 - #endif - #ifndef GWSERIAL_RX - #define GWSERIAL_RX -1 - #endif - #ifdef GWSERIAL_TYPE - addSerial(new SerialWrapper(&Serial1,SERIAL1_CHANNEL_ID),GWSERIAL_TYPE,GWSERIAL_RX,GWSERIAL_TX); - #else - #ifdef GWSERIAL_MODE - addSerial(new SerialWrapper(&Serial1,SERIAL1_CHANNEL_ID),GWSERIAL_MODE,GWSERIAL_RX,GWSERIAL_TX); - #endif - #endif - //serial 2 - #ifndef GWSERIAL2_TX - #define GWSERIAL2_TX -1 - #endif - #ifndef GWSERIAL2_RX - #define GWSERIAL2_RX -1 - #endif - #ifdef GWSERIAL2_TYPE - addSerial(new SerialWrapper(&Serial2,SERIAL2_CHANNEL_ID),GWSERIAL2_TYPE,GWSERIAL2_RX,GWSERIAL2_TX); - #else - #ifdef GWSERIAL2_MODE - addSerial(new SerialWrapper(&Serial2,SERIAL2_CHANNEL_ID),GWSERIAL2_MODE,GWSERIAL2_RX,GWSERIAL2_TX); - #endif - #endif + //tcp client bool tclEnabled=config->getBool(config->tclEnabled); - channel=new GwChannel(logger,"TCPClient",TCP_CLIENT_CHANNEL_ID); if (tclEnabled){ client=new GwTcpClient(logger); client->begin(TCP_CLIENT_CHANNEL_ID, @@ -384,26 +465,27 @@ void GwChannelList::begin(bool fallbackSerial){ config->getInt(config->remotePort), config->getBool(config->readTCL) ); - channel->setImpl(client); } - channel->begin( - tclEnabled, - config->getBool(config->sendTCL), - config->getBool(config->readTCL), - config->getString(config->tclReadFilter), - config->getString(config->tclReadFilter), - config->getBool(config->tclSeasmart), - config->getBool(config->tclToN2k), - false, - false - ); - theChannels.push_back(channel); - LOG_DEBUG(GwLog::LOG,"%s",channel->toString().c_str()); + addChannel(createChannel(logger,config,TCP_CLIENT_CHANNEL_ID,client)); + + //udp writer + if (config->getBool(GwConfigDefinitions::udpwEnabled)){ + GwUdpWriter *writer=new GwUdpWriter(config,logger,UDPW_CHANNEL_ID); + writer->begin(); + addChannel(createChannel(logger,config,UDPW_CHANNEL_ID,writer)); + } + //udp reader + if (config->getBool(GwConfigDefinitions::udprEnabled)){ + GwUdpReader *reader=new GwUdpReader(config,logger,UDPR_CHANNEL_ID); + reader->begin(); + addChannel(createChannel(logger,config,UDPR_CHANNEL_ID,reader)); + } logger->flush(); } String GwChannelList::getMode(int id){ - auto it=modes.find(id); - if (it != modes.end()) return it->second; + for (auto && c: theChannels){ + if (c->isOwnSource(id)) return c->getMode(); + } return "UNKNOWN"; } int GwChannelList::getJsonSize(){ @@ -428,36 +510,28 @@ void GwChannelList::toJson(GwJsonDocument &doc){ }); } GwChannel *GwChannelList::getChannelById(int sourceId){ - for (auto it=theChannels.begin();it != theChannels.end();it++){ - if ((*it)->isOwnSource(sourceId)) return *it; + for (auto && it: theChannels){ + if (it->isOwnSource(sourceId)) return it; } return NULL; } +/** + * slightly tricky generic setter for the API status + * we expect all values to be unsigned long + * the offsets are always offsetof(GwApi::Status,GwApi::Status::xxx) +*/ +static void setStatus(GwApi::Status *status,size_t offset,unsigned long v){ + if (offset == 0) return; + *((unsigned long *)(((unsigned char *)status)+offset))=v; +} + void GwChannelList::fillStatus(GwApi::Status &status){ - GwChannel *channel=getChannelById(USB_CHANNEL_ID); - if (channel){ - status.usbRx=channel->countRx(); - status.usbTx=channel->countTx(); - } - channel=getChannelById(SERIAL1_CHANNEL_ID); - if (channel){ - status.serRx=channel->countRx(); - status.serTx=channel->countTx(); - } - channel=getChannelById(SERIAL2_CHANNEL_ID); - if (channel){ - status.ser2Rx=channel->countRx(); - status.ser2Tx=channel->countTx(); - } - channel=getChannelById(MIN_TCP_CHANNEL_ID); - if (channel){ - status.tcpSerRx=channel->countRx(); - status.tcpSerTx=channel->countTx(); - } - channel=getChannelById(TCP_CLIENT_CHANNEL_ID); - if (channel){ - status.tcpClRx=channel->countRx(); - status.tcpClTx=channel->countTx(); + for (auto && channel: theChannels){ + ChannelParam *param=findChannelParam(channel->getMinId()); + if (param != nullptr){ + setStatus(&status,param->rxstatus,channel->countRx()); + setStatus(&status,param->txstatus,channel->countTx()); + } } } \ No newline at end of file diff --git a/lib/channel/GwChannelList.h b/lib/channel/GwChannelList.h index 373dc6a..f724716 100644 --- a/lib/channel/GwChannelList.h +++ b/lib/channel/GwChannelList.h @@ -8,6 +8,7 @@ #include "GWConfig.h" #include "GwJsonDocument.h" #include "GwApi.h" +#include "GwSerial.h" #include //NMEA message channels @@ -17,29 +18,22 @@ #define SERIAL2_CHANNEL_ID 3 #define TCP_CLIENT_CHANNEL_ID 4 #define MIN_TCP_CHANNEL_ID 5 +#define UDPW_CHANNEL_ID 20 +#define UDPR_CHANNEL_ID 21 #define MIN_USER_TASK 200 class GwSocketServer; class GwTcpClient; class GwChannelList{ private: - class SerialWrapperBase{ - public: - virtual void begin(GwLog* logger,unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1)=0; - virtual Stream *getStream()=0; - virtual int getId()=0; - }; GwLog *logger; GwConfigHandler *config; typedef std::vector ChannelList; ChannelList theChannels; - std::map modes; GwSocketServer *sockets; GwTcpClient *client; - void addSerial(SerialWrapperBase *stream,const String &mode,int rx,int tx); - void addSerial(SerialWrapperBase *stream,int type,int rx,int tx); public: - void addSerial(int id, int rx, int tx, int type); + void addChannel(GwChannel *); GwChannelList(GwLog *logger, GwConfigHandler *config); typedef std::function ChannelAction; void allChannels(ChannelAction action); diff --git a/lib/config/GWConfig.cpp b/lib/config/GWConfig.cpp index e53870a..b372d3d 100644 --- a/lib/config/GWConfig.cpp +++ b/lib/config/GWConfig.cpp @@ -4,6 +4,7 @@ #include #include #include +#include using CfgInit=std::function; static std::vector cfgInits; #define CFG_INIT(name,value,mode) \ @@ -96,15 +97,36 @@ bool GwConfigHandler::updateValue(String name, String value){ if (i->asString() == value){ return false; } - LOG_DEBUG(GwLog::LOG,"update config %s=>%s",name.c_str(),i->isSecret()?"***":value.c_str()); prefs->begin(PREF_NAME,false); prefs->putString(i->getName().c_str(),value); + LOG_DEBUG(GwLog::LOG,"update config %s=>%s, freeEntries=%d",name.c_str(),i->isSecret()?"***":value.c_str(),(int)(prefs->freeEntries())); prefs->end(); } return true; } bool GwConfigHandler::reset(){ - LOG_DEBUG(GwLog::LOG,"reset config"); + LOG_DEBUG(GwLog::ERROR,"reset config"); + //try to find the nvs partition + //currently we only support the default + bool wiped=false; + const esp_partition_t *nvspart=esp_partition_find_first(ESP_PARTITION_TYPE_DATA,ESP_PARTITION_SUBTYPE_DATA_NVS,"nvs"); + if (nvspart != NULL){ + LOG_DEBUG(GwLog::ERROR,"wiping nvs partition"); + esp_err_t err=esp_partition_erase_range(nvspart,0,nvspart->size); + if (err != ESP_OK){ + LOG_DEBUG(GwLog::ERROR,"wiping nvs partition failed: %d",(int)err); + } + else{ + wiped=true; + } + } + else{ + LOG_DEBUG(GwLog::ERROR,"nvs partition not found"); + } + if (wiped){ + return true; + } + LOG_DEBUG(GwLog::ERROR,"unable to wipe nvs partition, trying to reset values"); prefs->begin(PREF_NAME,false); for (int i=0;iputString(configs[i]->getName().c_str(),configs[i]->getDefault()); diff --git a/lib/config/GwConverterConfig.h b/lib/config/GwConverterConfig.h index a3e9c81..d0dbc6a 100644 --- a/lib/config/GwConverterConfig.h +++ b/lib/config/GwConverterConfig.h @@ -16,16 +16,63 @@ #define _GWCONVERTERCONFIG_H #include "GWConfig.h" +#include "N2kTypes.h" +#include + +//list of configs for the PGN 130306 wind references +static std::map windConfigs={ + {N2kWind_True_water,GwConfigDefinitions::windmtra}, + {N2kWind_Apparent,GwConfigDefinitions::windmawa}, + {N2kWind_True_boat,GwConfigDefinitions::windmgna}, + {N2kWind_Magnetic,GwConfigDefinitions::windmmgd}, + {N2kWind_True_North,GwConfigDefinitions::windmtng}, +}; class GwConverterConfig{ public: + class WindMapping{ + public: + using Wind0183Type=enum{ + AWA_AWS, + TWA_TWS, + TWD_TWS, + GWA_GWS, + GWD_GWS + }; + tN2kWindReference n2kType; + Wind0183Type nmea0183Type; + bool valid=false; + WindMapping(){} + WindMapping(const tN2kWindReference &n2k,const Wind0183Type &n183): + n2kType(n2k),nmea0183Type(n183),valid(true){} + WindMapping(const tN2kWindReference &n2k,const String &n183): + n2kType(n2k){ + if (n183 == "twa_tws"){ + nmea0183Type=TWA_TWS; + valid=true; + return; + } + if (n183 == "awa_aws"){ + nmea0183Type=AWA_AWS; + valid=true; + return; + } + if (n183 == "twd_tws"){ + nmea0183Type=TWD_TWS; + valid=true; + return; + } + } + }; int minXdrInterval=100; int starboardRudderInstance=0; int portRudderInstance=-1; //ignore int min2KInterval=50; int rmcInterval=1000; int rmcCheckTime=4000; - void init(GwConfigHandler *config){ + int winst312=256; + std::vector windMappings; + void init(GwConfigHandler *config, GwLog*logger){ minXdrInterval=config->getInt(GwConfigDefinitions::minXdrInterval,100); starboardRudderInstance=config->getInt(GwConfigDefinitions::stbRudderI,0); portRudderInstance=config->getInt(GwConfigDefinitions::portRudderI,-1); @@ -36,6 +83,30 @@ class GwConverterConfig{ rmcInterval=config->getInt(GwConfigDefinitions::sendRMCi,1000); if (rmcInterval < 0) rmcInterval=0; if (rmcInterval > 0 && rmcInterval <100) rmcInterval=100; + winst312=config->getInt(GwConfigDefinitions::winst312,256); + for (auto && it:windConfigs){ + String cfg=config->getString(it.second); + WindMapping mapping(it.first,cfg); + if (mapping.valid){ + LOG_DEBUG(GwLog::ERROR,"add wind mapping n2k=%d,nmea0183=%01d(%s)", + (int)(mapping.n2kType),(int)(mapping.nmea0183Type),cfg.c_str()); + windMappings.push_back(mapping); + } + } } + const WindMapping findWindMapping(const tN2kWindReference &n2k) const{ + for (const auto & it:windMappings){ + if (it.n2kType == n2k) return it; + } + return WindMapping(); + } + const WindMapping findWindMapping(const WindMapping::Wind0183Type &n183) const{ + for (const auto & it:windMappings){ + if (it.nmea0183Type == n183) return it; + } + return WindMapping(); + } + + }; #endif \ No newline at end of file diff --git a/lib/exampletask/GwExampleTask.cpp b/lib/exampletask/GwExampleTask.cpp index 340b785..9b4cbcd 100644 --- a/lib/exampletask/GwExampleTask.cpp +++ b/lib/exampletask/GwExampleTask.cpp @@ -7,6 +7,7 @@ #include #include "N2kMessages.h" #include "GwXdrTypeMappings.h" + /** * INVALID!!! - the next interface declaration will not work * as it is not in the correct header file @@ -144,6 +145,26 @@ String formatValue(GwApi::BoatValue *value){ return String(buffer); } +class ExampleWebData{ + SemaphoreHandle_t lock; + int data=0; + public: + ExampleWebData(){ + lock=xSemaphoreCreateMutex(); + } + ~ExampleWebData(){ + vSemaphoreDelete(lock); + } + void set(int v){ + GWSYNCHRONIZED(&lock); + data=v; + } + int get(){ + GWSYNCHRONIZED(&lock); + return data; + } +}; + void exampleTask(GwApi *api){ GwLog *logger=api->getLogger(); //get some configuration data @@ -172,8 +193,24 @@ void exampleTask(GwApi *api){ LOG_DEBUG(GwLog::LOG,"exampleNotWorking update returned %d",(int)nwrs); String voltageTransducer=api->getConfig()->getString(GwConfigDefinitions::exTransducer); int voltageInstance=api->getConfig()->getInt(GwConfigDefinitions::exInstanceId); + ExampleWebData webData; + /** + * an example web request handler + * it uses a synchronized data structure as it gets called from a different thread + * be aware that you must not block for longer times here! + */ + api->registerRequestHandler("data",[&webData](AsyncWebServerRequest *request){ + int data=webData.get(); + char buffer[30]; + snprintf(buffer,29,"%d",data); + buffer[29]=0; + request->send(200,"text/plain",buffer); + }); + int loopcounter=0; while(true){ delay(1000); + loopcounter++; + webData.set(loopcounter); /* * getting values from the internal data store (boatData) requires some special handling * our tasks runs (potentially) at some time on a different core then the main code diff --git a/lib/exampletask/Readme.md b/lib/exampletask/Readme.md index 965e177..4c51d92 100644 --- a/lib/exampletask/Readme.md +++ b/lib/exampletask/Readme.md @@ -32,6 +32,26 @@ Files This file allows to add some config definitions that are needed for our task. For the possible options have a look at the global [config.json](../../web/config.json). Be careful not to overwrite config defitions from the global file. A good practice wood be to prefix the names of definitions with parts of the library name. Always put them in a separate category so that they do not interfere with the system ones. The defined config items can later be accessed in the code (see the example in [GwExampleTask.cpp](GwExampleTask.cpp)). + * [index.js](index.js)
+ You can add javascript code that will contribute to the UI of the system. The WebUI provides a small API that allows you to "hook" into some functions to include your own parts of the UI. This includes adding new tabs, modifying/replacing the data display items, modifying the status display or accessing the config items. + For the API refer to [../../web/index.js](../../web/index.js#L2001). + To start interacting just register for some events like api.EVENTS.init. You can check the capabilities you have defined to see if your task is active. + By registering an own formatter [api.addUserFormatter](../../web/index.js#L2054) you can influence the way boat data items are shown. + You can even go for an own display by registering for the event *dataItemCreated* and replace the dom element content with your own html. By additionally having added a user formatter you can now fill your own html with the current value. + By using [api.addTabPage](../../web/index.js#L2046) you can add new tabs that you can populate with your own code. Or you can link to an external URL.
+ Please be aware that your js code is always combined with the code from the core into one js file.
+ For fast testing there is a small python script that allow you to test the UI without always flushing each change. + Just run it with + ``` + tools/testServer.py nnn http://x.x.x.x/api + ``` + with nnn being the local port and x.x.x.x the address of a running system. Open `http://localhost:nnn` in your browser.
+ After a change just start the compilation and reload the page. + + * [index.css](index.css)
+ You can add own css to influence the styling of the display. + + Interfaces ---------- The task init function and the task function interact with the core using an [API](../api/GwApi.h) that they get when started. @@ -50,7 +70,8 @@ Files * add capabilities (since 20231105 - as an alternative to a static DECLARE_CAPABILITY ) * add a user task (since 20231105 - as an alternative to a static DECLARE_USERTASK) * store or read task interface data (see below) - + * add a request handler for web requests (since 202411xx) - see registerRequestHandler in the API + __Interfacing between Task__ diff --git a/lib/exampletask/index.css b/lib/exampletask/index.css new file mode 100644 index 0000000..b5b9328 --- /dev/null +++ b/lib/exampletask/index.css @@ -0,0 +1,3 @@ +.examplecss{ + background-color: coral; +} \ No newline at end of file diff --git a/lib/exampletask/index.js b/lib/exampletask/index.js new file mode 100644 index 0000000..b2c6f9d --- /dev/null +++ b/lib/exampletask/index.js @@ -0,0 +1,101 @@ +(function(){ + const api=window.esp32nmea2k; + if (! api) return; + //we only do something if a special capability is set + //on our case this is "testboard" + //so we only start any action when we receive the init event + //and we successfully checked that our requested capability is there + const tabName="example"; + const configName="exampleBDSel"; + const infoUrl='https://github.com/wellenvogel/esp32-nmea2000/tree/master/lib/exampletask'; + let boatItemName; + let boatItemElement; + api.registerListener((id, data) => { + //data is capabilities + //check if our requested capability is there (see GwExampleTask.h) + if (!data.testboard) return; //do nothing if we are not active + //add a simple additional tab page + //you will have to build the content of the page dynamically + //using normal dom manipulation methods + //you can use the helper addEl to create elements + let page = api.addTabPage(tabName, "Example"); + api.addEl('div', 'hdg', page, "this is a test tab"); + let vrow = api.addEl('div', 'row', page); + api.addEl('span', 'label', vrow, 'loops: '); + let lcount = api.addEl('span', 'value', vrow, '0'); + //query the loop count + window.setInterval(() => { + fetch('/api/user/exampleTask/data') + .then((res) => { + if (!res.ok) throw Error("server error: " + res.status); + return res.text(); + }) + .then((txt) => { + //set the text content of our value element with what we received + lcount.textContent = txt; + }) + .catch((e) => console.log("rq:", e)); + }, 1000); + api.addEl('button', '', page, 'Info').addEventListener('click', function (ev) { + window.open(infoUrl, 'info'); + }) + //add a tab for an external URL + api.addTabPage('exhelp', 'Info', infoUrl); + //now as we know we are active - register all the listeners we need + api.registerListener((id, data) => { + console.log("exampletask status listener", data); + }, api.EVENTS.status) + api.registerListener((id, data) => { + if (data === tabName) { + //maybe we need some activity when our page is being activated + console.log("example tab activated"); + } + }, api.EVENTS.tab); + + api.registerListener((id, data) => { + //we have a configuration that + //gives us the name of a boat data item we would like to + //handle special + //in our case we just use an own formatter and add some + //css to the display field + //as this item can change we need to keep track of the + //last item we handled + let nextboatItemName = data[configName]; + console.log("value of " + configName, nextboatItemName); + if (nextboatItemName) { + //register a user formatter that will be called whenever + //there is a new valid value + //we simply add an "X:" in front + api.addUserFormatter(nextboatItemName, "m(x)", function (v, valid) { + if (!valid) return; + return "X:" + v; + }) + //after this call the item will be recreated + } + if (boatItemName !== undefined && boatItemName != nextboatItemName) { + //if the boat item that we handle has changed, remove + //the previous user formatter (this will recreate the item) + api.removeUserFormatter(boatItemName); + } + boatItemName = nextboatItemName; + boatItemElement = undefined; + }, api.EVENTS.config); + api.registerListener((id, data) => { + //this event is called whenever a data item has + //been created (or recreated) + //if this is the item we handle, we just add a css class + //we could also completely rebuild the dom below the element + //and use our formatter to directly write/draw the data + //avoid direct manipulation of the element (i.e. changing the classlist) + //as this element remains there all the time + if (boatItemName && boatItemName == data.name) { + boatItemElement = data.element; + //use the helper forEl to find elements within the dashboard item + //the value element has the class "dashValue" + api.forEl(".dashValue", function (el) { + el.classList.add("examplecss"); + }, boatItemElement); + } + }, api.EVENTS.dataItemCreated); + }, api.EVENTS.init); +})(); diff --git a/lib/gwwebserver/GwWebServer.h b/lib/gwwebserver/GwWebServer.h index 84c265a..c2b48d8 100644 --- a/lib/gwwebserver/GwWebServer.h +++ b/lib/gwwebserver/GwWebServer.h @@ -4,6 +4,7 @@ #include #include "GwMessage.h" #include "GwLog.h" +#include "GwApi.h" class GwWebServer{ private: AsyncWebServer *server; @@ -11,7 +12,7 @@ class GwWebServer{ GwLog *logger; public: typedef GwRequestMessage *(RequestCreator)(AsyncWebServerRequest *request); - using HandlerFunction=std::function; + using HandlerFunction=GwApi::HandlerFunction; GwWebServer(GwLog *logger, GwRequestQueue *queue,int port); ~GwWebServer(); void begin(); diff --git a/lib/gwwifi/GwWifi.cpp b/lib/gwwifi/GwWifi.cpp index 549f99d..69715d2 100644 --- a/lib/gwwifi/GwWifi.cpp +++ b/lib/gwwifi/GwWifi.cpp @@ -1,3 +1,4 @@ +#include #include "GWWifi.h" @@ -35,7 +36,29 @@ void GwWifi::setup(){ LOG_DEBUG(GwLog::ERROR,"unable to set access point mask %s, falling back to %s", apMask.c_str(),AP_subnet.toString().c_str()); } + //try to remove any existing config from nvs + //this will avoid issues when updating from framework 6.3.2 to 6.8.1 - see #78 + //we do not need the nvs config any way - so we set persistent to false + //unfortunately this will be to late (config from nvs has already been loaded) + //if we update from an older version that has config in the nvs + //so we need to make a dummy init, erase the flash and deinit + wifi_config_t conf_current; + wifi_init_config_t conf=WIFI_INIT_CONFIG_DEFAULT(); + esp_err_t err=esp_wifi_init(&conf); + esp_wifi_get_config((wifi_interface_t)WIFI_IF_AP, &conf_current); + LOG_DEBUG(GwLog::DEBUG,"Wifi AP old config before reset ssid=%s, pass=%s, channel=%d",conf_current.ap.ssid,conf_current.ap.password,conf_current.ap.channel); + if (err){ + LOG_DEBUG(GwLog::ERROR,"unable to pre-init wifi: %d",(int)err); + } + err=esp_wifi_restore(); + if (err){ + LOG_DEBUG(GwLog::ERROR,"unable to reset wifi: %d",(int)err); + } + err=esp_wifi_deinit(); + WiFi.persistent(false); WiFi.mode(WIFI_MODE_APSTA); //enable both AP and client + esp_wifi_get_config((wifi_interface_t)WIFI_IF_AP, &conf_current); + LOG_DEBUG(GwLog::DEBUG,"Wifi AP old config after reset ssid=%s, pass=%s, channel=%d",conf_current.ap.ssid,conf_current.ap.password,conf_current.ap.channel); const char *ssid=config->getConfigItem(config->systemName)->asCString(); if (fixedApPass){ WiFi.softAP(ssid,AP_password); @@ -45,7 +68,7 @@ void GwWifi::setup(){ } delay(100); WiFi.softAPConfig(AP_local_ip, AP_gateway, AP_subnet); - LOG_DEBUG(GwLog::LOG,"WifiAP created: ssid=%s,adress=%s", + LOG_DEBUG(GwLog::ERROR,"WifiAP created: ssid=%s,adress=%s", ssid, WiFi.softAPIP().toString().c_str() ); diff --git a/lib/hardware/GwHardware.h b/lib/hardware/GwHardware.h index 8306409..0b14ab3 100644 --- a/lib/hardware/GwHardware.h +++ b/lib/hardware/GwHardware.h @@ -24,6 +24,7 @@ #define GWSERIAL_TYPE_BI 2 #define GWSERIAL_TYPE_RX 3 #define GWSERIAL_TYPE_TX 4 +#define GWSERIAL_TYPE_UNK 0 #include #include #include "GwAppInfo.h" diff --git a/lib/ledtask/GwLedTask.cpp b/lib/ledtask/GwLedTask.cpp index 5df1e9d..0a03005 100644 --- a/lib/ledtask/GwLedTask.cpp +++ b/lib/ledtask/GwLedTask.cpp @@ -1,8 +1,19 @@ #include "GwLedTask.h" #include "GwHardware.h" #include "GwApi.h" + +void handleLeds(GwApi *api); +void initLeds(GwApi *param) +{ +#ifdef GWLED_FASTLED + param->addUserTask(handleLeds, "handleLeds"); +#endif +} + +#ifdef GWLED_FASTLED #include "FastLED.h" -typedef enum { +typedef enum +{ LED_OFF, LED_GREEN, LED_BLUE, @@ -10,41 +21,38 @@ typedef enum { LED_WHITE } GwLedMode; -static CRGB::HTMLColorCode colorFromMode(GwLedMode cmode){ - switch(cmode){ - case LED_BLUE: - return CRGB::Blue; - case LED_GREEN: - return CRGB::Green; - case LED_RED: - return CRGB::Red; - case LED_WHITE: - return CRGB::White; - default: - return CRGB::Black; +static CRGB::HTMLColorCode colorFromMode(GwLedMode cmode) +{ + switch (cmode) + { + case LED_BLUE: + return CRGB::Blue; + case LED_GREEN: + return CRGB::Green; + case LED_RED: + return CRGB::Red; + case LED_WHITE: + return CRGB::White; + default: + return CRGB::Black; } } -void handleLeds(GwApi *api){ - GwLog *logger=api->getLogger(); - #ifndef GWLED_FASTLED - LOG_DEBUG(GwLog::LOG,"currently only fastled handling"); - delay(50); - vTaskDelete(NULL); - return; - #else +void handleLeds(GwApi *api) +{ + GwLog *logger = api->getLogger(); CRGB leds[1]; - #ifdef GWLED_SCHEMA - FastLED.addLeds(leds,1); - #else - FastLED.addLeds(leds,1); - #endif - uint8_t brightness=api->getConfig()->getInt(GwConfigDefinitions::ledBrightness,128); - GwLedMode currentMode=LED_GREEN; - leds[0]=colorFromMode(currentMode); +#ifdef GWLED_SCHEMA + FastLED.addLeds(leds, 1); +#else + FastLED.addLeds(leds, 1); +#endif + uint8_t brightness = api->getConfig()->getInt(GwConfigDefinitions::ledBrightness, 128); + GwLedMode currentMode = LED_GREEN; + leds[0] = colorFromMode(currentMode); FastLED.setBrightness(brightness); FastLED.show(); - LOG_DEBUG(GwLog::LOG,"led task started with mode %d",(int)currentMode); - int apiResult=0; + LOG_DEBUG(GwLog::LOG, "led task started with mode %d, brightness=%d", (int)currentMode, (int)brightness); + int apiResult = 0; while (true) { delay(50); @@ -77,5 +85,5 @@ void handleLeds(GwApi *api){ } } vTaskDelete(NULL); - #endif -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/lib/ledtask/GwLedTask.h b/lib/ledtask/GwLedTask.h index 2ae1532..03cb980 100644 --- a/lib/ledtask/GwLedTask.h +++ b/lib/ledtask/GwLedTask.h @@ -1,8 +1,9 @@ #ifndef _GWLEDS_H #define _GWLEDS_H #include "GwApi.h" -//task function -void handleLeds(GwApi *param); +//task init function -DECLARE_USERTASK(handleLeds); +void initLeds(GwApi *param); + +DECLARE_INITFUNCTION(initLeds); #endif \ No newline at end of file diff --git a/lib/ledtask/platformio.ini b/lib/ledtask/platformio.ini new file mode 100644 index 0000000..c883d3b --- /dev/null +++ b/lib/ledtask/platformio.ini @@ -0,0 +1,15 @@ +[platformio] +#if you want a pio run to only build +#your special environments you can set this here +#by uncommenting the next line +#default_envs = testboard +[env:nodemculed] +board = nodemcu-32s +lib_deps = ${env.lib_deps} +build_flags = + -D BOARD_HOMBERGER + -D GWLED_CODE=1 + -D GWLED_PIN=33 + ${env.build_flags} +upload_port = /dev/esp32 +upload_protocol = esptool diff --git a/lib/log/GwLog.h b/lib/log/GwLog.h index 4958760..bdaa963 100644 --- a/lib/log/GwLog.h +++ b/lib/log/GwLog.h @@ -38,5 +38,7 @@ class GwLog{ long long getRecordCounter(){return recordCounter;} }; #define LOG_DEBUG(level,...){ if (logger != NULL && logger->isActive(level)) logger->logDebug(level,__VA_ARGS__);} +#define LOG_INFO(...){ if (logger != NULL && logger->isActive(GwLog::LOG)) logger->logDebug(GwLog::LOG,__VA_ARGS__);} +#define LOG_ERROR(...){ if (logger != NULL && logger->isActive(GwLog::ERROR)) logger->logDebug(GwLog::ERROR,__VA_ARGS__);} #endif \ No newline at end of file diff --git a/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp b/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp index bfed8fd..bd593d5 100644 --- a/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp +++ b/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp @@ -103,7 +103,7 @@ private: if (v != NMEA0183UInt32NA){ return target->update(v,sourceId); } - return v; + return false; } uint32_t getUint32(GwBoatItem *src){ return src->getDataWithDefault(N2kUInt32NA); @@ -399,28 +399,29 @@ private: return; } tN2kMsg n2kMsg; - tN2kWindReference n2kRef; bool shouldSend=false; WindAngle=formatDegToRad(WindAngle); + GwConverterConfig::WindMapping mapping; switch(Reference){ case NMEA0183Wind_Apparent: - n2kRef=N2kWind_Apparent; shouldSend=updateDouble(boatData->AWA,WindAngle,msg.sourceId) && - updateDouble(boatData->AWS,WindSpeed,msg.sourceId); - if (WindSpeed != NMEA0183DoubleNA) boatData->MaxAws->updateMax(WindSpeed); + updateDouble(boatData->AWS,WindSpeed,msg.sourceId); + if (WindSpeed != NMEA0183DoubleNA) boatData->MaxAws->updateMax(WindSpeed,msg.sourceId); + mapping=config.findWindMapping(GwConverterConfig::WindMapping::AWA_AWS); break; case NMEA0183Wind_True: - n2kRef=N2kWind_True_North; - shouldSend=updateDouble(boatData->TWD,WindAngle,msg.sourceId) && - updateDouble(boatData->TWS,WindSpeed,msg.sourceId); - if (WindSpeed != NMEA0183DoubleNA) boatData->MaxTws->updateMax(WindSpeed); + shouldSend=updateDouble(boatData->TWA,WindAngle,msg.sourceId) && + updateDouble(boatData->TWS,WindSpeed,msg.sourceId); + if (WindSpeed != NMEA0183DoubleNA) boatData->MaxTws->updateMax(WindSpeed,msg.sourceId); + mapping=config.findWindMapping(GwConverterConfig::WindMapping::TWA_TWS); break; default: LOG_DEBUG(GwLog::DEBUG,"unknown wind reference %d in %s",(int)Reference,msg.line); } - if (shouldSend){ - SetN2kWindSpeed(n2kMsg,1,WindSpeed,WindAngle,n2kRef); - send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((int)n2kRef)); + //TODO: try to compute TWD and get mapping for this one + if (shouldSend && mapping.valid){ + SetN2kWindSpeed(n2kMsg,1,WindSpeed,WindAngle,mapping.n2kType); + send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((int)mapping.n2kType)); } } void convertVWR(const SNMEA0183Msg &msg) @@ -457,18 +458,20 @@ private: bool shouldSend = false; shouldSend = updateDouble(boatData->AWA, WindAngle, msg.sourceId) && updateDouble(boatData->AWS, WindSpeed, msg.sourceId); - if (WindSpeed != NMEA0183DoubleNA) boatData->MaxAws->updateMax(WindSpeed); + if (WindSpeed != NMEA0183DoubleNA) boatData->MaxAws->updateMax(WindSpeed,msg.sourceId); if (shouldSend) { - SetN2kWindSpeed(n2kMsg, 1, WindSpeed, WindAngle, N2kWind_Apparent); - send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((int)N2kWind_Apparent)); + const GwConverterConfig::WindMapping mapping=config.findWindMapping(GwConverterConfig::WindMapping::AWA_AWS); + if (mapping.valid){ + SetN2kWindSpeed(n2kMsg, 1, WindSpeed, WindAngle, mapping.n2kType); + send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((int)mapping.n2kType)); + } } } void convertMWD(const SNMEA0183Msg &msg) { - double WindAngle = NMEA0183DoubleNA, WindAngleMagnetic=NMEA0183DoubleNA, - WindSpeed = NMEA0183DoubleNA; + double WindDirection = NMEA0183DoubleNA, WindDirectionMagnetic=NMEA0183DoubleNA, WindSpeed = NMEA0183DoubleNA; if (msg.FieldCount() < 8 ) { LOG_DEBUG(GwLog::DEBUG, "failed to parse MWD %s", msg.line); @@ -476,11 +479,11 @@ private: } if (msg.FieldLen(0) > 0 && msg.Field(1)[0] == 'T') { - WindAngle = formatDegToRad(atof(msg.Field(0))); + WindDirection = formatDegToRad(atof(msg.Field(0))); } if (msg.FieldLen(2) > 0 && msg.Field(3)[0] == 'M') { - WindAngleMagnetic = formatDegToRad(atof(msg.Field(2))); + WindDirectionMagnetic = formatDegToRad(atof(msg.Field(2))); } if (msg.FieldLen(4) > 0 && msg.Field(5)[0] == 'N') { @@ -497,32 +500,38 @@ private: } tN2kMsg n2kMsg; bool shouldSend = false; - if (WindAngle != NMEA0183DoubleNA){ - shouldSend = updateDouble(boatData->TWD, WindAngle, msg.sourceId) && + if (WindDirection != NMEA0183DoubleNA){ + shouldSend = updateDouble(boatData->TWD, WindDirection, msg.sourceId) && updateDouble(boatData->TWS, WindSpeed, msg.sourceId); - if (WindSpeed != NMEA0183DoubleNA) boatData->MaxTws->updateMax(WindSpeed); - } - if (shouldSend) - { - SetN2kWindSpeed(n2kMsg, 1, WindSpeed, WindAngle, N2kWind_True_North); - send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((int)N2kWind_True_North)); - } - if (WindAngleMagnetic != NMEA0183DoubleNA && shouldSend){ - SetN2kWindSpeed(n2kMsg, 1, WindSpeed, WindAngleMagnetic, N2kWind_Magnetic); - send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((int)N2kWind_Magnetic)); + if (WindSpeed != NMEA0183DoubleNA) boatData->MaxTws->updateMax(WindSpeed,msg.sourceId); + if(shouldSend && boatData->HDT->isValid()) { + double twa = WindDirection-boatData->HDT->getData(); + if(twa<0) { twa+=2*M_PI; } + updateDouble(boatData->TWA, twa, msg.sourceId); + const GwConverterConfig::WindMapping mapping=config.findWindMapping(GwConverterConfig::WindMapping::TWA_TWS); + if (mapping.valid){ + SetN2kWindSpeed(n2kMsg, 1, WindSpeed, twa, mapping.n2kType); + send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((int)mapping.n2kType)); + } + const GwConverterConfig::WindMapping mapping2=config.findWindMapping(GwConverterConfig::WindMapping::TWD_TWS); + if (mapping2.valid){ + SetN2kWindSpeed(n2kMsg, 1, WindSpeed, WindDirection, mapping2.n2kType); + send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((int)mapping2.n2kType)); + } + } } } void convertHDM(const SNMEA0183Msg &msg){ - double MHDG=NMEA0183DoubleNA; - if (!NMEA0183ParseHDM_nc(msg, MHDG)) + double HDM=NMEA0183DoubleNA; + if (!NMEA0183ParseHDM_nc(msg, HDM)) { LOG_DEBUG(GwLog::DEBUG, "failed to parse HDM %s", msg.line); return; } - if (! UD(MHDG)) return; + if (! UD(HDM)) return; tN2kMsg n2kMsg; - SetN2kMagneticHeading(n2kMsg,1,MHDG, + SetN2kMagneticHeading(n2kMsg,1,HDM, boatData->VAR->getDataWithDefault(N2kDoubleNA), boatData->DEV->getDataWithDefault(N2kDoubleNA) ); @@ -530,28 +539,29 @@ private: } void convertHDT(const SNMEA0183Msg &msg){ - double HDG=NMEA0183DoubleNA; - if (!NMEA0183ParseHDT_nc(msg, HDG)) + double HDT=NMEA0183DoubleNA; + if (!NMEA0183ParseHDT_nc(msg, HDT)) { LOG_DEBUG(GwLog::DEBUG, "failed to parse HDT %s", msg.line); return; } - if (! UD(HDG)) return; + if (! UD(HDT)) return; tN2kMsg n2kMsg; - SetN2kTrueHeading(n2kMsg,1,HDG); + SetN2kTrueHeading(n2kMsg,1,HDT); send(n2kMsg,msg.sourceId); } + void convertHDG(const SNMEA0183Msg &msg){ - double MHDG=NMEA0183DoubleNA; - double VAR=NMEA0183DoubleNA; + double HDM=NMEA0183DoubleNA; double DEV=NMEA0183DoubleNA; + double VAR=NMEA0183DoubleNA; if (msg.FieldCount() < 5) { LOG_DEBUG(GwLog::DEBUG, "failed to parse HDG %s", msg.line); return; } if (msg.FieldLen(0)>0){ - MHDG=formatDegToRad(atof(msg.Field(0))); + HDM=formatDegToRad(atof(msg.Field(0))); } else{ return; @@ -565,11 +575,11 @@ private: if (msg.Field(4)[0] == 'W') VAR=-VAR; } - if (! UD(MHDG)) return; + if (! UD(HDM)) return; UD(VAR); UD(DEV); tN2kMsg n2kMsg; - SetN2kMagneticHeading(n2kMsg,1,MHDG,DEV,VAR); + SetN2kMagneticHeading(n2kMsg,1,HDM,DEV,VAR); send(n2kMsg,msg.sourceId,"127250M"); } @@ -592,10 +602,10 @@ private: } //offset == 0? SK does not allow this if (Offset != NMEA0183DoubleNA && Offset>=0 ){ - if (! boatData->DBS->update(DepthBelowTransducer+Offset)) return; + if (! boatData->DBS->update(DepthBelowTransducer+Offset,msg.sourceId)) return; } if (Offset == NMEA0183DoubleNA) Offset=N2kDoubleNA; - if (! boatData->DBT->update(DepthBelowTransducer)) return; + if (! boatData->DBT->update(DepthBelowTransducer,msg.sourceId)) return; tN2kMsg n2kMsg; SetN2kWaterDepth(n2kMsg,1,DepthBelowTransducer,Offset); send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((Offset != N2kDoubleNA)?1:0)); @@ -705,11 +715,11 @@ private: return; } tN2kMsg n2kMsg; - if (updateDouble(boatData->HDG,TrueHeading,msg.sourceId)){ + if (updateDouble(boatData->HDT,TrueHeading,msg.sourceId)){ SetN2kTrueHeading(n2kMsg,1,TrueHeading); send(n2kMsg,msg.sourceId); } - if(updateDouble(boatData->MHDG,MagneticHeading,msg.sourceId)){ + if(updateDouble(boatData->HDM,MagneticHeading,msg.sourceId)){ SetN2kMagneticHeading(n2kMsg,1,MagneticHeading, boatData->DEV->getDataWithDefault(N2kDoubleNA), boatData->VAR->getDataWithDefault(N2kDoubleNA) @@ -850,7 +860,7 @@ private: LOG_DEBUG(GwLog::DEBUG,"GSV invalid current %u %s",current,msg.line); return; } - for (int idx=2;idx < msg.FieldCount();idx+=4){ + for (int idx=3;idx < msg.FieldCount();idx+=4){ if (msg.FieldLen(idx) < 1 || msg.FieldLen(idx+1) < 1 || msg.FieldLen(idx+2) < 1 || diff --git a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp index 26c6811..1056c87 100644 --- a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp +++ b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp @@ -185,13 +185,13 @@ private: if (N2kIsNA(Variation)){ //no variation if (ref == N2khr_magnetic){ - updateDouble(boatData->MHDG,Heading); + updateDouble(boatData->HDM,Heading); if (NMEA0183SetHDM(NMEA0183Msg,Heading,talkerId)){ SendMessage(NMEA0183Msg); } } if (ref == N2khr_true){ - updateDouble(boatData->HDG,Heading); + updateDouble(boatData->HDT,Heading); if (NMEA0183SetHDT(NMEA0183Msg,Heading,talkerId)){ SendMessage(NMEA0183Msg); } @@ -206,8 +206,8 @@ private: if (ref == N2khr_true){ MagneticHeading=Heading-Variation; } - updateDouble(boatData->MHDG,MagneticHeading); - updateDouble(boatData->HDG,Heading); + updateDouble(boatData->HDM,MagneticHeading); + updateDouble(boatData->HDT,Heading); if (!N2kIsNA(MagneticHeading)){ if (NMEA0183SetHDG(NMEA0183Msg, MagneticHeading,_Deviation, Variation,talkerId)) @@ -252,8 +252,8 @@ private: tNMEA0183Msg NMEA0183Msg; updateDouble(boatData->STW, WaterReferenced); unsigned long now = millis(); - double MagneticHeading = (boatData->HDG->isValid(now) && boatData->VAR->isValid(now)) ? boatData->HDG->getData() + boatData->VAR->getData() : NMEA0183DoubleNA; - if (NMEA0183SetVHW(NMEA0183Msg, boatData->HDG->getDataWithDefault(NMEA0183DoubleNA), MagneticHeading, WaterReferenced,talkerId)) + double MagneticHeading = (boatData->HDT->isValid(now) && boatData->VAR->isValid(now)) ? boatData->HDT->getData() + boatData->VAR->getData() : NMEA0183DoubleNA; + if (NMEA0183SetVHW(NMEA0183Msg, boatData->HDT->getDataWithDefault(NMEA0183DoubleNA), MagneticHeading, WaterReferenced,talkerId)) { SendMessage(NMEA0183Msg); } @@ -468,37 +468,73 @@ private: { unsigned char SID; tN2kWindReference WindReference; - tNMEA0183WindReference NMEA0183Reference = NMEA0183Wind_True; - - double x, y; double WindAngle=N2kDoubleNA, WindSpeed=N2kDoubleNA; - - if (ParseN2kWindSpeed(N2kMsg, SID, WindSpeed, WindAngle, WindReference)) - { + tNMEA0183WindReference NMEA0183Reference; + if (ParseN2kWindSpeed(N2kMsg, SID, WindSpeed, WindAngle, WindReference)) { tNMEA0183Msg NMEA0183Msg; + GwConverterConfig::WindMapping mapping=config.findWindMapping(WindReference); + bool shouldSend = false; - if (WindReference == N2kWind_Apparent) + // MWV sentence contains apparent/true ANGLE and SPEED + // https://gpsd.gitlab.io/gpsd/NMEA.html#_mwv_wind_speed_and_angle + // https://docs.vaisala.com/r/M211109EN-L/en-US/GUID-7402DEF8-5E82-446F-B63E-998F49F3D743/GUID-C77934C7-2A72-466E-BC52-CE6B8CC7ACB6 + if (mapping.valid) { - NMEA0183Reference = NMEA0183Wind_Apparent; - updateDouble(boatData->AWA, WindAngle); - updateDouble(boatData->AWS, WindSpeed); - setMax(boatData->MaxAws, boatData->AWS); - } - if (WindReference == N2kWind_True_North) - { - NMEA0183Reference = NMEA0183Wind_True; - updateDouble(boatData->TWD, WindAngle); - updateDouble(boatData->TWS, WindSpeed); + if (mapping.nmea0183Type == GwConverterConfig::WindMapping::AWA_AWS) + { + NMEA0183Reference = NMEA0183Wind_Apparent; + updateDouble(boatData->AWA, WindAngle); + updateDouble(boatData->AWS, WindSpeed); + setMax(boatData->MaxAws, boatData->AWS); + shouldSend = true; + } + if (mapping.nmea0183Type == GwConverterConfig::WindMapping::TWA_TWS) + { + NMEA0183Reference = NMEA0183Wind_True; + updateDouble(boatData->TWA, WindAngle); + updateDouble(boatData->TWS, WindSpeed); + setMax(boatData->MaxTws, boatData->TWS); + shouldSend = true; + if (boatData->HDT->isValid()) + { + double twd = WindAngle + boatData->HDT->getData(); + if (twd > 2 * M_PI) + { + twd -= 2 * M_PI; + } + updateDouble(boatData->TWD, twd); + } + } + if (mapping.nmea0183Type == GwConverterConfig::WindMapping::TWD_TWS) + { + NMEA0183Reference = NMEA0183Wind_True; + updateDouble(boatData->TWD, WindAngle); + updateDouble(boatData->TWS, WindSpeed); + setMax(boatData->MaxTws, boatData->TWS); + if (boatData->HDT->isValid()) + { + shouldSend = true; + double twa = WindAngle - boatData->HDT->getData(); + if (twa > 2 * M_PI) + { + twa -= 2 * M_PI; + } + updateDouble(boatData->TWA, twa); + WindAngle=twa; + } + } + + if (shouldSend && NMEA0183SetMWV(NMEA0183Msg, formatCourse(WindAngle), NMEA0183Reference, WindSpeed, talkerId)) + { + SendMessage(NMEA0183Msg); + } } - if (NMEA0183SetMWV(NMEA0183Msg, formatCourse(WindAngle), NMEA0183Reference, WindSpeed,talkerId)) - SendMessage(NMEA0183Msg); - - if (WindReference == N2kWind_Apparent && boatData->SOG->isValid()) + /* if (WindReference == N2kWind_Apparent && boatData->SOG->isValid()) { // Lets calculate and send TWS/TWA if SOG is available - x = WindSpeed * cos(WindAngle); - y = WindSpeed * sin(WindAngle); + double x = WindSpeed * cos(WindAngle); + double y = WindSpeed * sin(WindAngle); updateDouble(boatData->TWD, atan2(y, -boatData->SOG->getData() + x)); updateDouble(boatData->TWS, sqrt((y * y) + ((-boatData->SOG->getData() + x) * (-boatData->SOG->getData() + x)))); @@ -534,7 +570,7 @@ private: return; SendMessage(NMEA0183Msg); - } + } */ } } //***************************************************************************** @@ -657,12 +693,14 @@ private: double _Heading=N2kDoubleNA; double _ROT=N2kDoubleNA; tN2kAISNavStatus _NavStatus; + tN2kAISTransceiverInformation _AISTransceiverInformation; + uint8_t _SID; uint8_t _MessageType = 1; tNMEA0183AISMsg NMEA0183AISMsg; if (ParseN2kPGN129038(N2kMsg, SID, _Repeat, _UserID, _Latitude, _Longitude, _Accuracy, _RAIM, _Seconds, - _COG, _SOG, _Heading, _ROT, _NavStatus)) + _COG, _SOG, _Heading, _ROT, _NavStatus,_AISTransceiverInformation,_SID)) { // Debug @@ -746,12 +784,13 @@ private: tN2kGNSStype _GNSStype; tN2kAISTransceiverInformation _AISinfo; tN2kAISDTE _DTE; + uint8_t _SID; tNMEA0183AISMsg NMEA0183AISMsg; - if (ParseN2kPGN129794(N2kMsg, _MessageID, _Repeat, _UserID, _IMONumber, _Callsign, _Name, _VesselType, - _Length, _Beam, _PosRefStbd, _PosRefBow, _ETAdate, _ETAtime, _Draught, _Destination, - _AISversion, _GNSStype, _DTE, _AISinfo)) + if (ParseN2kPGN129794(N2kMsg, _MessageID, _Repeat, _UserID, _IMONumber, _Callsign, 8, _Name,21, _VesselType, + _Length, _Beam, _PosRefStbd, _PosRefBow, _ETAdate, _ETAtime, _Draught, _Destination,21, + _AISversion, _GNSStype, _DTE, _AISinfo,_SID)) { #ifdef SERIAL_PRINT_AIS_FIELDS @@ -855,9 +894,10 @@ private: bool _Display, _DSC, _Band, _Msg22, _State; tN2kAISMode _Mode; tN2kAISTransceiverInformation _AISTranceiverInformation; + uint8_t _SID; if (ParseN2kPGN129039(N2kMsg, _MessageID, _Repeat, _UserID, _Latitude, _Longitude, _Accuracy, _RAIM, - _Seconds, _COG, _SOG, _AISTranceiverInformation, _Heading, _Unit, _Display, _DSC, _Band, _Msg22, _Mode, _State)) + _Seconds, _COG, _SOG, _AISTranceiverInformation, _Heading, _Unit, _Display, _DSC, _Band, _Msg22, _Mode, _State,_SID)) { tNMEA0183AISMsg NMEA0183AISMsg; @@ -896,8 +936,10 @@ private: tN2kAISRepeat _Repeat; uint32_t _UserID; // MMSI char _Name[21]; + tN2kAISTransceiverInformation _AISInfo; + uint8_t _SID; - if (ParseN2kPGN129809(N2kMsg, _MessageID, _Repeat, _UserID, _Name)) + if (ParseN2kPGN129809(N2kMsg, _MessageID, _Repeat, _UserID, _Name,21,_AISInfo,_SID)) { tNMEA0183AISMsg NMEA0183AISMsg; @@ -923,9 +965,11 @@ private: double _Beam=N2kDoubleNA; double _PosRefStbd=N2kDoubleNA; double _PosRefBow=N2kDoubleNA; + tN2kAISTransceiverInformation _AISInfo; + uint8_t _SID; - if (ParseN2kPGN129810(N2kMsg, _MessageID, _Repeat, _UserID, _VesselType, _Vendor, _Callsign, - _Length, _Beam, _PosRefStbd, _PosRefBow, _MothershipID)) + if (ParseN2kPGN129810(N2kMsg, _MessageID, _Repeat, _UserID, _VesselType, _Vendor,4, _Callsign,8, + _Length, _Beam, _PosRefStbd, _PosRefBow, _MothershipID,_AISInfo,_SID)) { // @@ -1121,8 +1165,8 @@ private: int16_t ETADate=0; double BearingOriginToDestinationWaypoint=N2kDoubleNA; double BearingPositionToDestinationWaypoint=N2kDoubleNA; - uint8_t OriginWaypointNumber; - uint8_t DestinationWaypointNumber; + uint32_t OriginWaypointNumber; + uint32_t DestinationWaypointNumber; double DestinationLatitude=N2kDoubleNA; double DestinationLongitude=N2kDoubleNA; double WaypointClosingVelocity=N2kDoubleNA; @@ -1288,6 +1332,20 @@ private: return; } int i=0; + if (TempSource == N2kts_SeaTemperature) { + updateDouble(boatData->WTemp, Temperature); + tNMEA0183Msg NMEA0183Msg; + + if (!NMEA0183Msg.Init("MTW", talkerId)) + return; + if (!NMEA0183Msg.AddDoubleField(KelvinToC(Temperature))) + return; + if (!NMEA0183Msg.AddStrField("C")) + return; + + SendMessage(NMEA0183Msg); + } + GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRTEMP,TempSource,0,0); if (updateDouble(&mapping,Temperature)){ LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str()); @@ -1320,6 +1378,21 @@ private: LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN); return; } + if (TemperatureSource == N2kts_SeaTemperature && + (config.winst312 == TemperatureInstance || config.winst312 == 256)) { + updateDouble(boatData->WTemp, Temperature); + tNMEA0183Msg NMEA0183Msg; + + if (!NMEA0183Msg.Init("MTW", talkerId)) + return; + if (!NMEA0183Msg.AddDoubleField(KelvinToC(Temperature))) + return; + if (!NMEA0183Msg.AddStrField("C")) + return; + + SendMessage(NMEA0183Msg); + } + GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRTEMP,(int)TemperatureSource,0,TemperatureInstance); if (updateDouble(&mapping,Temperature)){ LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str()); diff --git a/lib/nmea2ktoais/NMEA0183AISMessages.cpp b/lib/nmea2ktoais/NMEA0183AISMessages.cpp index 081a1b6..a0f9ec0 100644 --- a/lib/nmea2ktoais/NMEA0183AISMessages.cpp +++ b/lib/nmea2ktoais/NMEA0183AISMessages.cpp @@ -219,7 +219,7 @@ bool SetAISClassBMessage18(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, u bool SetAISClassBMessage24PartA(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, uint32_t UserID, char *Name) { bool found = false; - for (int i = 0; i < vships.size(); i++) { + for (size_t i = 0; i < vships.size(); i++) { if ( vships[i]->_userID == UserID ) { found = true; break; diff --git a/lib/queue/GwBuffer.cpp b/lib/queue/GwBuffer.cpp index 8e12934..66a8d75 100644 --- a/lib/queue/GwBuffer.cpp +++ b/lib/queue/GwBuffer.cpp @@ -21,7 +21,7 @@ GwBuffer::~GwBuffer(){ } void GwBuffer::reset(String reason) { - LOG_DEBUG(GwLog::LOG,"reseting buffer %s, reason %s",this->name.c_str(),reason.c_str()); + if (! reason.isEmpty())LOG_DEBUG(GwLog::LOG,"reseting buffer %s, reason %s",this->name.c_str(),reason.c_str()); writePointer = buffer; readPointer = buffer; lp("reset"); diff --git a/lib/queue/GwBuffer.h b/lib/queue/GwBuffer.h index 17490ec..145984f 100644 --- a/lib/queue/GwBuffer.h +++ b/lib/queue/GwBuffer.h @@ -18,9 +18,9 @@ class GwMessageFetcher{ * buffer to safely inserte data if it fits * and to write out data if possible */ -typedef size_t (*GwBufferHandleFunction)(uint8_t *buffer, size_t len, void *param); class GwBuffer{ public: + using GwBufferHandleFunction=std::function; static const size_t TX_BUFFER_SIZE=1620; // app. 20 NMEA messages static const size_t RX_BUFFER_SIZE=600; // enough for 1 NMEA message or actisense message or seasmart message typedef enum { @@ -33,7 +33,7 @@ class GwBuffer{ uint8_t *buffer; uint8_t *writePointer; uint8_t *readPointer; - size_t offset(uint8_t* ptr){ + size_t offset(uint8_t* ptr) const{ return (size_t)(ptr-buffer); } GwLog *logger; diff --git a/lib/queue/GwSynchronized.h b/lib/queue/GwSynchronized.h index 53241db..786b5f0 100644 --- a/lib/queue/GwSynchronized.h +++ b/lib/queue/GwSynchronized.h @@ -7,10 +7,10 @@ class GwSynchronized{ public: GwSynchronized(SemaphoreHandle_t *locker){ this->locker=locker; - xSemaphoreTake(*locker, portMAX_DELAY); + if (locker != nullptr) xSemaphoreTake(*locker, portMAX_DELAY); } ~GwSynchronized(){ - xSemaphoreGive(*locker); + if (locker != nullptr) xSemaphoreGive(*locker); } }; diff --git a/lib/serial/GwSerial.cpp b/lib/serial/GwSerial.cpp index c810e58..c0fb06e 100644 --- a/lib/serial/GwSerial.cpp +++ b/lib/serial/GwSerial.cpp @@ -1,4 +1,5 @@ #include "GwSerial.h" +#include "GwHardware.h" class GwSerialStream: public Stream{ private: @@ -40,11 +41,13 @@ class GwSerialStream: public Stream{ -GwSerial::GwSerial(GwLog *logger, Stream *s, int id,bool allowRead):serial(s) +GwSerial::GwSerial(GwLog *logger, Stream * stream,int id,int type,bool allowRead) { LOG_DEBUG(GwLog::DEBUG,"creating GwSerial %p id %d",this,id); - this->id=id; this->logger = logger; + this->id=id; + this->stream=stream; + this->type=type; String bufName="Ser("; bufName+=String(id); bufName+=")"; @@ -62,6 +65,20 @@ GwSerial::~GwSerial() if (readBuffer) delete readBuffer; } +String GwSerial::getMode(){ + switch (type){ + case GWSERIAL_TYPE_UNI: + return "UNI"; + case GWSERIAL_TYPE_BI: + return "BI"; + case GWSERIAL_TYPE_RX: + return "RX"; + case GWSERIAL_TYPE_TX: + return "TX"; + } + return "UNKNOWN"; +} + bool GwSerial::isInitialized() { return initialized; } size_t GwSerial::enqueue(const uint8_t *data, size_t len, bool partial) { @@ -70,9 +87,9 @@ size_t GwSerial::enqueue(const uint8_t *data, size_t len, bool partial) } GwBuffer::WriteStatus GwSerial::write(){ if (! isInitialized()) return GwBuffer::ERROR; - size_t numWrite=serial->availableForWrite(); + size_t numWrite=availableForWrite(); size_t rt=buffer->fetchData(numWrite,[](uint8_t *buffer,size_t len, void *p){ - return ((GwSerial *)p)->serial->write(buffer,len); + return ((GwSerial *)p)->stream->write(buffer,len); },this); if (rt != 0){ LOG_DEBUG(GwLog::DEBUG+1,"Serial %d write %d",id,rt); @@ -93,11 +110,11 @@ void GwSerial::loop(bool handleRead,bool handleWrite){ write(); if (! isInitialized()) return; if (! handleRead) return; - size_t available=serial->available(); + size_t available=stream->available(); if (! available) return; if (allowRead){ size_t rd=readBuffer->fillData(available,[](uint8_t *buffer, size_t len, void *p)->size_t{ - return ((GwSerial *)p)->serial->readBytes(buffer,len); + return ((GwSerial *)p)->stream->readBytes(buffer,len); },this); if (rd != 0){ LOG_DEBUG(GwLog::DEBUG+2,"GwSerial %d read %d bytes",id,rd); @@ -106,7 +123,7 @@ void GwSerial::loop(bool handleRead,bool handleWrite){ else{ uint8_t buffer[10]; if (available > 10) available=10; - serial->readBytes(buffer,available); + stream->readBytes(buffer,available); } } void GwSerial::readMessages(GwMessageFetcher *writer){ @@ -115,10 +132,11 @@ void GwSerial::readMessages(GwMessageFetcher *writer){ writer->handleBuffer(readBuffer); } -bool GwSerial::flush(long max){ +bool GwSerial::flush(){ if (! isInitialized()) return false; + long max=getFlushTimeout(); if (! availableWrite) { - if ( serial->availableForWrite() < 1){ + if ( availableForWrite() < 1){ return false; } availableWrite=true; @@ -128,7 +146,7 @@ bool GwSerial::flush(long max){ if (write() != GwBuffer::AGAIN) return true; vTaskDelay(1); } - availableWrite=(serial->availableForWrite() > 0); + availableWrite=(availableForWrite() > 0); return false; } Stream * GwSerial::getStream(bool partialWrite){ diff --git a/lib/serial/GwSerial.h b/lib/serial/GwSerial.h index 5298bc6..f3712ab 100644 --- a/lib/serial/GwSerial.h +++ b/lib/serial/GwSerial.h @@ -4,31 +4,119 @@ #include "GwLog.h" #include "GwBuffer.h" #include "GwChannelInterface.h" +#if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3 + #include "hal/usb_serial_jtag_ll.h" +#endif + +#define USBCDC_RESTART_TIME 100 class GwSerialStream; class GwSerial : public GwChannelInterface{ - private: + protected: GwBuffer *buffer; GwBuffer *readBuffer=NULL; GwLog *logger; + Stream *stream; bool initialized=false; bool allowRead=true; GwBuffer::WriteStatus write(); int id=-1; int overflows=0; size_t enqueue(const uint8_t *data, size_t len,bool partial=false); - Stream *serial; bool availableWrite=false; //if this is false we will wait for availabkleWrite until we flush again + virtual long getFlushTimeout(){return 2000;} + virtual int availableForWrite()=0; + int type=0; public: - static const int bufferSize=200; - GwSerial(GwLog *logger,Stream *stream,int id,bool allowRead=true); - ~GwSerial(); + GwSerial(GwLog *logger,Stream *stream,int id,int type,bool allowRead=true); + virtual ~GwSerial(); bool isInitialized(); virtual size_t sendToClients(const char *buf,int sourceId,bool partial=false); virtual void loop(bool handleRead=true,bool handleWrite=true); virtual void readMessages(GwMessageFetcher *writer); - bool flush(long millis=200); + bool flush(); virtual Stream *getStream(bool partialWrites); bool getAvailableWrite(){return availableWrite;} + virtual void begin(unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1)=0; + virtual String getMode() override; friend GwSerialStream; }; + +template + class GwSerialImpl : public GwSerial{ + private: + unsigned long lastWritable=0; + template + void beginImpl(C *s,unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1){} + void beginImpl(HardwareSerial *s,unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1){ + s->begin(baud,config,rxPin,txPin); + } + template + void setError(C* s, GwLog *logger){} + void setError(HardwareSerial *s,GwLog *logger){ + LOG_DEBUG(GwLog::LOG,"enable serial errors for channel %d",id); + s->onReceiveError([logger,this](hardwareSerial_error_t err){ + LOG_DEBUG(GwLog::ERROR,"serial error on id %d: %d",this->id,(int)err); + }); + } + #if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3 + void beginImpl(HWCDC *s,unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1){ + s->begin(baud); + } + #endif + template + long getFlushTimeoutImpl(const C*){return 2000;} + #if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3 + long getFlushTimeoutImpl(HWCDC *){return 200;} + #endif + + template + int availableForWrite(C* c){ + return c->availableForWrite(); + } + + #if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3 + /** + * issue #81 + * workaround for the HWCDC beeing stuck at some point in time + * with availableForWrite == 0 but the ISR being disabled + * we simply give a small delay of 100ms for availableForWrite being 0 + * and afterwards retrigger the ISR + */ + int availableForWrite(HWCDC* c){ + int rt=c->availableForWrite(); + if (rt > 0) { + lastWritable=millis(); + return rt; + } + unsigned long now=millis(); + if (now > (lastWritable+USBCDC_RESTART_TIME)){ + lastWritable=now; + if (c->isConnected()){ + //this retriggers the ISR + usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY); + } + } + return rt; + } + #endif + + T *serial; + protected: + virtual long getFlushTimeout() override{ + return getFlushTimeoutImpl(serial); + } + virtual int availableForWrite(){ + return availableForWrite(serial); + } + public: + GwSerialImpl(GwLog* logger,T* s,int i,int type,bool allowRead=true): GwSerial(logger,s,i,type,allowRead),serial(s){} + virtual ~GwSerialImpl(){} + virtual void begin(unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1) override{ + beginImpl(serial,baud,config,rxPin,txPin); + setError(serial,logger); + }; + + }; + + #endif \ No newline at end of file diff --git a/lib/socketserver/GwSocketHelper.h b/lib/socketserver/GwSocketHelper.h index 8dea507..f153d45 100644 --- a/lib/socketserver/GwSocketHelper.h +++ b/lib/socketserver/GwSocketHelper.h @@ -17,4 +17,12 @@ class GwSocketHelper{ if (setsockopt(socket, IPPROTO_TCP, TCP_KEEPCNT, &val, sizeof(val)) != ESP_OK) return false; return true; } + static bool isMulticast(const String &addr){ + in_addr iaddr; + if (inet_pton(AF_INET,addr.c_str(),&iaddr) != 1) return false; + return IN_MULTICAST(ntohl(iaddr.s_addr)); + } + static bool equals(const in_addr &left, const in_addr &right){ + return left.s_addr == right.s_addr; + } }; \ No newline at end of file diff --git a/lib/socketserver/GwUdpReader.cpp b/lib/socketserver/GwUdpReader.cpp new file mode 100644 index 0000000..612eb10 --- /dev/null +++ b/lib/socketserver/GwUdpReader.cpp @@ -0,0 +1,167 @@ +#include "GwUdpReader.h" +#include +#include +#include "GwBuffer.h" +#include "GwSocketConnection.h" +#include "GwSocketHelper.h" +#include "GWWifi.h" + + +GwUdpReader::GwUdpReader(const GwConfigHandler *config, GwLog *logger, int minId) +{ + this->config = config; + this->logger = logger; + this->minId = minId; + port=config->getInt(GwConfigDefinitions::udprPort); + buffer= new GwBuffer(logger,GwBuffer::RX_BUFFER_SIZE,"udprd"); +} + +void GwUdpReader::createAndBind(){ + if (fd >= 0){ + ::close(fd); + } + if (currentStationIp.isEmpty() && (type == T_STA || type == T_MCSTA)) return; + fd=socket(AF_INET,SOCK_DGRAM,IPPROTO_IP); + if (fd < 0){ + LOG_ERROR("UDPR: unable to create udp socket: %d",errno); + return; + } + int enable = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); + if (type == T_STA) + { + if (inet_pton(AF_INET, currentStationIp.c_str(), &listenA.sin_addr) != 1) + { + LOG_ERROR("UDPR: invalid station ip address %s", currentStationIp.c_str()); + close(fd); + fd = -1; + return; + } + } + if (bind(fd,(struct sockaddr *)&listenA,sizeof(listenA)) < 0){ + LOG_ERROR("UDPR: unable to bind: %d",errno); + close(fd); + fd=-1; + return; + } + LOG_INFO("UDPR: socket created and bound"); + if (type != T_MCALL && type != T_MCAP && type != T_MCSTA) { + return; + } + struct ip_mreq mc; + mc.imr_multiaddr=listenA.sin_addr; + if (type == T_MCALL || type == T_MCAP){ + mc.imr_interface=apAddr; + int res=setsockopt(fd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mc,sizeof(mc)); + if (res != 0){ + LOG_ERROR("UDPR: unable to add MC membership for AP:%d",errno); + } + else{ + LOG_INFO("UDPR: membership for for AP"); + } + } + if (!currentStationIp.isEmpty() && (type == T_MCALL || type == T_MCSTA)) + { + mc.imr_interface = staAddr; + int res = setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mc, sizeof(mc)); + if (res != 0) + { + LOG_ERROR("UDPR: unable to add MC membership for STA:%d", errno); + } + else{ + LOG_INFO("UDPR: membership for STA %s",currentStationIp.c_str()); + } + } +} + +void GwUdpReader::begin() +{ + if (type != T_UNKNOWN) return; //already started + type=(UType)(config->getInt(GwConfigDefinitions::udprType)); + LOG_INFO("UDPR begin, mode=%d",(int)type); + port=config->getInt(GwConfigDefinitions::udprPort); + listenA.sin_family=AF_INET; + listenA.sin_port=htons(port); + listenA.sin_addr.s_addr=htonl(INADDR_ANY); //default + String ap=WiFi.softAPIP().toString(); + if (inet_pton(AF_INET, ap.c_str(), &apAddr) != 1) + { + LOG_ERROR("UDPR: invalid ap ip address %s", ap.c_str()); + return; + } + if (type == T_MCALL || type == T_MCAP || type == T_MCSTA){ + String mcAddr=config->getString(GwConfigDefinitions::udprMC); + if (inet_pton(AF_INET, mcAddr.c_str(), &listenA.sin_addr) != 1) + { + LOG_ERROR("UDPR: invalid mc address %s", mcAddr.c_str()); + close(fd); + fd = -1; + return; + } + LOG_INFO("UDPR: using multicast address %s",mcAddr.c_str()); + } + if (type == T_AP){ + listenA.sin_addr=apAddr; + } + String sta; + if (WiFi.isConnected()) sta=WiFi.localIP().toString(); + setStationAdd(sta); + createAndBind(); +} + +bool GwUdpReader::setStationAdd(const String &sta){ + if (sta == currentStationIp) return false; + currentStationIp=sta; + if (inet_pton(AF_INET, currentStationIp.c_str(), &staAddr) != 1){ + LOG_ERROR("UDPR: invalid station ip address %s", currentStationIp.c_str()); + return false; + } + LOG_INFO("UDPR: new station IP %s",currentStationIp.c_str()); + return true; +} +void GwUdpReader::loop(bool handleRead, bool handleWrite) +{ + if (handleRead){ + if (type == T_STA || type == T_MCALL || type == T_MCSTA){ + //only change anything if we considered the station IP + String nextStationIp; + if (WiFi.isConnected()){ + nextStationIp=WiFi.localIP().toString(); + } + if (setStationAdd(nextStationIp)){ + LOG_INFO("UDPR: wifi client IP changed, restart"); + createAndBind(); + } + } + } + +} + +void GwUdpReader::readMessages(GwMessageFetcher *writer) +{ + if (fd < 0) return; + //we expect one NMEA message in one UDP packet + buffer->reset(); + size_t rd=buffer->fillData(buffer->freeSpace(), + [this](uint8_t *rcvb,size_t rcvlen,void *param)->size_t{ + struct sockaddr_in from; + socklen_t fromLen=sizeof(from); + ssize_t res=recvfrom(fd,rcvb,rcvlen,MSG_DONTWAIT, + (struct sockaddr*)&from,&fromLen); + if (res <= 0) return 0; + if (GwSocketHelper::equals(from.sin_addr,apAddr)) return 0; + if (!currentStationIp.isEmpty() && (GwSocketHelper::equals(from.sin_addr,staAddr))) return 0; + return res; + },this); + if (buffer->usedSpace() > 0)(GwLog::DEBUG,"UDPR: received %d bytes",buffer->usedSpace()); + writer->handleBuffer(buffer); +} +size_t GwUdpReader::sendToClients(const char *buf, int source,bool partial) +{ + return 0; +} + + +GwUdpReader::~GwUdpReader() +{ +} \ No newline at end of file diff --git a/lib/socketserver/GwUdpReader.h b/lib/socketserver/GwUdpReader.h new file mode 100644 index 0000000..08c56bb --- /dev/null +++ b/lib/socketserver/GwUdpReader.h @@ -0,0 +1,45 @@ +#ifndef _GWUDPREADER_H +#define _GWUDPREADER_H +#include "GWConfig.h" +#include "GwLog.h" +#include "GwBuffer.h" +#include "GwChannelInterface.h" +#include +#include +#include + +class GwUdpReader: public GwChannelInterface{ + public: + using UType=enum{ + T_ALL=0, + T_AP=1, + T_STA=2, + T_MCALL=4, + T_MCAP=5, + T_MCSTA=6, + T_UNKNOWN=-1 + }; + private: + const GwConfigHandler *config; + GwLog *logger; + int minId; + int port; + int fd=-1; + struct sockaddr_in listenA; + String listenIp; + String currentStationIp; + struct in_addr apAddr; + struct in_addr staAddr; + UType type=T_UNKNOWN; + void createAndBind(); + bool setStationAdd(const String &sta); + GwBuffer *buffer=nullptr; + public: + GwUdpReader(const GwConfigHandler *config,GwLog *logger,int minId); + ~GwUdpReader(); + void begin(); + virtual void loop(bool handleRead=true,bool handleWrite=true); + virtual size_t sendToClients(const char *buf,int sourceId, bool partialWrite=false); + virtual void readMessages(GwMessageFetcher *writer); +}; +#endif \ No newline at end of file diff --git a/lib/socketserver/GwUdpWriter.cpp b/lib/socketserver/GwUdpWriter.cpp new file mode 100644 index 0000000..c91880e --- /dev/null +++ b/lib/socketserver/GwUdpWriter.cpp @@ -0,0 +1,203 @@ +#include "GwUdpWriter.h" +#include +#include +#include "GwBuffer.h" +#include "GwSocketConnection.h" +#include "GwSocketHelper.h" +#include "GWWifi.h" + +GwUdpWriter::WriterSocket::WriterSocket(GwLog *l,int p,const String &src,const String &dst, SourceMode sm) : + sourceMode(sm), source(src), destination(dst), port(p),logger(l) +{ + if (inet_pton(AF_INET, dst.c_str(), &dstA.sin_addr) != 1) + { + LOG_ERROR("UDPW: invalid destination ip address %s", dst.c_str()); + return; + } + if (sourceMode != SourceMode::S_UNBOUND) + { + if (inet_pton(AF_INET, src.c_str(), &srcA) != 1) + { + LOG_ERROR("UDPW: invalid source ip address %s", src.c_str()); + return; + } + } + dstA.sin_family=AF_INET; + dstA.sin_port=htons(port); + fd=socket(AF_INET,SOCK_DGRAM,IPPROTO_IP); + if (fd < 0){ + LOG_ERROR("UDPW: unable to create udp socket: %d",errno); + return; + } + int enable = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); + setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &enable, sizeof(int)); + switch (sourceMode) + { + case SourceMode::S_SRC: + { + sockaddr_in bindA; + bindA.sin_family = AF_INET; + bindA.sin_port = htons(0); // let system select + bindA.sin_addr = srcA; + if (bind(fd, (struct sockaddr *)&bindA, sizeof(bindA)) != 0) + { + LOG_ERROR("UDPW: bind failed for address %s: %d", source.c_str(), errno); + ::close(fd); + fd = -1; + return; + } + } + break; + case SourceMode::S_MC: + { + if (setsockopt(fd,IPPROTO_IP,IP_MULTICAST_IF,&srcA,sizeof(srcA)) != 0){ + LOG_ERROR("UDPW: unable to set MC source %s: %d",source.c_str(),errno); + ::close(fd); + fd=-1; + return; + } + int loop=0; + setsockopt(fd,IPPROTO_IP,IP_MULTICAST_LOOP,&loop,sizeof(loop)); + } + break; + default: + //not bound + break; + } +} +bool GwUdpWriter::WriterSocket::changed(const String &newSrc, const String &newDst){ + if (newDst != destination) return true; + if (sourceMode == SourceMode::S_UNBOUND) return false; + return newSrc != source; +} +size_t GwUdpWriter::WriterSocket::send(const char *buf,size_t len){ + if (fd < 0) return 0; + ssize_t err = sendto(fd,buf,len,0,(struct sockaddr *)&dstA, sizeof(dstA)); + if (err < 0){ + LOG_DEBUG(GwLog::DEBUG,"UDPW %s error sending: %d",destination.c_str(), errno); + return 0; + } + return err; +} + +GwUdpWriter::GwUdpWriter(const GwConfigHandler *config, GwLog *logger, int minId) +{ + this->config = config; + this->logger = logger; + this->minId = minId; + port=config->getInt(GwConfigDefinitions::udpwPort); + +} +void GwUdpWriter::checkStaSocket(){ + String src; + String bc; + if (type == T_BCAP || type == T_MCAP || type == T_NORM || type == T_UNKNOWN ) return; + bool connected=false; + if (WiFi.isConnected()){ + src=WiFi.localIP().toString(); + bc=WiFi.broadcastIP().toString(); + connected=true; + } + else{ + if (staSocket == nullptr) return; + } + String dst; + WriterSocket::SourceMode sm=WriterSocket::SourceMode::S_SRC; + switch (type){ + case T_BCALL: + case T_BCSTA: + sm=WriterSocket::SourceMode::S_SRC; + dst=bc; + break; + case T_MCALL: + case T_MCSTA: + dst=config->getString(GwConfigDefinitions::udpwMC); + sm=WriterSocket::SourceMode::S_MC; + break; + + } + if (staSocket != nullptr) + { + if (!connected || staSocket->changed(src, dst)) + { + staSocket->close(); + delete staSocket; + staSocket = nullptr; + LOG_INFO("changing/stopping UDPW(sta) socket"); + } + } + if (staSocket == nullptr && connected) + { + LOG_INFO("creating new UDP(sta) socket src=%s, dst=%s", src.c_str(), dst.c_str()); + staSocket = new WriterSocket(logger, port, src, dst, WriterSocket::SourceMode::S_SRC); + } +} + +void GwUdpWriter::begin() +{ + if (type != T_UNKNOWN) return; //already started + type=(UType)(config->getInt(GwConfigDefinitions::udpwType)); + LOG_INFO("UDPW begin, mode=%d",(int)type); + String src=WiFi.softAPIP().toString(); + String dst; + WriterSocket::SourceMode sm=WriterSocket::SourceMode::S_UNBOUND; + bool createApSocket=false; + switch(type){ + case T_BCALL: + case T_BCAP: + createApSocket=true; + dst=WiFi.softAPBroadcastIP().toString(); + sm=WriterSocket::SourceMode::S_SRC; + break; + case T_MCALL: + case T_MCAP: + createApSocket=true; + dst=config->getString(GwConfigDefinitions::udpwMC); + sm=WriterSocket::SourceMode::S_SRC; + break; + case T_NORM: + createApSocket=true; + dst=config->getString(GwConfigDefinitions::udpwAddress); + sm=WriterSocket::SourceMode::S_UNBOUND; + } + if (createApSocket){ + LOG_INFO("creating new UDPW(ap) socket src=%s, dst=%s", src.c_str(), dst.c_str()); + apSocket=new WriterSocket(logger,port,src,dst,sm); + } + checkStaSocket(); +} + +void GwUdpWriter::loop(bool handleRead, bool handleWrite) +{ + if (handleWrite){ + checkStaSocket(); + } + +} + +void GwUdpWriter::readMessages(GwMessageFetcher *writer) +{ + +} +size_t GwUdpWriter::sendToClients(const char *buf, int source,bool partial) +{ + if (source == minId) return 0; + size_t len=strlen(buf); + bool hasSent=false; + size_t res=0; + if (apSocket != nullptr){ + res=apSocket->send(buf,len); + if (res > 0) hasSent=true; + } + if (staSocket != nullptr){ + res=staSocket->send(buf,len); + if (res > 0) hasSent=true; + } + return hasSent?len:0; +} + + +GwUdpWriter::~GwUdpWriter() +{ +} \ No newline at end of file diff --git a/lib/socketserver/GwUdpWriter.h b/lib/socketserver/GwUdpWriter.h new file mode 100644 index 0000000..e17a17e --- /dev/null +++ b/lib/socketserver/GwUdpWriter.h @@ -0,0 +1,73 @@ +#ifndef _GWUDPWRITER_H +#define _GWUDPWRITER_H +#include "GWConfig.h" +#include "GwLog.h" +#include "GwBuffer.h" +#include "GwChannelInterface.h" +#include +#include +#include + +class GwUdpWriter: public GwChannelInterface{ + public: + using UType=enum{ + T_BCALL=0, + T_BCAP=1, + T_BCSTA=2, + T_NORM=3, + T_MCALL=4, + T_MCAP=5, + T_MCSTA=6, + T_UNKNOWN=-1 + }; + private: + class WriterSocket{ + public: + int fd=-1; + struct in_addr srcA; + struct sockaddr_in dstA; + String source; + String destination; + int port; + GwLog *logger; + using SourceMode=enum { + S_UNBOUND=0, + S_MC, + S_SRC + }; + SourceMode sourceMode; + WriterSocket(GwLog *logger,int p,const String &src,const String &dst, SourceMode sm); + void close(){ + if (fd > 0){ + ::close(fd); + } + fd=-1; + } + ~WriterSocket(){ + close(); + } + bool changed(const String &newSrc, const String &newDst); + size_t send(const char *buf,size_t len); + }; + const GwConfigHandler *config; + GwLog *logger; + /** + * we use fd/address to send to the AP network + * and fd2,address2 to send to the station network + * for type "normal" we only use fd + */ + WriterSocket *apSocket=nullptr; //also for T_NORM + WriterSocket *staSocket=nullptr; + int minId; + int port; + UType type=T_UNKNOWN; + void checkStaSocket(); + public: + GwUdpWriter(const GwConfigHandler *config,GwLog *logger,int minId); + ~GwUdpWriter(); + void begin(); + virtual void loop(bool handleRead=true,bool handleWrite=true); + virtual size_t sendToClients(const char *buf,int sourceId, bool partialWrite=false); + virtual void readMessages(GwMessageFetcher *writer); +}; +#endif \ No newline at end of file diff --git a/lib/usercode/GwUserCode.cpp b/lib/usercode/GwUserCode.cpp index 4522c92..211eda9 100644 --- a/lib/usercode/GwUserCode.cpp +++ b/lib/usercode/GwUserCode.cpp @@ -191,6 +191,7 @@ class TaskApi : public GwApiInternal SemaphoreHandle_t *mainLock; SemaphoreHandle_t localLock; std::map> counter; + std::map webHandlers; String name; bool counterUsed=false; int counterIdx=0; @@ -315,6 +316,10 @@ public: virtual bool addXdrMapping(const GwXDRMappingDef &def){ return api->addXdrMapping(def); } + virtual void registerRequestHandler(const String &url,HandlerFunction handler){ + GWSYNCHRONIZED(&localLock); + webHandlers[url]=handler; + } virtual void addCapability(const String &name, const String &value){ if (! isInit) return; userCapabilities[name]=value; @@ -335,6 +340,16 @@ public: virtual void setCalibrationValue(const String &name, double value){ api->setCalibrationValue(name,value); } + virtual bool handleWebRequest(const String &url,AsyncWebServerRequest *req){ + GWSYNCHRONIZED(&localLock); + auto it=webHandlers.find(url); + if (it == webHandlers.end()){ + api->getLogger()->logDebug(GwLog::LOG,"no web handler task=%s url=%s",name.c_str(),url.c_str()); + return false; + } + it->second(req); + return true; + } }; @@ -404,4 +419,19 @@ int GwUserCode::getJsonSize(){ } } return rt; +} +void GwUserCode::handleWebRequest(const String &url,AsyncWebServerRequest *req){ + int sep1=url.indexOf('/'); + String tname; + if (sep1 > 0){ + tname=url.substring(0,sep1); + for (auto &&it:userTasks){ + if (it.api && it.name == tname){ + if (it.api->handleWebRequest(url.substring(sep1+1),req)) return; + break; + } + } + } + LOG_DEBUG(GwLog::DEBUG,"no task found for web request %s[%s]",url.c_str(),tname.c_str()); + req->send(404, "text/plain", "not found"); } \ No newline at end of file diff --git a/lib/usercode/GwUserCode.h b/lib/usercode/GwUserCode.h index a218bc9..94e745d 100644 --- a/lib/usercode/GwUserCode.h +++ b/lib/usercode/GwUserCode.h @@ -11,6 +11,7 @@ class GwApiInternal : public GwApi{ ~GwApiInternal(){} virtual void fillStatus(GwJsonDocument &status){}; virtual int getJsonSize(){return 0;}; + virtual bool handleWebRequest(const String &url,AsyncWebServerRequest *req){return false;} }; class GwUserTask{ public: @@ -50,5 +51,6 @@ class GwUserCode{ Capabilities *getCapabilities(); void fillStatus(GwJsonDocument &status); int getJsonSize(); + void handleWebRequest(const String &url,AsyncWebServerRequest *); }; #endif \ No newline at end of file diff --git a/lib/xdrmappings/GwXDRMappings.cpp b/lib/xdrmappings/GwXDRMappings.cpp index b2eb0c2..ad26bc8 100644 --- a/lib/xdrmappings/GwXDRMappings.cpp +++ b/lib/xdrmappings/GwXDRMappings.cpp @@ -355,6 +355,7 @@ void GwXDRMappings::begin() GwXDRFoundMapping GwXDRMappings::selectMapping(GwXDRMapping::MappingList *list, int instance, const char *key) { GwXDRMapping *candidate = NULL; + unsigned long invalidTime=config->getInt(GwConfigDefinitions::timoSensor); for (auto mit = list->begin(); mit != list->end(); mit++) { GwXDRMappingDef *def = (*mit)->definition; @@ -369,7 +370,7 @@ GwXDRFoundMapping GwXDRMappings::selectMapping(GwXDRMapping::MappingList *list, { LOG_DEBUG(GwLog::DEBUG + 1, "selected mapping %s for %s, i=%d", def->toString().c_str(), key, instance); - return GwXDRFoundMapping(*mit, instance); + return GwXDRFoundMapping(*mit,invalidTime, instance); } if (instance < 0) { @@ -393,7 +394,7 @@ GwXDRFoundMapping GwXDRMappings::selectMapping(GwXDRMapping::MappingList *list, { LOG_DEBUG(GwLog::DEBUG + 1, "selected mapping %s for %s, i=%d", candidate->definition->toString().c_str(), key, instance); - return GwXDRFoundMapping(candidate, instance>=0?instance:candidate->definition->instanceId); + return GwXDRFoundMapping(candidate, invalidTime,instance>=0?instance:candidate->definition->instanceId); } LOG_DEBUG(GwLog::DEBUG + 1, "no instance mapping found for key=%s, i=%d", key, instance); return GwXDRFoundMapping(); @@ -472,8 +473,9 @@ String GwXDRMappings::getXdrEntry(String mapping, double value,int instance){ } GwXDRType *type = findType(code, &typeIndex); bool first=true; + unsigned long invalidTime=config->getInt(GwConfigDefinitions::timoSensor); while (type){ - GwXDRFoundMapping found(def,type); + GwXDRFoundMapping found(def,type,invalidTime); found.instanceId=instance; if (first) first=false; else rt+=","; diff --git a/lib/xdrmappings/GwXDRMappings.h b/lib/xdrmappings/GwXDRMappings.h index 246ade5..7643bee 100644 --- a/lib/xdrmappings/GwXDRMappings.h +++ b/lib/xdrmappings/GwXDRMappings.h @@ -167,15 +167,18 @@ class GwXDRFoundMapping : public GwBoatItemNameProvider{ GwXDRType *type=NULL; int instanceId=-1; bool empty=true; - GwXDRFoundMapping(GwXDRMappingDef *definition,GwXDRType *type){ + unsigned long timeout=0; + GwXDRFoundMapping(GwXDRMappingDef *definition,GwXDRType *type, unsigned long timeout){ this->definition=definition; this->type=type; + this->timeout=timeout; empty=false; } - GwXDRFoundMapping(GwXDRMapping* mapping,int instance=0){ + GwXDRFoundMapping(GwXDRMapping* mapping,unsigned long timeout,int instance){ this->definition=mapping->definition; this->type=mapping->type; this->instanceId=instance; + this->timeout=timeout; empty=false; } GwXDRFoundMapping(){} @@ -195,6 +198,9 @@ class GwXDRFoundMapping : public GwBoatItemNameProvider{ return "formatXdr:"+type->xdrtype+":"+type->boatDataUnit; }; virtual ~GwXDRFoundMapping(){} + virtual unsigned long getInvalidTime() override{ + return timeout; + } }; //the class GwXDRMappings is not intended to be deleted diff --git a/platformio.ini b/platformio.ini index f774c6b..df144b8 100644 --- a/platformio.ini +++ b/platformio.ini @@ -16,25 +16,28 @@ default_envs= extra_configs= lib/*task*/platformio.ini -[env] -platform = espressif32 @ 6.3.2 -framework = arduino -;platform_packages= -; framework-arduinoespressif32 @ 3.20011.230801 -; framework-espidf @ 3.50101.0 +[basedeps] lib_deps = - ttlappalainen/NMEA2000-library @ 4.18.9 - ttlappalainen/NMEA0183 @ 1.9.1 + ttlappalainen/NMEA2000-library @ 4.22.0 + ttlappalainen/NMEA0183 @ 1.10.1 ArduinoJson @ 6.18.5 AsyncTCP-esphome @ 2.0.1 ottowinter/ESPAsyncWebServer-esphome@2.0.1 - fastled/FastLED @ 3.6.0 FS Preferences ESPmDNS WiFi Update - + +[env] +platform = espressif32 @ 6.8.1 +framework = arduino +;platform_packages= +; framework-arduinoespressif32 @ 3.20017.0 +; framework-espidf @ 3.50101.0 +lib_deps = + ${basedeps.lib_deps} + fastled/FastLED @ 3.6.0 board_build.embed_files = diff --git a/src/main.cpp b/src/main.cpp index 599b340..ae8f3d5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -138,7 +138,7 @@ bool fixedApPass=true; #endif GwWifi gwWifi(&config,&logger,fixedApPass); GwChannelList channels(&logger,&config); -GwBoatData boatData(&logger); +GwBoatData boatData(&logger,&config); GwXDRMappings xdrMappings(&logger,&config); bool sendOutN2k=true; @@ -348,6 +348,8 @@ public: } return xdrMappings.addFixedMapping(mapping); } + virtual void registerRequestHandler(const String &url,HandlerFunction handler){ + } virtual void addCapability(const String &name, const String &value){} virtual bool addUserTask(GwUserTaskFunction task,const String Name, int stackSize=2000){ return false; @@ -768,6 +770,7 @@ void loopFunction(void *){ //delay(1); } } +const String USERPREFIX="/api/user/"; void setup() { mainLock=xSemaphoreCreateMutex(); uint8_t chipid[6]; @@ -784,6 +787,7 @@ void setup() { logger.prefix="FALLBACK:"; logger.setWriter(new DefaultLogWriter()); #endif + boatData.begin(); userCodeHandler.startInitTasks(MIN_USER_TASK); channels.preinit(); config.stopChanges(); @@ -844,13 +848,18 @@ void setup() { snprintf(buffer,29,"%g",value); buffer[29]=0; request->send(200,"text/plain",buffer); - }); + }); + webserver.registerHandler((USERPREFIX+"*").c_str(),[&USERPREFIX](AsyncWebServerRequest *req){ + String turl=req->url().substring(USERPREFIX.length()); + logger.logDebug(GwLog::DEBUG,"user web request for %s",turl.c_str()); + userCodeHandler.handleWebRequest(turl,req); + }); webserver.begin(); xdrMappings.begin(); logger.flush(); GwConverterConfig converterConfig; - converterConfig.init(&config); + converterConfig.init(&config,&logger); nmea0183Converter= N2kDataToNMEA0183::create(&logger, &boatData, [](const tNMEA0183Msg &msg, int sourceId){ SendNMEA0183Message(msg,sourceId,false); diff --git a/tools/flashtool.pyz b/tools/flashtool.pyz index e8ceb14..5dca3bb 100644 Binary files a/tools/flashtool.pyz and b/tools/flashtool.pyz differ diff --git a/tools/flashtool/esptool.py b/tools/flashtool/esptool.py index 01176af..d1d62b4 100755 --- a/tools/flashtool/esptool.py +++ b/tools/flashtool/esptool.py @@ -1,20 +1,8 @@ #!/usr/bin/env python # -# ESP8266 & ESP32 family ROM Bootloader Utility -# Copyright (C) 2014-2021 Fredrik Ahlberg, Angus Gratton, Espressif Systems (Shanghai) CO LTD, other contributors as noted. -# https://github.com/espressif/esptool +# SPDX-FileCopyrightText: 2014-2022 Fredrik Ahlberg, Angus Gratton, Espressif Systems (Shanghai) CO LTD, other contributors as noted. # -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation; either version 2 of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License along with -# this program; if not, write to the Free Software Foundation, Inc., 51 Franklin -# Street, Fifth Floor, Boston, MA 02110-1301 USA. +# SPDX-License-Identifier: GPL-2.0-or-later from __future__ import division, print_function @@ -27,6 +15,7 @@ import inspect import io import itertools import os +import re import shlex import string import struct @@ -68,7 +57,7 @@ except Exception: raise -__version__ = "3.1" +__version__ = "3.3.3" MAX_UINT32 = 0xffffffff MAX_UINT24 = 0xffffff @@ -84,6 +73,9 @@ ERASE_WRITE_TIMEOUT_PER_MB = 40 # timeout (per megabyte) for erasing and w MEM_END_ROM_TIMEOUT = 0.05 # special short timeout for ESP_MEM_END, as it may never respond DEFAULT_SERIAL_WRITE_TIMEOUT = 10 # timeout for serial port write DEFAULT_CONNECT_ATTEMPTS = 7 # default number of times to try connection +WRITE_BLOCK_ATTEMPTS = 3 # number of times to try writing a data block + +SUPPORTED_CHIPS = ['esp8266', 'esp32', 'esp32s2', 'esp32s3beta2', 'esp32s3', 'esp32c3', 'esp32c6beta', 'esp32h2beta1', 'esp32h2beta2', 'esp32c2'] def timeout_per_mb(seconds_per_mb, size_bytes): @@ -100,9 +92,12 @@ def _chip_to_rom_loader(chip): 'esp32': ESP32ROM, 'esp32s2': ESP32S2ROM, 'esp32s3beta2': ESP32S3BETA2ROM, - 'esp32s3beta3': ESP32S3BETA3ROM, + 'esp32s3': ESP32S3ROM, 'esp32c3': ESP32C3ROM, 'esp32c6beta': ESP32C6BETAROM, + 'esp32h2beta1': ESP32H2BETA1ROM, + 'esp32h2beta2': ESP32H2BETA2ROM, + 'esp32c2': ESP32C2ROM, }[chip] @@ -124,13 +119,37 @@ def get_default_connected_device(serial_list, port, connect_attempts, initial_ba if port is not None: raise print("%s failed to connect: %s" % (each_port, err)) + if _esp and _esp._port: + _esp._port.close() _esp = None return _esp -DETECTED_FLASH_SIZES = {0x12: '256KB', 0x13: '512KB', 0x14: '1MB', - 0x15: '2MB', 0x16: '4MB', 0x17: '8MB', - 0x18: '16MB', 0x19: '32MB', 0x1a: '64MB'} +DETECTED_FLASH_SIZES = { + 0x12: "256KB", + 0x13: "512KB", + 0x14: "1MB", + 0x15: "2MB", + 0x16: "4MB", + 0x17: "8MB", + 0x18: "16MB", + 0x19: "32MB", + 0x1A: "64MB", + 0x1B: "128MB", + 0x1C: "256MB", + 0x20: "64MB", + 0x21: "128MB", + 0x22: "256MB", + 0x32: "256KB", + 0x33: "512KB", + 0x34: "1MB", + 0x35: "2MB", + 0x36: "4MB", + 0x37: "8MB", + 0x38: "16MB", + 0x39: "32MB", + 0x3A: "64MB", +} def check_supported_function(func, check_func): @@ -139,8 +158,8 @@ def check_supported_function(func, check_func): bootloader function to check if it's supported. This is used to capture the multidimensional differences in - functionality between the ESP8266 & ESP32/32S2/32S3/32C3 ROM loaders, and the - software stub that runs on both. Not possible to do this cleanly + functionality between the ESP8266 & ESP32 (and later chips) ROM loaders, and the + software stub that runs on these. Not possible to do this cleanly via inheritance alone. """ def inner(*args, **kwargs): @@ -152,16 +171,26 @@ def check_supported_function(func, check_func): return inner +def esp8266_function_only(func): + """ Attribute for a function only supported on ESP8266 """ + return check_supported_function(func, lambda o: o.CHIP_NAME == "ESP8266") + + def stub_function_only(func): """ Attribute for a function only supported in the software stub loader """ return check_supported_function(func, lambda o: o.IS_STUB) def stub_and_esp32_function_only(func): - """ Attribute for a function only supported by software stubs or ESP32/32S2/32S3/32C3 ROM """ + """ Attribute for a function only supported by software stubs or ESP32 and later chips ROM """ return check_supported_function(func, lambda o: o.IS_STUB or isinstance(o, ESP32ROM)) +def esp32s3_or_newer_function_only(func): + """ Attribute for a function only supported by ESP32S3 and later chips ROM """ + return check_supported_function(func, lambda o: isinstance(o, ESP32S3ROM) or isinstance(o, ESP32C3ROM)) + + PYTHON2 = sys.version_info[0] < 3 # True if on pre-Python 3 # Function to return nth byte of a bitstring @@ -204,14 +233,9 @@ def _mask_to_shift(mask): return shift -def esp8266_function_only(func): - """ Attribute for a function only supported on ESP8266 """ - return check_supported_function(func, lambda o: o.CHIP_NAME == "ESP8266") - - class ESPLoader(object): """ Base class providing access to ESP ROM & software stub bootloaders. - Subclasses provide ESP8266 & ESP32 specific functionality. + Subclasses provide ESP8266 & ESP32 Family specific functionality. Don't instantiate this base class directly, either instantiate a subclass or call ESPLoader.detect_chip() which will interrogate the chip and return the @@ -221,8 +245,12 @@ class ESPLoader(object): CHIP_NAME = "Espressif device" IS_STUB = False + FPGA_SLOW_BOOT = False + DEFAULT_PORT = "/dev/ttyUSB0" + USES_RFC2217 = False + # Commands supported by ESP8266 ROM bootloader ESP_FLASH_BEGIN = 0x02 ESP_FLASH_DATA = 0x03 @@ -234,7 +262,7 @@ class ESPLoader(object): ESP_WRITE_REG = 0x09 ESP_READ_REG = 0x0a - # Some comands supported by ESP32 ROM bootloader (or -8266 w/ stub) + # Some comands supported by ESP32 and later chips ROM bootloader (or -8266 w/ stub) ESP_SPI_SET_PARAMS = 0x0B ESP_SPI_ATTACH = 0x0D ESP_READ_FLASH_SLOW = 0x0e # ROM only, much slower than the stub flash read @@ -244,7 +272,7 @@ class ESPLoader(object): ESP_FLASH_DEFL_END = 0x12 ESP_SPI_FLASH_MD5 = 0x13 - # Commands supported by ESP32-S2/S3/C3/C6 ROM bootloader only + # Commands supported by ESP32-S2 and later chips ROM bootloader only ESP_GET_SECURITY_INFO = 0x14 # Some commands supported by stub only @@ -295,6 +323,9 @@ class ESPLoader(object): # Device PIDs USB_JTAG_SERIAL_PID = 0x1001 + # Chip IDs that are no longer supported by esptool + UNSUPPORTED_CHIPS = {6: "ESP32-S3(beta 3)"} + def __init__(self, port=DEFAULT_PORT, baud=ESP_ROM_BAUD, trace_enabled=False): """Base constructor for ESPLoader bootloader interaction @@ -308,6 +339,7 @@ class ESPLoader(object): """ self.secure_download_mode = False # flag is set to True if esptool detects the ROM is in Secure Download Mode + self.stub_is_disabled = False # flag is set to True if esptool detects conditions which require the stub to be disabled if isinstance(port, basestring): self._port = serial.serial_for_url(port) @@ -343,36 +375,62 @@ class ESPLoader(object): connect_attempts=DEFAULT_CONNECT_ATTEMPTS): """ Use serial access to detect the chip type. - We use the UART's datecode register for this, it's mapped at - the same address on ESP8266 & ESP32 so we can use one - memory read and compare to the datecode register for each chip - type. + First, get_security_info command is sent to detect the ID of the chip + (supported only by ESP32-C3 and later, works even in the Secure Download Mode). + If this fails, we reconnect and fall-back to reading the magic number. + It's mapped at a specific ROM address and has a different value on each chip model. + This way we can use one memory read and compare it to the magic number for each chip type. This routine automatically performs ESPLoader.connect() (passing connect_mode parameter) as part of querying the chip. """ + inst = None detect_port = ESPLoader(port, baud, trace_enabled=trace_enabled) + if detect_port.serial_port.startswith("rfc2217:"): + detect_port.USES_RFC2217 = True detect_port.connect(connect_mode, connect_attempts, detecting=True) try: print('Detecting chip type...', end='') - sys.stdout.flush() - chip_magic_value = detect_port.read_reg(ESPLoader.CHIP_DETECT_MAGIC_REG_ADDR) + res = detect_port.check_command('get security info', ESPLoader.ESP_GET_SECURITY_INFO, b'') + res = struct.unpack(" 0 else itertools.count(): - last_error = self._connect_attempt(mode=mode, esp32r0_delay=False, usb_jtag_serial=usb_jtag_serial) - if last_error is None: - break - last_error = self._connect_attempt(mode=mode, esp32r0_delay=True, usb_jtag_serial=usb_jtag_serial) + for _, extra_delay in zip(range(attempts) if attempts > 0 else itertools.count(), itertools.cycle((False, True))): + last_error = self._connect_attempt(mode=mode, usb_jtag_serial=usb_jtag_serial, extra_delay=extra_delay) if last_error is None: break finally: print('') # end 'Connecting...' line if last_error is not None: - raise FatalError('Failed to connect to %s: %s' % (self.CHIP_NAME, last_error)) + raise FatalError('Failed to connect to {}: {}' + '\nFor troubleshooting steps visit: ' + 'https://docs.espressif.com/projects/esptool/en/latest/troubleshooting.html'.format(self.CHIP_NAME, last_error)) if not detecting: try: @@ -644,11 +703,12 @@ class ESPLoader(object): chip_magic_value = self.read_reg(ESPLoader.CHIP_DETECT_MAGIC_REG_ADDR) if chip_magic_value not in self.CHIP_DETECT_MAGIC_VALUE: actually = None - for cls in [ESP8266ROM, ESP32ROM, ESP32S2ROM, ESP32S3BETA2ROM, ESP32S3BETA3ROM, ESP32C3ROM]: + for cls in [ESP8266ROM, ESP32ROM, ESP32S2ROM, ESP32S3BETA2ROM, ESP32S3ROM, + ESP32C3ROM, ESP32H2BETA1ROM, ESP32H2BETA2ROM, ESP32C2ROM, ESP32C6BETAROM]: if chip_magic_value in cls.CHIP_DETECT_MAGIC_VALUE: actually = cls break - if actually is None: + if warnings and actually is None: print(("WARNING: This chip doesn't appear to be a %s (chip magic value 0x%08x). " "Probably it is unsupported by this version of esptool.") % (self.CHIP_NAME, chip_magic_value)) else: @@ -656,6 +716,7 @@ class ESPLoader(object): except UnsupportedCommandError: self.secure_download_mode = True self._post_connect() + self.check_chip_id() def _post_connect(self): """ @@ -751,7 +812,8 @@ class ESPLoader(object): timeout = timeout_per_mb(ERASE_REGION_TIMEOUT_PER_MB, size) # ROM performs the erase up front params = struct.pack(' 0: self.write_reg(SPI_MISO_DLEN_REG, miso_bits - 1) + flags = 0 + if dummy_len > 0: + flags |= (dummy_len - 1) + if addr_len > 0: + flags |= (addr_len - 1) << SPI_USR_ADDR_LEN_SHIFT + if flags: + self.write_reg(SPI_USR1_REG, flags) else: - def set_data_lengths(mosi_bits, miso_bits): SPI_DATA_LEN_REG = SPI_USR1_REG SPI_MOSI_BITLEN_S = 17 SPI_MISO_BITLEN_S = 8 mosi_mask = 0 if (mosi_bits == 0) else (mosi_bits - 1) miso_mask = 0 if (miso_bits == 0) else (miso_bits - 1) - self.write_reg(SPI_DATA_LEN_REG, - (miso_mask << SPI_MISO_BITLEN_S) | ( - mosi_mask << SPI_MOSI_BITLEN_S)) + flags = (miso_mask << SPI_MISO_BITLEN_S) | (mosi_mask << SPI_MOSI_BITLEN_S) + if dummy_len > 0: + flags |= (dummy_len - 1) + if addr_len > 0: + flags |= (addr_len - 1) << SPI_USR_ADDR_LEN_SHIFT + self.write_reg(SPI_DATA_LEN_REG, flags) # SPI peripheral "command" bitmasks for SPI_CMD_REG SPI_CMD_USR = (1 << 18) # shift values SPI_USR2_COMMAND_LEN_SHIFT = 28 + SPI_USR_ADDR_LEN_SHIFT = 26 if read_bits > 32: raise FatalError("Reading more than 32 bits back from a SPI flash operation is unsupported") @@ -1065,10 +1202,16 @@ class ESPLoader(object): flags |= SPI_USR_MISO if data_bits > 0: flags |= SPI_USR_MOSI + if addr_len > 0: + flags |= SPI_USR_ADDR + if dummy_len > 0: + flags |= SPI_USR_DUMMY set_data_lengths(data_bits, read_bits) self.write_reg(SPI_USR_REG, flags) self.write_reg(SPI_USR2_REG, (7 << SPI_USR2_COMMAND_LEN_SHIFT) | spiflash_command) + if addr and addr_len > 0: + self.write_reg(SPI_ADDR_REG, addr) if data_bits == 0: self.write_reg(SPI_W0_REG, 0) # clear data register before we read it else: @@ -1093,6 +1236,10 @@ class ESPLoader(object): self.write_reg(SPI_USR2_REG, old_spi_usr2) return status + def read_spiflash_sfdp(self, addr, read_bits): + CMD_RDSFDP = 0x5A + return self.run_spiflash_command(CMD_RDSFDP, read_bits=read_bits, addr=addr, addr_len=24, dummy_len=8) + def read_status(self, num_bytes=2): """Read up to 24 bits (num_bytes) of SPI flash status register contents via RDSR, RDSR2, RDSR3 commands @@ -1191,6 +1338,17 @@ class ESPLoader(object): # in the stub loader self.command(self.ESP_RUN_USER_CODE, wait_response=False) + def check_chip_id(self): + try: + chip_id = self.get_chip_id() + if chip_id != self.IMAGE_CHIP_ID: + print("WARNING: Chip ID {} ({}) doesn't match expected Chip ID {}. esptool may not work correctly." + .format(chip_id, self.UNSUPPORTED_CHIPS.get(chip_id, 'Unknown'), self.IMAGE_CHIP_ID)) + # Try to flash anyways by disabling stub + self.stub_is_disabled = True + except NotImplementedInROMError: + pass + class ESP8266ROM(ESPLoader): """ Access class for ESP8266 ROM bootloader @@ -1229,6 +1387,13 @@ class ESP8266ROM(ESPLoader): '16MB': 0x90, } + FLASH_FREQUENCY = { + '80m': 0xf, + '40m': 0x0, + '26m': 0x1, + '20m': 0x2, + } + BOOTLOADER_FLASH_OFFSET = 0 MEMORY_MAP = [[0x3FF00000, 0x3FF00010, "DPORT"], @@ -1366,6 +1531,8 @@ class ESP32ROM(ESPLoader): IMAGE_CHIP_ID = 0 IS_STUB = False + FPGA_SLOW_BOOT = True + CHIP_DETECT_MAGIC_VALUE = [0x00f01d83] IROM_MAP_START = 0x400d0000 @@ -1389,6 +1556,9 @@ class ESP32ROM(ESPLoader): EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = (1 << 7) # EFUSE_RD_DISABLE_DL_ENCRYPT DR_REG_SYSCON_BASE = 0x3ff66000 + APB_CTL_DATE_ADDR = DR_REG_SYSCON_BASE + 0x7C + APB_CTL_DATE_V = 0x1 + APB_CTL_DATE_S = 31 SPI_W0_OFFS = 0x80 @@ -1401,7 +1571,17 @@ class ESP32ROM(ESPLoader): '2MB': 0x10, '4MB': 0x20, '8MB': 0x30, - '16MB': 0x40 + '16MB': 0x40, + '32MB': 0x50, + '64MB': 0x60, + '128MB': 0x70 + } + + FLASH_FREQUENCY = { + '80m': 0xf, + '40m': 0x0, + '26m': 0x1, + '20m': 0x2, } BOOTLOADER_FLASH_OFFSET = 0x1000 @@ -1485,28 +1665,37 @@ class ESP32ROM(ESPLoader): pkg_version += ((word3 >> 2) & 0x1) << 3 return pkg_version - def get_chip_revision(self): - word3 = self.read_efuse(3) - word5 = self.read_efuse(5) - apb_ctl_date = self.read_reg(self.DR_REG_SYSCON_BASE + 0x7C) + # Returns new version format based on major and minor versions + def get_chip_full_revision(self): + return self.get_major_chip_version() * 100 + self.get_minor_chip_version() - rev_bit0 = (word3 >> 15) & 0x1 - rev_bit1 = (word5 >> 20) & 0x1 - rev_bit2 = (apb_ctl_date >> 31) & 0x1 - if rev_bit0: - if rev_bit1: - if rev_bit2: - return 3 - else: - return 2 - else: - return 1 - return 0 + # Returns old version format (ECO number). Use the new format get_chip_full_revision(). + def get_chip_revision(self): + return self.get_major_chip_version() + + def get_minor_chip_version(self): + return (self.read_efuse(5) >> 24) & 0x3 + + def get_major_chip_version(self): + rev_bit0 = (self.read_efuse(3) >> 15) & 0x1 + rev_bit1 = (self.read_efuse(5) >> 20) & 0x1 + apb_ctl_date = self.read_reg(self.APB_CTL_DATE_ADDR) + rev_bit2 = (apb_ctl_date >> self.APB_CTL_DATE_S) & self.APB_CTL_DATE_V + combine_value = (rev_bit2 << 2) | (rev_bit1 << 1) | rev_bit0 + + revision = { + 0: 0, + 1: 1, + 3: 2, + 7: 3, + }.get(combine_value, 0) + return revision def get_chip_description(self): pkg_version = self.get_pkg_version() - chip_revision = self.get_chip_revision() - rev3 = (chip_revision == 3) + major_rev = self.get_major_chip_version() + minor_rev = self.get_minor_chip_version() + rev3 = major_rev == 3 single_core = self.read_efuse(3) & (1 << 0) # CHIP_VER DIS_APP_CPU chip_name = { @@ -1516,13 +1705,14 @@ class ESP32ROM(ESPLoader): 4: "ESP32-U4WDH", 5: "ESP32-PICO-V3" if rev3 else "ESP32-PICO-D4", 6: "ESP32-PICO-V3-02", + 7: "ESP32-D0WDR2-V3", }.get(pkg_version, "unknown ESP32") # ESP32-D0WD-V3, ESP32-D0WDQ6-V3 if chip_name.startswith("ESP32-D0WD") and rev3: chip_name += "-V3" - return "%s (revision %d)" % (chip_name, chip_revision) + return "%s (revision v%d.%d)" % (chip_name, major_rev, minor_rev) def get_chip_features(self): features = ["WiFi"] @@ -1638,6 +1828,8 @@ class ESP32S2ROM(ESP32ROM): CHIP_NAME = "ESP32-S2" IMAGE_CHIP_ID = 2 + FPGA_SLOW_BOOT = False + IROM_MAP_START = 0x40080000 IROM_MAP_END = 0x40b80000 DROM_MAP_START = 0x3F000000 @@ -1662,6 +1854,8 @@ class ESP32S2ROM(ESP32ROM): # todo: use espefuse APIs to get this info EFUSE_BASE = 0x3f41A000 EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address + EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x044 + EFUSE_BLOCK2_ADDR = EFUSE_BASE + 0x05C EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34 EFUSE_PURPOSE_KEY0_SHIFT = 24 @@ -1706,21 +1900,50 @@ class ESP32S2ROM(ESP32ROM): [0x40080000, 0x40800000, "IROM"], [0x50000000, 0x50002000, "RTC_DATA"]] + # Returns old version format (ECO number). Use the new format get_chip_full_revision(). + def get_chip_revision(self): + return self.get_major_chip_version() + def get_pkg_version(self): + num_word = 4 + return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 0) & 0x0F + + def get_minor_chip_version(self): + hi_num_word = 3 + hi = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * hi_num_word)) >> 20) & 0x01 + low_num_word = 4 + low = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * low_num_word)) >> 4) & 0x07 + return (hi << 3) + low + + def get_major_chip_version(self): num_word = 3 - block1_addr = self.EFUSE_BASE + 0x044 - word3 = self.read_reg(block1_addr + (4 * num_word)) - pkg_version = (word3 >> 21) & 0x0F - return pkg_version + return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 18) & 0x03 + + def get_flash_version(self): + num_word = 3 + return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x0F + + def get_psram_version(self): + num_word = 3 + return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 28) & 0x0F + + def get_block2_version(self): + # BLK_VERSION_MINOR + num_word = 4 + return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 4) & 0x07 def get_chip_description(self): chip_name = { 0: "ESP32-S2", - 1: "ESP32-S2FH16", - 2: "ESP32-S2FH32", - }.get(self.get_pkg_version(), "unknown ESP32-S2") + 1: "ESP32-S2FH2", + 2: "ESP32-S2FH4", + 102: "ESP32-S2FNR2", + 100: "ESP32-S2R2", + }.get(self.get_flash_version() + self.get_psram_version() * 100, "unknown ESP32-S2") - return "%s" % (chip_name) + major_rev = self.get_major_chip_version() + minor_rev = self.get_minor_chip_version() + return "%s (revision v%d.%d)" % (chip_name, major_rev, minor_rev) def get_chip_features(self): features = ["WiFi"] @@ -1728,22 +1951,27 @@ class ESP32S2ROM(ESP32ROM): if self.secure_download_mode: features += ["Secure Download Mode Enabled"] - pkg_version = self.get_pkg_version() + flash_version = { + 0: "No Embedded Flash", + 1: "Embedded Flash 2MB", + 2: "Embedded Flash 4MB", + }.get(self.get_flash_version(), "Unknown Embedded Flash") + features += [flash_version] - if pkg_version in [1, 2]: - if pkg_version == 1: - features += ["Embedded 2MB Flash"] - elif pkg_version == 2: - features += ["Embedded 4MB Flash"] - features += ["105C temp rating"] + psram_version = { + 0: "No Embedded PSRAM", + 1: "Embedded PSRAM 2MB", + 2: "Embedded PSRAM 4MB", + }.get(self.get_psram_version(), "Unknown Embedded PSRAM") + features += [psram_version] - num_word = 4 - block2_addr = self.EFUSE_BASE + 0x05C - word4 = self.read_reg(block2_addr + (4 * num_word)) - block2_version = (word4 >> 4) & 0x07 + block2_version = { + 0: "No calibration in BLK2 of efuse", + 1: "ADC and temperature sensor calibration in BLK2 of efuse V1", + 2: "ADC and temperature sensor calibration in BLK2 of efuse V2", + }.get(self.get_block2_version(), "Unknown Calibration in BLK2") + features += [block2_version] - if block2_version == 1: - features += ["ADC and temperature sensor calibration in BLK2 of efuse"] return features def get_crystal_freq(self): @@ -1810,16 +2038,17 @@ class ESP32S2ROM(ESP32ROM): strap_reg = self.read_reg(self.GPIO_STRAP_REG) force_dl_reg = self.read_reg(self.RTC_CNTL_OPTION1_REG) if strap_reg & self.GPIO_STRAP_SPI_BOOT_MASK == 0 and force_dl_reg & self.RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK == 0: - print("ERROR: {} chip was placed into download mode using GPIO0.\n" + print("WARNING: {} chip was placed into download mode using GPIO0.\n" "esptool.py can not exit the download mode over USB. " "To run the app, reset the chip manually.\n" - "To suppress this error, set --after option to 'no_reset'.".format(self.get_chip_description())) + "To suppress this note, set --after option to 'no_reset'.".format(self.get_chip_description())) raise SystemExit(1) def hard_reset(self): if self.uses_usb(): self._check_if_can_reset() + print('Hard resetting via RTS pin...') self._setRTS(True) # EN->LOW if self.uses_usb(): # Give the chip some time to come out of reset, to be able to handle further DTR/RTS transitions @@ -1827,12 +2056,21 @@ class ESP32S2ROM(ESP32ROM): self._setRTS(False) time.sleep(0.2) else: + time.sleep(0.1) self._setRTS(False) class ESP32S3ROM(ESP32ROM): CHIP_NAME = "ESP32-S3" + IMAGE_CHIP_ID = 9 + + CHIP_DETECT_MAGIC_VALUE = [0x9] + + BOOTLOADER_FLASH_OFFSET = 0x0 + + FPGA_SLOW_BOOT = False + IROM_MAP_START = 0x42000000 IROM_MAP_END = 0x44000000 DROM_MAP_START = 0x3c000000 @@ -1851,9 +2089,10 @@ class ESP32S3ROM(ESP32ROM): FLASH_ENCRYPTED_WRITE_ALIGN = 16 # todo: use espefuse APIs to get this info - EFUSE_BASE = 0x6001A000 # BLOCK0 read base address + EFUSE_BASE = 0x60007000 # BLOCK0 read base address MAC_EFUSE_REG = EFUSE_BASE + 0x044 - + EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x44 + EFUSE_BLOCK2_ADDR = EFUSE_BASE + 0x5C EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34 @@ -1876,9 +2115,17 @@ class ESP32S3ROM(ESP32ROM): PURPOSE_VAL_XTS_AES256_KEY_2 = 3 PURPOSE_VAL_XTS_AES128_KEY = 4 - UART_CLKDIV_REG = 0x60000014 + UARTDEV_BUF_NO = 0x3fcef14c # Variable in ROM .bss which indicates the port in use + UARTDEV_BUF_NO_USB = 3 # Value of the above variable indicating that USB is in use + + USB_RAM_BLOCK = 0x800 # Max block size USB CDC is used GPIO_STRAP_REG = 0x60004038 + GPIO_STRAP_SPI_BOOT_MASK = 0x8 # Not download mode + RTC_CNTL_OPTION1_REG = 0x6000812C + RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK = 0x1 # Is download mode forced over USB? + + UART_CLKDIV_REG = 0x60000014 MEMORY_MAP = [[0x00000000, 0x00010000, "PADDING"], [0x3C000000, 0x3D000000, "DROM"], @@ -1893,8 +2140,57 @@ class ESP32S3ROM(ESP32ROM): [0x42000000, 0x42800000, "IROM"], [0x50000000, 0x50002000, "RTC_DATA"]] + # Returns old version format (ECO number). Use the new format get_chip_full_revision(). + def get_chip_revision(self): + return self.get_minor_chip_version() + + def get_pkg_version(self): + num_word = 3 + return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x07 + + def is_eco0(self, minor_raw): + # Workaround: The major version field was allocated to other purposes + # when block version is v1.1. + # Luckily only chip v0.0 have this kind of block version and efuse usage. + return ( + (minor_raw & 0x7) == 0 and self.get_blk_version_major() == 1 and self.get_blk_version_minor() == 1 + ) + + def get_minor_chip_version(self): + minor_raw = self.get_raw_minor_chip_version() + if self.is_eco0(minor_raw): + return 0 + return minor_raw + + def get_raw_minor_chip_version(self): + hi_num_word = 5 + hi = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * hi_num_word)) >> 23) & 0x01 + low_num_word = 3 + low = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * low_num_word)) >> 18) & 0x07 + return (hi << 3) + low + + def get_blk_version_major(self): + num_word = 4 + return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 0) & 0x03 + + def get_blk_version_minor(self): + num_word = 3 + return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 24) & 0x07 + + def get_major_chip_version(self): + minor_raw = self.get_raw_minor_chip_version() + if self.is_eco0(minor_raw): + return 0 + return self.get_raw_major_chip_version() + + def get_raw_major_chip_version(self): + num_word = 5 + return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 24) & 0x03 + def get_chip_description(self): - return "ESP32-S3" + major_rev = self.get_major_chip_version() + minor_rev = self.get_minor_chip_version() + return "%s (revision v%d.%d)" % (self.CHIP_NAME, major_rev, minor_rev) def get_chip_features(self): return ["WiFi", "BLE"] @@ -1940,6 +2236,50 @@ class ESP32S3ROM(ESP32ROM): except TypeError: # Python 3, bitstring elements are already bytes return tuple(bitstring) + def uses_usb(self, _cache=[]): + if self.secure_download_mode: + return False # can't detect native USB in secure download mode + if not _cache: + buf_no = self.read_reg(self.UARTDEV_BUF_NO) & 0xff + _cache.append(buf_no == self.UARTDEV_BUF_NO_USB) + return _cache[0] + + def _post_connect(self): + if self.uses_usb(): + self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK + + def _check_if_can_reset(self): + """ + Check the strapping register to see if we can reset out of download mode. + """ + if os.getenv("ESPTOOL_TESTING") is not None: + print("ESPTOOL_TESTING is set, ignoring strapping mode check") + # Esptool tests over USB CDC run with GPIO0 strapped low, don't complain in this case. + return + strap_reg = self.read_reg(self.GPIO_STRAP_REG) + force_dl_reg = self.read_reg(self.RTC_CNTL_OPTION1_REG) + if strap_reg & self.GPIO_STRAP_SPI_BOOT_MASK == 0 and force_dl_reg & self.RTC_CNTL_FORCE_DOWNLOAD_BOOT_MASK == 0: + print("WARNING: {} chip was placed into download mode using GPIO0.\n" + "esptool.py can not exit the download mode over USB. " + "To run the app, reset the chip manually.\n" + "To suppress this note, set --after option to 'no_reset'.".format(self.get_chip_description())) + raise SystemExit(1) + + def hard_reset(self): + if self.uses_usb(): + self._check_if_can_reset() + + print('Hard resetting via RTS pin...') + self._setRTS(True) # EN->LOW + if self.uses_usb(): + # Give the chip some time to come out of reset, to be able to handle further DTR/RTS transitions + time.sleep(0.2) + self._setRTS(False) + time.sleep(0.2) + else: + time.sleep(0.1) + self._setRTS(False) + class ESP32S3BETA2ROM(ESP32S3ROM): CHIP_NAME = "ESP32-S3(beta2)" @@ -1947,24 +2287,20 @@ class ESP32S3BETA2ROM(ESP32S3ROM): CHIP_DETECT_MAGIC_VALUE = [0xeb004136] - def get_chip_description(self): - return "ESP32-S3(beta2)" - - -class ESP32S3BETA3ROM(ESP32S3ROM): - CHIP_NAME = "ESP32-S3(beta3)" - IMAGE_CHIP_ID = 6 - - CHIP_DETECT_MAGIC_VALUE = [0x9] + EFUSE_BASE = 0x6001A000 # BLOCK0 read base address def get_chip_description(self): - return "ESP32-S3(beta3)" + major_rev = self.get_major_chip_version() + minor_rev = self.get_minor_chip_version() + return "%s (revision v%d.%d)" % (self.CHIP_NAME, major_rev, minor_rev) class ESP32C3ROM(ESP32ROM): CHIP_NAME = "ESP32-C3" IMAGE_CHIP_ID = 5 + FPGA_SLOW_BOOT = False + IROM_MAP_START = 0x42000000 IROM_MAP_END = 0x42800000 DROM_MAP_START = 0x3c000000 @@ -1986,6 +2322,7 @@ class ESP32C3ROM(ESP32ROM): UART_DATE_REG_ADDR = 0x60000000 + 0x7c EFUSE_BASE = 0x60008800 + EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x044 MAC_EFUSE_REG = EFUSE_BASE + 0x044 EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address @@ -2024,27 +2361,32 @@ class ESP32C3ROM(ESP32ROM): [0x50000000, 0x50002000, "RTC_DRAM"], [0x600FE000, 0x60100000, "MEM_INTERNAL2"]] + # Returns old version format (ECO number). Use the new format get_chip_full_revision(). + def get_chip_revision(self): + return self.get_minor_chip_version() + def get_pkg_version(self): num_word = 3 - block1_addr = self.EFUSE_BASE + 0x044 - word3 = self.read_reg(block1_addr + (4 * num_word)) - pkg_version = (word3 >> 21) & 0x0F - return pkg_version + return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x07 - def get_chip_revision(self): - # reads WAFER_VERSION field from EFUSE_RD_MAC_SPI_SYS_3_REG - block1_addr = self.EFUSE_BASE + 0x044 - num_word = 3 - pos = 18 - return (self.read_reg(block1_addr + (4 * num_word)) & (0x7 << pos)) >> pos + def get_minor_chip_version(self): + hi_num_word = 5 + hi = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * hi_num_word)) >> 23) & 0x01 + low_num_word = 3 + low = (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * low_num_word)) >> 18) & 0x07 + return (hi << 3) + low + + def get_major_chip_version(self): + num_word = 5 + return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 24) & 0x03 def get_chip_description(self): chip_name = { 0: "ESP32-C3", }.get(self.get_pkg_version(), "unknown ESP32-C3") - chip_revision = self.get_chip_revision() - - return "%s (revision %d)" % (chip_name, chip_revision) + major_rev = self.get_major_chip_version() + minor_rev = self.get_minor_chip_version() + return "%s (revision v%d.%d)" % (chip_name, major_rev, minor_rev) def get_chip_features(self): return ["Wi-Fi"] @@ -2087,21 +2429,226 @@ class ESP32C3ROM(ESP32ROM): return any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes) +class ESP32H2BETA1ROM(ESP32ROM): + CHIP_NAME = "ESP32-H2(beta1)" + IMAGE_CHIP_ID = 10 + + IROM_MAP_START = 0x42000000 + IROM_MAP_END = 0x42800000 + DROM_MAP_START = 0x3c000000 + DROM_MAP_END = 0x3c800000 + + SPI_REG_BASE = 0x60002000 + SPI_USR_OFFS = 0x18 + SPI_USR1_OFFS = 0x1C + SPI_USR2_OFFS = 0x20 + SPI_MOSI_DLEN_OFFS = 0x24 + SPI_MISO_DLEN_OFFS = 0x28 + SPI_W0_OFFS = 0x58 + + BOOTLOADER_FLASH_OFFSET = 0x0 + + CHIP_DETECT_MAGIC_VALUE = [0xca26cc22] + + UART_DATE_REG_ADDR = 0x60000000 + 0x7c + + EFUSE_BASE = 0x6001A000 + EFUSE_BLOCK1_ADDR = EFUSE_BASE + 0x044 + MAC_EFUSE_REG = EFUSE_BASE + 0x044 + + EFUSE_RD_REG_BASE = EFUSE_BASE + 0x030 # BLOCK0 read base address + + EFUSE_PURPOSE_KEY0_REG = EFUSE_BASE + 0x34 + EFUSE_PURPOSE_KEY0_SHIFT = 24 + EFUSE_PURPOSE_KEY1_REG = EFUSE_BASE + 0x34 + EFUSE_PURPOSE_KEY1_SHIFT = 28 + EFUSE_PURPOSE_KEY2_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY2_SHIFT = 0 + EFUSE_PURPOSE_KEY3_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY3_SHIFT = 4 + EFUSE_PURPOSE_KEY4_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY4_SHIFT = 8 + EFUSE_PURPOSE_KEY5_REG = EFUSE_BASE + 0x38 + EFUSE_PURPOSE_KEY5_SHIFT = 12 + + EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT_REG = EFUSE_RD_REG_BASE + EFUSE_DIS_DOWNLOAD_MANUAL_ENCRYPT = 1 << 20 + + PURPOSE_VAL_XTS_AES128_KEY = 4 + + GPIO_STRAP_REG = 0x3f404038 + + FLASH_ENCRYPTED_WRITE_ALIGN = 16 + + MEMORY_MAP = [] + + FLASH_FREQUENCY = { + '48m': 0xf, + '24m': 0x0, + '16m': 0x1, + '12m': 0x2, + } + + # Returns old version format (ECO number). Use the new format get_chip_full_revision(). + def get_chip_revision(self): + return 0 + + def get_pkg_version(self): + num_word = 4 + return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 0) & 0x07 + + def get_minor_chip_version(self): + num_word = 3 + return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 18) & 0x07 + + def get_major_chip_version(self): + num_word = 3 + return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 21) & 0x03 + + def get_chip_description(self): + chip_name = { + 0: "ESP32-H2", + }.get(self.get_pkg_version(), "unknown ESP32-H2") + major_rev = self.get_major_chip_version() + minor_rev = self.get_minor_chip_version() + return "%s (revision v%d.%d)" % (chip_name, major_rev, minor_rev) + + def get_chip_features(self): + return ["BLE/802.15.4"] + + def get_crystal_freq(self): + return 32 + + def override_vddsdio(self, new_voltage): + raise NotImplementedInROMError("VDD_SDIO overrides are not supported for ESP32-H2") + + def read_mac(self): + mac0 = self.read_reg(self.MAC_EFUSE_REG) + mac1 = self.read_reg(self.MAC_EFUSE_REG + 4) # only bottom 16 bits are MAC + bitstring = struct.pack(">II", mac1, mac0)[2:] + try: + return tuple(ord(b) for b in bitstring) + except TypeError: # Python 3, bitstring elements are already bytes + return tuple(bitstring) + + def get_flash_crypt_config(self): + return None # doesn't exist on ESP32-H2 + + def get_key_block_purpose(self, key_block): + if key_block < 0 or key_block > 5: + raise FatalError("Valid key block numbers must be in range 0-5") + + reg, shift = [(self.EFUSE_PURPOSE_KEY0_REG, self.EFUSE_PURPOSE_KEY0_SHIFT), + (self.EFUSE_PURPOSE_KEY1_REG, self.EFUSE_PURPOSE_KEY1_SHIFT), + (self.EFUSE_PURPOSE_KEY2_REG, self.EFUSE_PURPOSE_KEY2_SHIFT), + (self.EFUSE_PURPOSE_KEY3_REG, self.EFUSE_PURPOSE_KEY3_SHIFT), + (self.EFUSE_PURPOSE_KEY4_REG, self.EFUSE_PURPOSE_KEY4_SHIFT), + (self.EFUSE_PURPOSE_KEY5_REG, self.EFUSE_PURPOSE_KEY5_SHIFT)][key_block] + return (self.read_reg(reg) >> shift) & 0xF + + def is_flash_encryption_key_valid(self): + # Need to see an AES-128 key + purposes = [self.get_key_block_purpose(b) for b in range(6)] + + return any(p == self.PURPOSE_VAL_XTS_AES128_KEY for p in purposes) + + +class ESP32H2BETA2ROM(ESP32H2BETA1ROM): + CHIP_NAME = "ESP32-H2(beta2)" + IMAGE_CHIP_ID = 14 + + def get_chip_description(self): + major_rev = self.get_major_chip_version() + minor_rev = self.get_minor_chip_version() + return "%s (revision v%d.%d)" % (self.CHIP_NAME, major_rev, minor_rev) + + +class ESP32C2ROM(ESP32C3ROM): + CHIP_NAME = "ESP32-C2" + IMAGE_CHIP_ID = 12 + + IROM_MAP_START = 0x42000000 + IROM_MAP_END = 0x42400000 + DROM_MAP_START = 0x3c000000 + DROM_MAP_END = 0x3c400000 + + # Magic value for ESP32C2 ECO0 and ECO1 respectively + CHIP_DETECT_MAGIC_VALUE = [0x6F51306F, 0x7c41a06f] + + EFUSE_BASE = 0x60008800 + EFUSE_BLOCK2_ADDR = EFUSE_BASE + 0x040 + MAC_EFUSE_REG = EFUSE_BASE + 0x040 + + FLASH_FREQUENCY = { + '60m': 0xf, + '30m': 0x0, + '20m': 0x1, + '15m': 0x2, + } + + # Returns old version format (ECO number). Use the new format get_chip_full_revision(). + def get_chip_revision(self): + return self.get_major_chip_version() + + def get_pkg_version(self): + num_word = 1 + return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 22) & 0x07 + + def get_chip_description(self): + chip_name = { + 0: "ESP32-C2", + 1: "ESP32-C2", + }.get(self.get_pkg_version(), "unknown ESP32-C2") + major_rev = self.get_major_chip_version() + minor_rev = self.get_minor_chip_version() + return "%s (revision v%d.%d)" % (chip_name, major_rev, minor_rev) + + def get_minor_chip_version(self): + num_word = 1 + return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 16) & 0xF + + def get_major_chip_version(self): + num_word = 1 + return (self.read_reg(self.EFUSE_BLOCK2_ADDR + (4 * num_word)) >> 20) & 0x3 + + def _post_connect(self): + # ESP32C2 ECO0 is no longer supported by the flasher stub + if self.get_chip_revision() == 0: + self.stub_is_disabled = True + self.IS_STUB = False + + class ESP32C6BETAROM(ESP32C3ROM): - CHIP_NAME = "ESP32-C6 BETA" + CHIP_NAME = "ESP32-C6(beta)" IMAGE_CHIP_ID = 7 CHIP_DETECT_MAGIC_VALUE = [0x0da1806f] UART_DATE_REG_ADDR = 0x00000500 + # Returns old version format (ECO number). Use the new format get_chip_full_revision(). + def get_chip_revision(self): + return 0 + + def get_pkg_version(self): + num_word = 3 + return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 29) & 0x07 + + def get_minor_chip_version(self): + num_word = 3 + return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 18) & 0x0F + + def get_major_chip_version(self): + num_word = 3 + return (self.read_reg(self.EFUSE_BLOCK1_ADDR + (4 * num_word)) >> 22) & 0x03 + def get_chip_description(self): chip_name = { 0: "ESP32-C6", }.get(self.get_pkg_version(), "unknown ESP32-C6") - chip_revision = self.get_chip_revision() - - return "%s (revision %d)" % (chip_name, chip_revision) + major_rev = self.get_major_chip_version() + minor_rev = self.get_minor_chip_version() + return "%s (revision v%d.%d)" % (chip_name, major_rev, minor_rev) class ESP32StubLoader(ESP32ROM): @@ -2165,7 +2712,7 @@ class ESP32S3BETA2StubLoader(ESP32S3BETA2ROM): ESP32S3BETA2ROM.STUB_CLASS = ESP32S3BETA2StubLoader -class ESP32S3BETA3StubLoader(ESP32S3BETA3ROM): +class ESP32S3StubLoader(ESP32S3ROM): """ Access class for ESP32S3 stub loader, runs on top of ROM. (Basically the same as ESP32StubLoader, but different base class. @@ -2181,8 +2728,12 @@ class ESP32S3BETA3StubLoader(ESP32S3BETA3ROM): self._trace_enabled = rom_loader._trace_enabled self.flush_input() # resets _slip_reader + if rom_loader.uses_usb(): + self.ESP_RAM_BLOCK = self.USB_RAM_BLOCK + self.FLASH_WRITE_SIZE = self.USB_RAM_BLOCK -ESP32S3BETA3ROM.STUB_CLASS = ESP32S3BETA3StubLoader + +ESP32S3ROM.STUB_CLASS = ESP32S3StubLoader class ESP32C3StubLoader(ESP32C3ROM): @@ -2205,6 +2756,66 @@ class ESP32C3StubLoader(ESP32C3ROM): ESP32C3ROM.STUB_CLASS = ESP32C3StubLoader +class ESP32H2BETA1StubLoader(ESP32H2BETA1ROM): + """ Access class for ESP32H2BETA1 stub loader, runs on top of ROM. + + (Basically the same as ESP32StubLoader, but different base class. + Can possibly be made into a mixin.) + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM + IS_STUB = True + + def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader + + +ESP32H2BETA1ROM.STUB_CLASS = ESP32H2BETA1StubLoader + + +class ESP32H2BETA2StubLoader(ESP32H2BETA2ROM): + """ Access class for ESP32H2BETA2 stub loader, runs on top of ROM. + + (Basically the same as ESP32StubLoader, but different base class. + Can possibly be made into a mixin.) + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM + IS_STUB = True + + def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader + + +ESP32H2BETA2ROM.STUB_CLASS = ESP32H2BETA2StubLoader + + +class ESP32C2StubLoader(ESP32C2ROM): + """ Access class for ESP32C2 stub loader, runs on top of ROM. + + (Basically the same as ESP32StubLoader, but different base class. + Can possibly be made into a mixin.) + """ + FLASH_WRITE_SIZE = 0x4000 # matches MAX_WRITE_BLOCK in stub_loader.c + STATUS_BYTES_LENGTH = 2 # same as ESP8266, different to ESP32 ROM + IS_STUB = True + + def __init__(self, rom_loader): + self.secure_download_mode = rom_loader.secure_download_mode + self._port = rom_loader._port + self._trace_enabled = rom_loader._trace_enabled + self.flush_input() # resets _slip_reader + + +ESP32C2ROM.STUB_CLASS = ESP32C2StubLoader + + class ESPBOOTLOADER(object): """ These are constants related to software ESP8266 bootloader, working with 'v2' image files """ @@ -2223,7 +2834,7 @@ def LoadFirmwareImage(chip, filename): Returns a BaseFirmwareImage subclass, either ESP8266ROMFirmwareImage (v1) or ESP8266V2FirmwareImage (v2). """ - chip = chip.lower().replace("-", "") + chip = re.sub(r"[-()]", "", chip.lower()) with open(filename, 'rb') as f: if chip == 'esp32': return ESP32FirmwareImage(f) @@ -2231,12 +2842,18 @@ def LoadFirmwareImage(chip, filename): return ESP32S2FirmwareImage(f) elif chip == "esp32s3beta2": return ESP32S3BETA2FirmwareImage(f) - elif chip == "esp32s3beta3": - return ESP32S3BETA3FirmwareImage(f) + elif chip == "esp32s3": + return ESP32S3FirmwareImage(f) elif chip == 'esp32c3': return ESP32C3FirmwareImage(f) elif chip == 'esp32c6beta': return ESP32C6BETAFirmwareImage(f) + elif chip == 'esp32h2beta1': + return ESP32H2BETA1FirmwareImage(f) + elif chip == 'esp32h2beta2': + return ESP32H2BETA2FirmwareImage(f) + elif chip == 'esp32c2': + return ESP32C2FirmwareImage(f) else: # Otherwise, ESP8266 so look at magic to determine the image type magic = ord(f.read(1)) f.seek(0) @@ -2314,6 +2931,7 @@ class BaseFirmwareImage(object): self.entrypoint = 0 self.elf_sha256 = None self.elf_sha256_offset = 0 + self.pad_to_size = 0 def load_common_header(self, load_file, expected_magic): (magic, segments, self.flash_mode, self.flash_size_freq, self.entrypoint) = struct.unpack(' ESP8266ROM.IROM_MAP_START) + + def save(self, filename): + total_segments = 0 + with io.BytesIO() as f: # write file to memory first + self.write_common_header(f, self.segments) + + checksum = ESPLoader.ESP_CHECKSUM_MAGIC + + # split segments into flash-mapped vs ram-loaded, and take copies so we can mutate them + flash_segments = [copy.deepcopy(s) for s in sorted(self.segments, key=lambda s:s.addr) if self.is_flash_addr(s.addr) and len(s.data)] + ram_segments = [copy.deepcopy(s) for s in sorted(self.segments, key=lambda s:s.addr) if not self.is_flash_addr(s.addr) and len(s.data)] + + # check for multiple ELF sections that are mapped in the same flash mapping region. + # this is usually a sign of a broken linker script, but if you have a legitimate + # use case then let us know + if len(flash_segments) > 0: + last_addr = flash_segments[0].addr + for segment in flash_segments[1:]: + if segment.addr // self.IROM_ALIGN == last_addr // self.IROM_ALIGN: + raise FatalError(("Segment loaded at 0x%08x lands in same 64KB flash mapping as segment loaded at 0x%08x. " + "Can't generate binary. Suggest changing linker script or ELF to merge sections.") % + (segment.addr, last_addr)) + last_addr = segment.addr + + # try to fit each flash segment on a 64kB aligned boundary + # by padding with parts of the non-flash segments... + while len(flash_segments) > 0: + segment = flash_segments[0] + # remove 8 bytes empty data for insert segment header + if segment.name == '.flash.rodata': + segment.data = segment.data[8:] + # write the flash segment + checksum = self.save_segment(f, segment, checksum) + flash_segments.pop(0) + total_segments += 1 + + # flash segments all written, so write any remaining RAM segments + for segment in ram_segments: + checksum = self.save_segment(f, segment, checksum) + total_segments += 1 + + # done writing segments + self.append_checksum(f, checksum) + image_length = f.tell() + + # kinda hacky: go back to the initial header and write the new segment count + # that includes padding segments. This header is not checksummed + f.seek(1) + try: + f.write(chr(total_segments)) + except TypeError: # Python 3 + f.write(bytes([total_segments])) + + if self.append_digest: + # calculate the SHA256 of the whole file and append it + f.seek(0) + digest = hashlib.sha256() + digest.update(f.read(image_length)) + f.write(digest.digest()) + + with open(filename, 'wb') as real_file: + real_file.write(f.getvalue()) + + def load_extended_header(self, load_file): + def split_byte(n): + return (n & 0x0F, (n >> 4) & 0x0F) + + fields = list(struct.unpack(self.EXTENDED_HEADER_STRUCT_FMT, load_file.read(16))) + + self.wp_pin = fields[0] + + # SPI pin drive stengths are two per byte + self.clk_drv, self.q_drv = split_byte(fields[1]) + self.d_drv, self.cs_drv = split_byte(fields[2]) + self.hd_drv, self.wp_drv = split_byte(fields[3]) + + if fields[15] in [0, 1]: + self.append_digest = (fields[15] == 1) + else: + raise RuntimeError("Invalid value for append_digest field (0x%02x). Should be 0 or 1.", fields[15]) + + # remaining fields in the middle should all be zero + if any(f for f in fields[4:15] if f != 0): + print("Warning: some reserved header fields have non-zero values. This image may be from a newer esptool.py?") + + ESP32ROM.BOOTLOADER_IMAGE = ESP32FirmwareImage @@ -2861,12 +3586,12 @@ class ESP32S3BETA2FirmwareImage(ESP32FirmwareImage): ESP32S3BETA2ROM.BOOTLOADER_IMAGE = ESP32S3BETA2FirmwareImage -class ESP32S3BETA3FirmwareImage(ESP32FirmwareImage): +class ESP32S3FirmwareImage(ESP32FirmwareImage): """ ESP32S3 Firmware Image almost exactly the same as ESP32FirmwareImage """ - ROM_LOADER = ESP32S3BETA3ROM + ROM_LOADER = ESP32S3ROM -ESP32S3BETA3ROM.BOOTLOADER_IMAGE = ESP32S3BETA3FirmwareImage +ESP32S3ROM.BOOTLOADER_IMAGE = ESP32S3FirmwareImage class ESP32C3FirmwareImage(ESP32FirmwareImage): @@ -2885,9 +3610,42 @@ class ESP32C6BETAFirmwareImage(ESP32FirmwareImage): ESP32C6BETAROM.BOOTLOADER_IMAGE = ESP32C6BETAFirmwareImage +class ESP32H2BETA1FirmwareImage(ESP32FirmwareImage): + """ ESP32H2 Firmware Image almost exactly the same as ESP32FirmwareImage """ + ROM_LOADER = ESP32H2BETA1ROM + + +ESP32H2BETA1ROM.BOOTLOADER_IMAGE = ESP32H2BETA1FirmwareImage + + +class ESP32H2BETA2FirmwareImage(ESP32FirmwareImage): + """ ESP32H2 Firmware Image almost exactly the same as ESP32FirmwareImage """ + ROM_LOADER = ESP32H2BETA2ROM + + +ESP32H2BETA2ROM.BOOTLOADER_IMAGE = ESP32H2BETA2FirmwareImage + + +class ESP32C2FirmwareImage(ESP32FirmwareImage): + """ ESP32C2 Firmware Image almost exactly the same as ESP32FirmwareImage """ + ROM_LOADER = ESP32C2ROM + + def set_mmu_page_size(self, size): + if size not in [16384, 32768, 65536]: + raise FatalError("{} is not a valid page size.".format(size)) + self.IROM_ALIGN = size + + +ESP32C2ROM.BOOTLOADER_IMAGE = ESP32C2FirmwareImage + + class ELFFile(object): SEC_TYPE_PROGBITS = 0x01 SEC_TYPE_STRTAB = 0x03 + SEC_TYPE_INITARRAY = 0x0e + SEC_TYPE_FINIARRAY = 0x0f + + PROG_SEC_TYPES = (SEC_TYPE_PROGBITS, SEC_TYPE_INITARRAY, SEC_TYPE_FINIARRAY) LEN_SEC_HEADER = 0x28 @@ -2944,7 +3702,7 @@ class ELFFile(object): name_offs, sec_type, _flags, lma, sec_offs, size = struct.unpack_from("H", result[:2]) + message += " (result was {}: {})".format(hexify(result), err_defs.get(err_code[0], 'Unknown result')) return FatalError(message) @@ -3273,7 +4073,7 @@ def _update_image_flash_params(esp, address, args, image): flash_freq = flash_size_freq & 0x0F if args.flash_freq != 'keep': - flash_freq = {'40m': 0, '26m': 1, '20m': 2, '80m': 0xf}[args.flash_freq] + flash_freq = esp.parse_flash_freq_arg(args.flash_freq) flash_size = flash_size_freq & 0xF0 if args.flash_size != 'keep': @@ -3333,7 +4133,7 @@ def write_flash(esp, args): argfile.seek(0, os.SEEK_END) if address + argfile.tell() > flash_end: raise FatalError(("File %s (length %d) at offset %d will not fit in %d bytes of flash. " - "Use --flash-size argument, or change flashing address.") + "Use --flash_size argument, or change flashing address.") % (argfile.name, argfile.tell(), address, flash_end)) argfile.seek(0) @@ -3497,8 +4297,20 @@ def write_flash(esp, args): def image_info(args): + if args.chip == "auto": + print("WARNING: --chip not specified, defaulting to ESP8266.") image = LoadFirmwareImage(args.chip, args.filename) print('Image version: %d' % image.version) + if args.chip != 'auto' and args.chip != 'esp8266': + print( + "Minimal chip revision:", + "v{}.{},".format(image.min_rev_full // 100, image.min_rev_full % 100), + "(legacy min_rev = {})".format(image.min_rev) + ) + print( + "Maximal chip revision:", + "v{}.{}".format(image.max_rev_full // 100, image.max_rev_full % 100), + ) print('Entry point: %08x' % image.entrypoint if image.entrypoint != 0 else 'Entry point not set') print('%d segments' % len(image.segments)) print() @@ -3539,9 +4351,10 @@ def make_image(args): def elf2image(args): e = ELFFile(args.input) if args.chip == 'auto': # Default to ESP8266 for backwards compatibility - print("Creating image for ESP8266...") args.chip = 'esp8266' + print("Creating {} image...".format(args.chip)) + if args.chip == 'esp32': image = ESP32FirmwareImage() if args.secure_pad: @@ -3556,8 +4369,8 @@ def elf2image(args): image = ESP32S3BETA2FirmwareImage() if args.secure_pad_v2: image.secure_pad = '2' - elif args.chip == 'esp32s3beta3': - image = ESP32S3BETA3FirmwareImage() + elif args.chip == 'esp32s3': + image = ESP32S3FirmwareImage() if args.secure_pad_v2: image.secure_pad = '2' elif args.chip == 'esp32c3': @@ -3568,21 +4381,43 @@ def elf2image(args): image = ESP32C6BETAFirmwareImage() if args.secure_pad_v2: image.secure_pad = '2' + elif args.chip == 'esp32h2beta1': + image = ESP32H2BETA1FirmwareImage() + if args.secure_pad_v2: + image.secure_pad = '2' + elif args.chip == 'esp32h2beta2': + image = ESP32H2BETA2FirmwareImage() + if args.secure_pad_v2: + image.secure_pad = '2' + elif args.chip == 'esp32c2': + image = ESP32C2FirmwareImage() + if args.secure_pad_v2: + image.secure_pad = '2' elif args.version == '1': # ESP8266 image = ESP8266ROMFirmwareImage() - else: + elif args.version == '2': image = ESP8266V2FirmwareImage() + else: + image = ESP8266V3FirmwareImage() image.entrypoint = e.entrypoint image.flash_mode = {'qio': 0, 'qout': 1, 'dio': 2, 'dout': 3}[args.flash_mode] if args.chip != 'esp8266': - image.min_rev = int(args.min_rev) + image.min_rev = args.min_rev + image.min_rev_full = args.min_rev_full + image.max_rev_full = args.max_rev_full + + if args.flash_mmu_page_size: + image.set_mmu_page_size(flash_size_bytes(args.flash_mmu_page_size)) # ELFSection is a subclass of ImageSegment, so can use interchangeably image.segments = e.segments if args.use_segments else e.sections - image.flash_size_freq = image.ROM_LOADER.FLASH_SIZES[args.flash_size] - image.flash_size_freq += {'40m': 0, '26m': 1, '20m': 2, '80m': 0xf}[args.flash_freq] + if args.pad_to_size: + image.pad_to_size = flash_size_bytes(args.pad_to_size) + + image.flash_size_freq = image.ROM_LOADER.parse_flash_size_arg(args.flash_size) + image.flash_size_freq += image.ROM_LOADER.parse_flash_freq_arg(args.flash_freq) if args.elf_sha256_offset: image.elf_sha256 = e.sha256() @@ -3600,6 +4435,8 @@ def elf2image(args): args.output = image.default_output_name(args.input) image.save(args.output) + print("Successfully created {} image.".format(args.chip)) + def read_mac(esp, args): mac = esp.read_mac() @@ -3716,15 +4553,23 @@ def write_flash_status(esp, args): def get_security_info(esp, args): - (flags, flash_crypt_cnt, key_purposes) = esp.get_security_info() - # TODO: better display - print('Flags: 0x%08x (%s)' % (flags, bin(flags))) - print('Flash_Crypt_Cnt: 0x%x' % flash_crypt_cnt) - print('Key_Purposes: %s' % (key_purposes,)) + si = esp.get_security_info() + # TODO: better display and tests + print('Flags: {:#010x} ({})'.format(si["flags"], bin(si["flags"]))) + print('Flash_Crypt_Cnt: {:#x}'.format(si["flash_crypt_cnt"])) + print('Key_Purposes: {}'.format(si["key_purposes"])) + if si["chip_id"] is not None and si["api_version"] is not None: + print('Chip_ID: {}'.format(si["chip_id"])) + print('Api_Version: {}'.format(si["api_version"])) def merge_bin(args): - chip_class = _chip_to_rom_loader(args.chip) + try: + chip_class = _chip_to_rom_loader(args.chip) + except KeyError: + msg = "Please specify the chip argument" if args.chip == "auto" else "Invalid chip choice: '{}'".format(args.chip) + msg = msg + " (choose from {})".format(', '.join(SUPPORTED_CHIPS)) + raise FatalError(msg) # sort the files by offset. The AddrFilenamePairAction has already checked for overlap input_files = sorted(args.addr_filename, key=lambda x: x[0]) @@ -3772,12 +4617,12 @@ def main(argv=None, esp=None): external_esp = esp is not None - parser = argparse.ArgumentParser(description='esptool.py v%s - ESP8266 ROM Bootloader Utility' % __version__, prog='esptool') + parser = argparse.ArgumentParser(description='esptool.py v%s - Espressif chips ROM Bootloader Utility' % __version__, prog='esptool') parser.add_argument('--chip', '-c', help='Target chip type', - type=lambda c: c.lower().replace('-', ''), # support ESP32-S2, etc. - choices=['auto', 'esp8266', 'esp32', 'esp32s2', 'esp32s3beta2', 'esp32s3beta3', 'esp32c3', 'esp32c6beta'], + type=format_chip_name, # support ESP32-S2, etc. + choices=['auto'] + SUPPORTED_CHIPS, default=os.environ.get('ESPTOOL_CHIP', 'auto')) parser.add_argument( @@ -3873,12 +4718,12 @@ def main(argv=None, esp=None): extra_fs_message = "" parent.add_argument('--flash_freq', '-ff', help='SPI Flash frequency', - choices=extra_keep_args + ['40m', '26m', '20m', '80m'], + choices=extra_keep_args + ['80m', '60m', '48m', '40m', '30m', '26m', '24m', '20m', '16m', '15m', '12m'], default=os.environ.get('ESPTOOL_FF', 'keep' if allow_keep else '40m')) parent.add_argument('--flash_mode', '-fm', help='SPI Flash mode', choices=extra_keep_args + ['qio', 'qout', 'dio', 'dout'], default=os.environ.get('ESPTOOL_FM', 'keep' if allow_keep else 'qio')) - parent.add_argument('--flash_size', '-fs', help='SPI Flash size in MegaBytes (1MB, 2MB, 4MB, 8MB, 16M)' + parent.add_argument('--flash_size', '-fs', help='SPI Flash size in MegaBytes (1MB, 2MB, 4MB, 8MB, 16MB, 32MB, 64MB, 128MB)' ' plus ESP8266-only (256KB, 512KB, 2MB-c1, 4MB-c1)' + extra_fs_message, action=FlashSizeAction, auto_detect=auto_detect, default=os.environ.get('ESPTOOL_FS', 'keep' if allow_keep else '1MB')) @@ -3935,8 +4780,36 @@ def main(argv=None, esp=None): help='Create an application image from ELF file') parser_elf2image.add_argument('input', help='Input ELF file') parser_elf2image.add_argument('--output', '-o', help='Output filename prefix (for version 1 image), or filename (for version 2 single image)', type=str) - parser_elf2image.add_argument('--version', '-e', help='Output image version', choices=['1', '2'], default='1') - parser_elf2image.add_argument('--min-rev', '-r', help='Minimum chip revision', choices=['0', '1', '2', '3'], default='0') + parser_elf2image.add_argument('--version', '-e', help='Output image version', choices=['1', '2', '3'], default='1') + parser_elf2image.add_argument( + # kept for compatibility + # Minimum chip revision (deprecated, consider using --min-rev-full) + "--min-rev", + "-r", + # In v3 we do not do help=argparse.SUPPRESS because + # it should remain visible. + help="Minimal chip revision (ECO version format)", + type=int, + choices=range(256), + metavar="{0, ... 255}", + default=0, + ) + parser_elf2image.add_argument( + "--min-rev-full", + help="Minimal chip revision (in format: major * 100 + minor)", + type=int, + choices=range(65536), + metavar="{0, ... 65535}", + default=0, + ) + parser_elf2image.add_argument( + "--max-rev-full", + help="Maximal chip revision (in format: major * 100 + minor)", + type=int, + choices=range(65536), + metavar="{0, ... 65535}", + default=65535, + ) parser_elf2image.add_argument('--secure-pad', action='store_true', help='Pad image so once signed it will end on a 64KB boundary. For Secure Boot v1 images only.') parser_elf2image.add_argument('--secure-pad-v2', action='store_true', @@ -3946,7 +4819,12 @@ def main(argv=None, esp=None): type=arg_auto_int, default=None) parser_elf2image.add_argument('--use_segments', help='If set, ELF segments will be used instead of ELF sections to genereate the image.', action='store_true') - + parser_elf2image.add_argument('--flash-mmu-page-size', help="Change flash MMU page size.", choices=['64KB', '32KB', '16KB']) + parser_elf2image.add_argument( + "--pad-to-size", + help="The block size with which the final binary image after padding must be aligned to. Value 0xFF is used for padding, similar to erase_flash", + default=None, + ) add_spi_flash_subparsers(parser_elf2image, allow_keep=False, auto_detect=False) subparsers.add_parser( @@ -4024,11 +4902,10 @@ def main(argv=None, esp=None): help='Address followed by binary filename, separated by space', action=AddrFilenamePairAction) - subparsers.add_parser( - 'version', help='Print esptool version') - subparsers.add_parser('get_security_info', help='Get some security-related data') + subparsers.add_parser('version', help='Print esptool version') + # internal sanity check - every operation matches a module function of the same name for operation in subparsers.choices.keys(): assert operation in globals(), "%s should be a module function" % operation @@ -4074,6 +4951,7 @@ def main(argv=None, esp=None): esp = esp or get_default_connected_device(ser_list, port=args.port, connect_attempts=args.connect_attempts, initial_baud=initial_baud, chip=args.chip, trace=args.trace, before=args.before) + if esp is None: raise FatalError("Could not connect to an Espressif device on any of the %d available serial ports." % len(ser_list)) @@ -4089,6 +4967,9 @@ def main(argv=None, esp=None): if esp.secure_download_mode: print("WARNING: Stub loader is not supported in Secure Download Mode, setting --no-stub") args.no_stub = True + elif not esp.IS_STUB and esp.stub_is_disabled: + print("WARNING: Stub loader has been disabled for compatibility, setting --no-stub") + args.no_stub = True else: esp = esp.run_stub() @@ -4112,11 +4993,84 @@ def main(argv=None, esp=None): # ROM loader doesn't enable flash unless we explicitly do it esp.flash_spi_attach(0) + # XMC chip startup sequence + XMC_VENDOR_ID = 0x20 + + def is_xmc_chip_strict(): + id = esp.flash_id() + rdid = ((id & 0xff) << 16) | ((id >> 16) & 0xff) | (id & 0xff00) + + vendor_id = ((rdid >> 16) & 0xFF) + mfid = ((rdid >> 8) & 0xFF) + cpid = (rdid & 0xFF) + + if vendor_id != XMC_VENDOR_ID: + return False + + matched = False + if mfid == 0x40: + if cpid >= 0x13 and cpid <= 0x20: + matched = True + elif mfid == 0x41: + if cpid >= 0x17 and cpid <= 0x20: + matched = True + elif mfid == 0x50: + if cpid >= 0x15 and cpid <= 0x16: + matched = True + return matched + + def flash_xmc_startup(): + # If the RDID value is a valid XMC one, may skip the flow + fast_check = True + if fast_check and is_xmc_chip_strict(): + return # Successful XMC flash chip boot-up detected by RDID, skipping. + + sfdp_mfid_addr = 0x10 + mf_id = esp.read_spiflash_sfdp(sfdp_mfid_addr, 8) + if mf_id != XMC_VENDOR_ID: # Non-XMC chip detected by SFDP Read, skipping. + return + + print("WARNING: XMC flash chip boot-up failure detected! Running XMC25QHxxC startup flow") + esp.run_spiflash_command(0xB9) # Enter DPD + esp.run_spiflash_command(0x79) # Enter UDPD + esp.run_spiflash_command(0xFF) # Exit UDPD + time.sleep(0.002) # Delay tXUDPD + esp.run_spiflash_command(0xAB) # Release Power-Down + time.sleep(0.00002) + # Check for success + if not is_xmc_chip_strict(): + print("WARNING: XMC flash boot-up fix failed.") + print("XMC flash chip boot-up fix successful!") + + # Check flash chip connection + if not esp.secure_download_mode: + try: + flash_id = esp.flash_id() + if flash_id in (0xffffff, 0x000000): + print('WARNING: Failed to communicate with the flash chip, read/write operations will fail. ' + 'Try checking the chip connections or removing any other hardware connected to IOs.') + except Exception as e: + esp.trace('Unable to verify flash chip connection ({}).'.format(e)) + + # Check if XMC SPI flash chip booted-up successfully, fix if not + if not esp.secure_download_mode: + try: + flash_xmc_startup() + except Exception as e: + esp.trace('Unable to perform XMC flash chip startup sequence ({}).'.format(e)) + if hasattr(args, "flash_size"): print("Configuring flash size...") detect_flash_size(esp, args) if args.flash_size != 'keep': # TODO: should set this even with 'keep' esp.flash_set_parameters(flash_size_bytes(args.flash_size)) + # Check if stub supports chosen flash size + if esp.IS_STUB and args.flash_size in ('32MB', '64MB', '128MB'): + print("WARNING: Flasher stub doesn't fully support flash size larger than 16MB, in case of failure use --no-stub.") + + if esp.IS_STUB and hasattr(args, "address") and hasattr(args, "size"): + if args.address + args.size > 0x1000000: + print("WARNING: Flasher stub doesn't fully support flash size larger than 16MB, in case of failure use --no-stub.") try: operation_func(esp, args) @@ -4281,292 +5235,407 @@ class AddrFilenamePairAction(argparse.Action): # Binary stub code (see flasher_stub dir for source & details) ESP8266ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" -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\ +5dwuzr3/OyU1/H+n+L8kk1ilxnz4f3giyVw=\ """))) ESP32ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" -eNqVWm1z2zYS/iu0EtmRL+kAFEUCvutEdh3ZTtKp3Tayk1PvSoJgk7uMR3Z0Y9lN/vthX0CAlJrefZBNguBid7H77Av4+97Krld7B0m1t1gL5X5isW6y54u1NNENXLQ3ZbZY28rd1DAtPMkP4XLHXZfu1yzWRiQw\ -AlRT96zRneEn7k+WJKvFWrulbOpuc/ebhNWEgLcm9JaS7n/eoeBYAdqOHaWI+xLGhCNpRRBHVIMGWHCjhZsKNDKgA5zKDkFN02TtRkUktUpY9EbFojrO4f26x5RjxnEAM5V4PD+lpziz/F9m9leHnxRJuxNJb0/w\ -pzxHFtRlvHgVkRSGtBEWZkmRqypSsO5xqNP3dBFGUNXz+01RHMXPbjQFaQYiSWhrtokjxJT4tZ5ZN8/tiy4DK7aOFGf6bOmeQF2utq9Jmu6PCUlvG8EWDQT8DydkSc+8kRt1BDw/IqstQesqCGJKsmQFs0D7oFjc\ -hbEbdFZY8r1SA7c86w99yQ1KDazCn7EQKxsMB5cZs47wTUkKb5opk5BA370qWXVejQbojnmM1VnCddPfSL24ph2o5f+pdQPWjAIotjE9/pZecYpgIQ0+OkIBDpQehN3SItoFoaZ8pXobp7L4fjr1V6c0jO/orCXl\ -HaOSfosSxgjQZlWwJG7TrN+0CGl0dN2Ci2bRVewXVTqKMIE3ye9sZ6YGiGFs0vwDRJLOEY0mxg2PtS+Z9IrfcFzqKsbA9KzvotEC6Epl4MYv5rWK1wXs7YwnZ0FyO4kMhc0b14+NwCCIVdGrSA/teEYwI8RnIgBP\ -pCNg5QxtJdrWngEGverFqiXTHb/uvrUipuuIUUQlNMtIOazILkicPwNPZlhyfyrQTXOVnY8NuTRsqRi7d2X27qfzxeKQQgm9bTkiol8eO4XlvAMYmx6zz0/IWUGrdryJehDMJOx5TThR1SS2ap28G9dae1TmYEC3\ -Jhv9/ASoHAxG8O9JBgSM0DEQq24EQQdaUgRuyuenj1ERMHdAKilDZKk9ctQEnCoC58Dat7AziAMpgY31WyHJOMs0WL8HPgxJkrRkZeR9abBB71a9AFriVRKPpx+8V+6wsiKsQ67FNoU+AszdkgBAKPDhQHhEATLK\ -e0tKbJCEpzAB5DOHPoSmcQqAI3KkkMMMLGCwPxZ/O+QYkY6u9GkcVJ4hJINCS7/Rkz6XT4mJNrSCEniu4A1L2TVhmqVdgOd1xSqptqjEzzFs7uMubXzX01RMp/gKnZrnZJtzNqM2SXLgU8M0PEML4ntZDRiqUADm\ -psn+KJXy11fxjQOoGvF/CgjyDfsAoFg7DBkxyOtu8p0d4gFipGRb8FE6lsnp7Dpe/yIktOje8vw7wx6fRbskw7StHt+Y/fBWxc64wc8GZrwcTMdgD/Mxpa3oFLzrVfQ2QHRZdvOwDh9RPMA6oQrv4EYUZLVkLjmx\ -F5SANmz+eMODuRgWxVR/Zi4f4818H98s45tVfLOOb0CpvzEe1qJ1JljvPbvVThky5DhblmVzRnJKBLoqaBL9OXu6uH4LhI4anhJlFUGki1CPoMw+MS7fQMSaXLo9Umz1ud+ImtbF+dvMr93AG3fRsgtMPlwgS7Pd\ -aCZu6fQT6V0yaPvaisxsufb2WnTt1civRijgz/FQN1FFMvkRI+DDDQpxG3n3xNv/ElK0JAQcjJ1oDMPgIp4diWle8ivE34S4qja0/DB4OC4Ido3lRBfpnK+2y60KkHRM+GfjwsCAb5fp7COTEbF+4cmToNt6I97M\ -OTGzlDyVPsvE/f1IZKrKUkRC+yw4RwXDyF8sVu8ofy3TVx4I33B9Ow4+XTa8RkGZhlHNC2Dhx11YAjQBFXd6SSqBHAS8W6OOfwUFJqRYxAMWRm9ggg1Aga/n28DBhrwHozwa+FNfblTm6z6OKbF5/sPp4Rnx2fYg\ -QNmQMohqShkUkoAbEZoYWE9kz3u1Xa8QxN0y3XpDimmncu2nrcgVIXF74xS2F1HIou6JTzw8Cx1JhIqIVKEe+vyel7aMUVc++5x+3MdYpVIOWdKhDV0ZQ1ev6R+kohMmA9iiSZw1xTpBtu1i21WLfa8pzgP2kX9j\ -+Vvb1oWvByXCGuc+HkZsfyPhLQQfSeEUX2/DwhHEVPFqUKRs4xN6lxZ5RS5hZOI8vi7Y9em5rznsjr9w8aPGsLf/qN/VqdqSdoe9xLKB4O/dWcAyj1JBgkNCmab5kQwcOI/7WM6KzwchAx1x6i+/bMdkX6N0VGTy\ -jf5YSltVleRKJptCvQwB1KSUHGIchQ4FdOUQKdjl4go+Xh6UVrF8teSek4kGEYwUqQdqglLGcBdHpg735L6HDN52fwxRMPuLT2lbsU4pK5D5qiPtFScLhmugoMfx0SHIfMS9QYmT9nBQ3l4sVqOLXSr4MQSY4o4o\ -SMs2hgz6t8e3fIFdqmOgsTxOGrw4HXaYzM4uZt3MRZrHRxcL7hzUaYhkEPHJSAkrwYINbvHX170hx3FhbgnocEIoLdNuAoTUxiE6wDj4WBmN4xzcwFkUZMVtR2nohLqgH3KSU3wQ4k7Pm998ckGZABpLfpc0PNlw\ -nkHjJ6DZBlEunf3GOsox4DUnOOiWQbhpLgNdg0g4e9xaFPaUYpZglbIglkCZzV0YN0SPKg7i3/cgcooVAZkbT3IWmjywXtVZb+ZhkNM37Te6x5XIz4ml/YbJIUu/+OmL1Z0OYAJ5oeXUqKvu44jd1qho3e5ytrW4\ -k3j497aqQB/3zR6srNK+IqdBi9E0QVNk8fdoUPrBD8ScyLpRrp047kt0GVX1MCHrT5hz9wD1cesNfkam7crWs94z4xnMgtlg6ilGFz7QYEaYJtzCy3xTL4UQQbeWQ2e44GyIISv2VlEdW58pzrk0rX1e3P5gm4o5\ -BFclt5VEmOaO2YDrfnqxS60C1CtS4r4xbuLREOWHv+MlYFkSenVVNiejbeyQPMOhxTFlKNhjK4mS7z63aTe0HpS82sg6D0C6Ww6lUByVnLg4VN8L1USbird9wjm1tBo7C/ZR8q/BtunFxmL7VJQr+Veua8Qd77A8\ -4PSelgczhQhuoh5DT5TTraLoTVHmrCfY+nzJMEim5beBsU+Z2DjZgQuGupL3DQ0K4DU3ES0h+VFLK/cdpyGJUOJFaskcOOVpm57byMS61/DAxY8hH0DYNPFSpCdkEfTKF15WvYGwPQz2BSGhFL51feutcx3Zswav\ -yJ6evqAoL31c7/gjLlf65VCA9SQsuFxRlghAbiHoasuJAB6M1dQttnJUH2FCcP+Y6AJiqPEX1Ej+76h1rugtvyfOU1fxHubYNL0Gy1+/BeLDI4orDSS2mvvncfzU4gFOInwG46EK0l3VbyuVVA8BVGDdoqgtj+N4\ -AiJO9qmzgnUaBKSJj/HcVYe5Uk7hIvGP8sjGKPqzaWftlIITA+zkiOOkFYZBD9BK4+kDlqI/cVaKiXCkLkzds02FrQhRtcr8uJpT0xVPhVKMTOufgdfXccZDc2IHkUUXoE1+yl6kYOfVfyC9fQMLvAArmbBZKr+3\ -tgucjvR1a6Czoxhtd4/9Cs99rTTstcXFJkEM5E1UrpWgj16MmbOD2yl4wCGlcHry/eL6M3W00TJsZBl4nKWp0ATvg/2ANKHmNjjWcD1WNBem0Lcxcj3iHr/GAoAb1yU3Lky6s7jdJUMQNupl2/QZtrE+wzXgp0So\ -K+N+d6mhfSKrYzIAw0G/KjKqFER7zNAcB8jSapu/i9mjziYEfDKUbkEtxPiq1Zv1kA8GQYtYwth7uPsSMM1/SgCVBdYtNlgSpMmial2YegsinXIage3KW7hY0jKyn0uYfP42nNthFWL9I1K0246Tb19OaUxmsQ1g\ -9HSs+vOKpj1peqD0VasPr+5w5WtmDWIWMGBgSmlOCrqV+YgSbzzZrajbhuV8/hAKBVk0FIFkcchxwa4ZOvBx65yjKFkoD4lsY5eEO2o8avkmZSz9dw8pNogs1fq4uXgCYsgwac7En1sMGXIh7QG7q3RbZfn41GA5\ -NT1Bhyfjmd1jgl2HxLxAd9uNoiEeFFm69s7ozGDvjks1nPYJxoEyQi3KHz0X+QEWC46YL9Y9ptUpcvWDv83w9tOMqzLbS14Rg8BNsRV2NQyOrHyry/aKfeX5h0k1JxNRbeh9e7qCzMBDOlV2YnqCCdwxH5Ogos65\ -5cjApdNNtMCYNeaD0pbdTXDDTmskY42JBvCBF1mcg6jhkJAOU+7KhDcrH7/GR3FpCj1Hjl9VldA07L4zg3jwpOy/QhMHE2f7J4Uu5NV17OV4aCSO+XAVUj/IhUGpkhuVYAfaH6LF5z8abRwL4J/96dtOe6S1x0cp\ -ufduBN09AlrwIgDqcvJ6y/krvQyim8yLPqEkQIFD9DdD5i97J23gx/qSDzWLl9xI1iSUUsX0FKzljFNM7Y0s3ao8/0XHEk83jyJmUm4veJDGFCOJLF5eAuWbt6D1K/zGSN+cY+p9X653Pazc8YkCdgcOiFEsBdk7\ -Sgxz9vtwPG3kzTnxXPrjWvkD95f5NArxiE+6scJSdOqHebUfM7426Wj0F8CzSu3CWjckfS1DFC3ZOA2e/u/6c18eoIBawuG74qMomCwqqhIx1E4S+p6Ab7HfuQxEsStaEojgwabXr6W6ueRmvPQfi/gTmGKL7ibv\ -2DaZe5Lm3n+lAElAU681QU2pnFp1I89DBlvnIz4lwGCbEwyWEz4pkhl7FNpdcfZmFn0hA4ai21rmI+kZuIYBzc1BaJEECursavYhoIPmjlGYoM/ezho/4XQ4z4jVcASUbUE0mXgY/WbzqZKyPzj7R7sC2HrO3YSw\ -yNM/g83dLRmmiBoa+SVro3N62H7P5xdqy4cn4ZstzHLwU6o0fN6gWKs+YRL4GdD4M/VOQxa5S9NFsTugrVC9SnfjVF2MHvl10WMHnnADbStac9Cezu+Fz7OINZz9jI+wtp3c1/5jPS9a3nl1EFjp6mrvaYIfjf7z\ -06q8hU9HpSiySeqUmLkn9np1e98OyonM3WBdrsroG1M+4djjJzGhcS4mkyz78l9OLzRi\ +eNqVWm1z2zYS/iuyEtmRL+kAFEUCvutEdh3ZTtKp3TaKk1PvSoJkk7uMx3Z0Y8VN/vth3wiQUtO7D7JJEFzsLnaffQF/31vV69XewaDcW66V8T+1XDfp0+Vau+gGLtqbIl2u69LfVDAtPMkO4XLHXxf+1yzXTg1g\ +BKgm/lljO8OP/J90MFgt19YvVSf+NvO/aVhNKXhrSm8Z7f9nHQqeFaDt2TGGuC9gTHmStQriqHLYAAt+NPdTgUYKdIBT3SFoaZqu/KiKpDYDFr0xsaiec3i/6jHlmfEcwEyjHi5O6SnOLP6Xmf3V4afVoN2JQW9P\ +8GeEoxrU5US8kkgqR9oIC7OkyFUZKdj2OLTJO7oII6jqxadNUTzFz340AWmGajCgrdkmjlIz4rcWZv08vy+2CKzUVaQ412fL9gTqcrV9TdJ0f0xpetsptmggID+ckA565o3cmCPg+QFZbQFaN0EQV5AlG5gF2gfF\ +4i5M/KC3woLvjRn65Vl/6Et+UFtgFf5MlFrVwXBwmQnrCN/UpPCmmTEJDfT9q5pVJ2p0QHfCY6zOAq6b/kba5RXtQKX/T607sGYUwLCN2cm39IpXBAvp8NERCnBg7DDsllXRLigz4yvT2ziTxvezmVyd0jC+Y9OW\ +lDhGqWWLBowRoM0yZ0n8ptWyaRHS2Oi6BRfLopvYL8pkHGECb5LsbGemBYhhbLL8A0TS3hGdJcYdj7UvueSS3/Bc2jLGwOSs76LRAuhKReBGFhOt4nUOezvnyWmQvJ5GhsLmjevHRuAQxMroVaSHdjwnmFHqMxGA\ +J9oTqPUcbSXa1p4BBr3a5aol0x2/6r61IqariFFEJTTLSDmsyC5InD8BT2ZY8n9K0E1zmZ5PHLk0bKma+Hd1+van8+XykEIJvV1zRES/PPYKy3gHMDY9ZJ+fkrOCVuvJJupBMNOw5xXhRFmR2KZ18m5ca+3RuIMh\ +3bp0/PMjoHIwHMO/RykQcMrGQGy6EQQd6JoicFM8PX2IioC5Q1JJESJLJchREXCaCJwDa9/CziAOJAQ2tWyFJuMskmD9AnwYkjRpqdaR9yXBBsWtegG0wKtBPJ68F6/cYWVFWIdcq20KfQCYuyUBgFAg4UAJogAZ\ +I96SEBsk4SlMAPncoYTQJE4BcESPDXKYggUM9yfqb4ccI5LxpT2Ng8oThGRQaCEbPe1z+ZiYaEMrKIHnKt6whF0TptW0C/C8Klkl5RaVyBzH5j7p0sZ3haZhOvlX6FQ8J92csxm1SZIDSQ2T8AwtiO91OWSoQgGY\ +myb9o1RKri/jGw9QFeL/DBDkG/YBQLF2GDJikNffZDs7xAPESM22IFE6lsnr7Cpe/yIktOje+vw7xx6fRrukw7StHt+4/fBWyc64wc8GZjwfziZgD4sJpa3oFLzrZfQ2QHRRdPOwDh9RPMA6oQzv4EbkZLVkLhmx\ +F5SANuz+eMODuTgWxZV/Zi4f4s18F99cxzer+GYd34BSf2M8rFTrTLDeO3arnSJkyHG2rIvmjOTUCHRl0CT6c/p4efUGCB01PCXKKoJIF6EeQZklMS5eQcSavvZ7ZNjqM9mIitbF+dvMr93AG3/RsgtM3l8gS/Pd\ +aCZu6ewj6V0zaEttRWZ2vRZ7zbv26vRXIxTw53momqgimf6IEfD+BoW4jbx7KvZ/DSnaIAQcjJ1oDKPgIsKOxjRv8CvE3wFxVW5o+X54f5wT7LqaE12kc77aLrfJQdIJ4V8dFwYOfLtI5h+YjIr1C08eBd1WG/Fm\ +wYlZTclTIVkm7u8HIlOWNUUktM+cc1QwjOzZcvWW8tcieSFA+Irr20nw6aLhNXLKNJxpngELP+7CEqAJqLiT16QSyEHAuy3q+FdQ4IAUi3jAwtgNTKgDUODr2TZwqEPeg1EeDfyxlBul+7qPY0rsnv5wenhGfLY9\ +CFA2pAyqnFEGhSTgRoUmBtYT6dNebdcrBHG3XLfe0GrWqVz7aStyRUjc3niF7UUU0qh7IomHsNCRRJmISBnqoc/veOmaMepSss/Zh32MVSbhkKU92tCVc3T1kv5BKjplMoAtlsRZU6xTZNs+tl222PeS4jxgH/k3\ +lr9V3brw1bBAWOPcR2Ck7m8kvIXgoymc4uttWDiCmKpeDPOEbXxK79IiL8glnB54j69ydn16LjVHvSMXPn5UGPb2H/S7OmVb0u6wl9RsIPh7exawTFAqSHBIKNM0P5KBA+dxH8tb8fkwZKBjTv31l+2YLDVKR0Uu\ +2+iPJbRVZUGu5NIZ1MsQQF1CySHGUehQQFcOkYJdLq7g4+VBaSXLV2nuObloEMHIkHqgJih0DHdxZOpwT+57yOBd708gCqZ/kZS2FeuUsgKdrTrSXnKy4LgGCnqcHB2CzEfcG9Q4aQ8H9e3FcjW+2KWCH0OAy++I\ +gq7ZxpBBeXtyyxfYpToGGtfHgwYvTkcdJtOzi3k3c9Hu4dHFkjsHVRIiGUR8MlLCSrBgh1v89XVvyHF8mLsGdDghlNZJNwFCapMQHWAcfKyIxnEObuA8CrLqtqM0dEKb0w85ySg+KHVnF81vklxQJoDGkt0NGp7s\ +OM+g8RPQbIMol8x/Yx1lGPCaExz0yyDcNK8DXYdIOH/YWhT2lGKWYJUiJ5ZAmc1dGHdEjyoO4l96EBnFioDMjZCchyYPrFd21psLDHL6ZmWje1yp7JxY2m+YHLL0i0xfru5sABPIC2tOjbrqPo7YbY2K1u0uV7cW\ +dxIP/95WFejj0uzByirpK3IWtBhNUzRF53+PBrUMvifmVNqNcu3ESV+i11FVDxPS/oQFdw9QH7di8HMybV+2nvWeOWEwDWaDqacaX0igwYwwGXALL5WmXgIhgm5rDp3hgrMhhqzYW1V5XEumuODStJK8uP3BNuUL\ +CK5GbyuJMM2dsAFX/fRil1oFqFekxH1j3MSjEcoPfyfXgGWD0Ksr0wUZbVOPyDM8WhxThoI9toIoSfe5Tbuh9WD05UbWeQDS3XIoheKo4MTFo/peqCbaVLztEy6opdXU82AfBf8abJtebCy2T0W50X/lukbd8Q7r\ +A07vaXkwU4jgLuox9EQ53SqK3RRlwXqCrc+uGQbJtGQbGPuMi42THThnqCt439CgAF4zF9FSmh+1tDLpOI1IhAIvkprMgVOetum5jUysewsPfPwY8QFEnQxEiuSELIJe+cLLmlcQtkfBviAkFEpa17dinevIni14\ +Rfr49BlFeS1xveOPuFwhy6EA62lY8HpFWSIAeQ1B19acCODBWEXd4lqPqyNMCD49JLqAGGbyBTWS/TtqnRt6S/bEe+oq3sMMm6ZXYPnrN0B8dERxpYHE1nL/PI6fVt3DSYRkMAJVkO6aflupoHoIoALrFkNteRzH\ +ExB1sk+dFazTICBNJcZzVx3maj2Di4E8yiIbo+jPpp22U3JODLCTo44HrTAMeoBWFk8fsBT9ibNSTIQjdWHqnm4qbEWIak0q42ZBTVc8FUowMq1/Bl5fxhkPzYkdROddgHbZKXuRgZ03/4H09hUs8AysZMpmaWRv\ +6y5wetJXrYHOj2K03T2WFZ5KrTTqtcXVJkEM5E1UrhWgj16MWbCD1zPwgENK4ez0++XVZ+poo2XUkWXgcZalQhO8D/YD0oSK2+BYw/VYsVyYQt/G6fWYe/wWCwBuXBfcuHDJzvJ2lwxB1VEvu06eYBvrM1wDfmqE\ +uiLudxcW2ie6PCYDcBz0yzylSkG1xwzNcYAsa7b5u5o/6GxCwCdH6RbUQoyv1rxaj/hgELSIJUz9Ce6+BEyTTwmgssC6pQ6WBGmyKlsXpt6CSmacRmC78hYurmkZ3c8lXLZ4E87tsAqp5REp2m/HybfPZzSm09gG\ +MHp6VuW8omlPmu4pfbXm/Ys7XPmKWYOYBQw4mFK4k5xudTamxBtPdkvqtmE5n92HQkHnDUUgnR9yXKjXDB34uHXOcZQsFIdEtqmvCXfMZNzyTcq4lu8eEmwQ1VTr4+biCYgjw6Q5Uzm3GDHkQtoDdlfatsqS+NRg\ +OTU7QYcn45l/wgS7Col5ju62G0VDPCiq6Vqc0ZvB3h2XajjtI4wDZYRalD96rrIDLBY8MSnWBdOqBLn6QW5TvP0456qs7iWviEHgptgKuxwFRzbS6qp7xb4R/mFSxclEVBuKb89WkBkIpFNlp2YnmMAd8zEJKuqc\ +W44MXDbZRAuMWRM+KG3Z3QQ37LRGMlaYaAAfeJHGOYgZjQjpMOUuXXizlPg1OYpLU+g5cvwqywFNw+47M4gHT6b+V2jiYOJc/0mhC3l1FXs5HhqpYz5chdQPcmFQquZGJdiBlUO0+PzHoo1jAfyznL7ttEdae3yU\ +kol3I+juEdCCFwFQF9OXW85f6WUQ3aUi+pSSAAMO0d8MnT3vnbSBH9vXfKiZP+dGsiWhjMlnp2AtZ5xiWjGyZKvy5IuOazzdPIqYSbi9ICCNKcYgsnj9GijfvAGtX+I3RvbmHFPvT8V6V2Dljk8UsDtwQIxiKcje\ +UWCYq78Px9NO35wTz4Uc1+ofWDEV8V7yqVQjFZahUz/Mq2XMSW3S0egvgGel2YW1bkj6SocoWrBxOjz935VzXx6ggFrA4bvhJjlko5i5lZEXFtxRL20bgdXqOlB27p5gBI82uWKWl7R8JiJnL/kWrU3fslUy3yTH\ +J/k+AcJ/U60tgUxhPE+20echd62yMZ8PYJjNCACLKZ8R6ZR9CS0uP3s1j76NAROxbRXzgTQMXMOA5bYgNEcCBXN2OX8fcMFyryhMsGdv5o1MOB0tUmI1HP6kW7BMDwRAv9l8arTuD87/0a4AVp5xHyEs8vjPAHN3\ +S26polZG9pq10Tk3bL/kk4XawuFR+FoL8xv8iCoJHzYY1qqkSgo/AJp8pq5pyB93abrKd4e0FaZX426cp6vxA1kXfXUohBtoWNGaw/Zcfi98mEWs4ewnfHi17cy+ks/0RLSs8+owsNLV1d7jAX4u+s+Pq+IWPhrV\ +Kk+niVdi6p/UV6vbT+2gnurMD1bFqoi+LuWzjT1+EhOaZGo6TdMv/wXshTKs\ """))) ESP32S2ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" -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\ ++rqe/Hebvj3Y4S8jQkWaGpP//j8geRE3\ """))) ESP32S3BETA2ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" -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\ """))) -ESP32S3BETA3ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" -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\ +ESP32S3ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +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\ """))) ESP32C3ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" -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\ +2JPh/i7GZklhkqv/AdnBtYo=\ """))) ESP32C6BETAROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" -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\ -TB3fMw==\ +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\ +"""))) +ESP32H2BETA1ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +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\ +"""))) +ESP32H2BETA2ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +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\ +"""))) +ESP32C2ROM.STUB_CODE = eval(zlib.decompress(base64.b64decode(b""" +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\ """))) diff --git a/tools/log.pl b/tools/log.pl new file mode 100755 index 0000000..86cb8e2 --- /dev/null +++ b/tools/log.pl @@ -0,0 +1,29 @@ +#! /usr/bin/env perl +use strict; +use POSIX qw(strftime); +my ($dev,$speed)=@ARGV; +if (not defined $dev){ + die "usage: $0 dev" +} +if (! -e $dev) { + die "$dev not found" +} +if (defined $speed){ + print("setting speed $speed"); + system("stty speed $speed < $dev") == 0 or die "unable to set speed"; +} +system("stty raw < $dev") == 0 or die "unable to set raw mode for $dev"; +open(my $fh,"<",$dev) or die "unable to open $dev"; +my $last=0; +while (<$fh>){ + my $x=time(); + if ($last != 0){ + if ($x > ($last+5)){ + print("****gap***\n"); + } + } + printf strftime("%Y/%m/%d-%H%M%S",localtime($x)); + printf("[%04.2f]: ",$x-$last); + $last=$x; + print $_; +} \ No newline at end of file diff --git a/tools/testServer.py b/tools/testServer.py index ecbfb4d..d00c6cd 100755 --- a/tools/testServer.py +++ b/tools/testServer.py @@ -55,8 +55,17 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler): super().do_GET() def do_POST(self): if not self.do_proxy(): - super().do_POST() + super().do_POST() + def guess_type(self,path): + if path.endswith('.gz'): + return super().guess_type(path[0:-3]) + return super().guess_type(path) + def end_headers(self): + if hasattr(self,"isgz") and self.isgz: + self.send_header("Content-Encoding","gzip") + super().end_headers() def translate_path(self, path): + self.isgz=False """Translate a /-separated PATH to the local filename syntax. Components that mean special things to the local file system @@ -90,6 +99,9 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler): rpath += '/' if os.path.exists(rpath): return rpath + if os.path.exists(rpath+".gz"): + self.isgz=True + return rpath+".gz" if isSecond: return rpath isSecond=True diff --git a/web/config.json b/web/config.json index ebc5a27..0b75d27 100644 --- a/web/config.json +++ b/web/config.json @@ -219,16 +219,56 @@ "category":"converter" }, { - "name":"checkRMCt", + "name": "checkRMCt", "label": "check RMC time", "type": "number", "description": "start sending RMC if we did not see an external RMC after this much ms", - "default":"4000", + "default": "4000", "min": 1000, - "check":"checkMinMax", - "category":"converter" - }, - { + "check": "checkMinMax", + "category": "converter" + }, + { + "name": "timeouts", + "type": "array", + "replace": [ + { + "n": "Default", + "d": "4000", + "l": "default", + "t": "NMEA" + }, + { + "n": "Sensor", + "d": "60000", + "l": "sensor", + "t": "sensor" + }, + { + "n": "Long", + "d": "32000", + "l": "long", + "t": "special NMEA" + }, + { + "n": "Ais", + "d": "120000", + "l": "ais", + "t": "ais" + } + ], + "children": [ + { + "name": "timo$n", + "label": "timeout $l", + "default": "$d", + "type": "number", + "description": "data timeouts(ms) for $t data", + "category": "converter" + } + ] + }, + { "name": "stbRudderI", "label":"stb rudder instance", "type": "number", @@ -250,6 +290,71 @@ "description": "the n2k instance to be used as port rudder 0...253, -1 to disable", "category": "converter" }, + { + "name": "windmappings", + "type": "array", + "replace":[ + { + "n": "tng", + "l": "true north ground", + "t": "True_North=0", + "d": "twa_tws" + }, + { + "n": "mgd", + "l": "magnetic ground dir", + "t": "Magnetic=1", + "d":"" + }, + { + "n": "awa", + "l": "apparent angle", + "t": "Apparent=2", + "d":"awa_aws" + }, + { + "n": "gna", + "l": "ground angle", + "t": "True_boat=3", + "d": "" + }, + { + "n": "tra", + "l": "true angle", + "t": "True_water=4", + "d":"" + } + + ], + "children":[ + { + "name":"windm$n", + "type":"list", + "description": "mapping of the PGN 130306 wind reference $t", + "label":"wind $l", + "list":[ + {"l": "-unset-","v":""}, + {"l": "TWA/TWS","v":"twa_tws"}, + {"l": "AWA/AWS", "v":"awa_aws"}, + {"l": "TWD/TWS","v":"twd_tws"} + ], + "category":"converter", + "default":"$d" + + } + ] + }, + { + "name": "winst312", + "label": "130312 WTemp iid", + "type": "number", + "check": "checkMinMax", + "min": -1, + "max": 256, + "description": "the temp instance of PGN 130312 used for water temperature (0...255), use -1 for none, 256 for any", + "default": "256", + "category":"converter" + }, { "name": "usbActisense", "label": "USB mode", @@ -586,6 +691,7 @@ "label": "TCP port", "type": "number", "default": "10110", + "check":"checkPort", "description": "the TCP port we listen on", "category": "TCP server" }, @@ -661,8 +767,12 @@ "label": "remote port", "type": "number", "default": "10110", + "check":"checkPort", "description": "the TCP port we connect to", - "category": "TCP client" + "category": "TCP client", + "condition":{ + "tclEnabled":"true" + } }, { "name": "remoteAddress", @@ -671,7 +781,10 @@ "default": "", "check": "checkIpAddress", "description": "the IP address we connect to in the form 192.168.1.2\nor an MDNS name like ESP32NMEA2K.local", - "category": "TCP client" + "category": "TCP client", + "condition":{ + "tclEnabled":"true" + } }, { "name": "sendTCL", @@ -679,7 +792,10 @@ "type": "boolean", "default": "true", "description": "send out NMEA data to remote TCP server", - "category": "TCP client" + "category": "TCP client", + "condition":{ + "tclEnabled":"true" + } }, { "name": "readTCL", @@ -687,7 +803,10 @@ "type": "boolean", "default": "true", "description": "receive NMEA data from remote TCP server", - "category": "TCP client" + "category": "TCP client", + "condition":{ + "tclEnabled":"true" + } }, { "name": "tclToN2k", @@ -695,7 +814,10 @@ "type": "boolean", "default": "true", "description": "convert NMEA0183 from remote TCP server to NMEA2000", - "category": "TCP client" + "category": "TCP client", + "condition":{ + "tclEnabled":"true" + } }, { "name": "tclReadFilter", @@ -703,7 +825,10 @@ "type": "filter", "default": "", "description": "filter for NMEA0183 data when reading from remote TCP server\nselect aison|aisoff, set a whitelist or a blacklist with NMEA sentences like RMC,RMB", - "category": "TCP client" + "category": "TCP client", + "condition":{ + "tclEnabled":"true" + } }, { "name": "tclWriteFilter", @@ -711,7 +836,10 @@ "type": "filter", "default": "", "description": "filter for NMEA0183 data when writing to remote TCP server\nselect aison|aisoff, set a whitelist or a blacklist with NMEA sentences like RMC,RMB", - "category": "TCP client" + "category": "TCP client", + "condition":{ + "tclEnabled":"true" + } }, { "name": "tclSeasmart", @@ -719,7 +847,172 @@ "type": "boolean", "default": "false", "description": "send NMEA2000 as seasmart to remote TCP server", - "category": "TCP client" + "category": "TCP client", + "condition":{ + "tclEnabled":"true" + } + }, + { + "name": "udpwEnabled", + "label": "enable", + "type": "boolean", + "default": "false", + "description":"enable the UDP writer", + "category":"UDP writer" + }, + { + "name": "udpwPort", + "label": "remote port", + "type": "number", + "default": "10110", + "description": "the UDP port we send to", + "check":"checkPort", + "category": "UDP writer", + "condition":{ + "udpwEnabled":"true" + } + }, + { + "name": "udpwType", + "label": "remote address type", + "type": "list", + "default": "0", + "description": "to which networks/addresses do we send\nbc-all: send broadcast to AP and wifi client network\nbc-ap: send broadcast to access point only\nbc-cli: send broadcast to wifi client network\nnormal: normal target address\nmc-all: multicast to AP and wifi client network\nmc-ap:multicast to AP network\nmc-cli: muticast to wifi client network", + "list":[ + {"l":"bc-all","v":"0"}, + {"l":"bc-ap","v":"1"}, + {"l":"bc-cli","v":"2"}, + {"l":"normal","v":"3"}, + {"l":"mc-all","v":"4"}, + {"l":"mc-ap","v":"5"}, + {"l":"mc-cli","v":"6"} + ], + "category": "UDP writer", + "condition":{ + "udpwEnabled":"true" + } + }, + { + "name": "udpwAddress", + "label": "remote address", + "type": "string", + "default": "", + "check": "checkIpAddress", + "description": "the IP address we connect to in the form 192.168.1.2", + "category": "UDP writer", + "condition":{ + "udpwType":["3"], + "udpwEnabled":"true" + } + }, + { + "name": "udpwMC", + "label": "multicast address", + "type": "string", + "default": "224.0.0.1", + "check": "checkMCAddress", + "description": "the multicast address we send to 224.0.0.0...239.255.255.255", + "category": "UDP writer", + "condition":{ + "udpwType":["4","5","6"], + "udpwEnabled":"true" + } + }, + { + "name": "udpwWriteFilter", + "label": "NMEA write Filter", + "type": "filter", + "default": "", + "description": "filter for NMEA0183 data when writing to remote UDP server\nselect aison|aisoff, set a whitelist or a blacklist with NMEA sentences like RMC,RMB", + "category": "UDP writer", + "condition":{ + "udpwEnabled":"true" + } + }, + { + "name": "udpwSeasmart", + "label": "Seasmart out", + "type": "boolean", + "default": "false", + "description": "send NMEA2000 as seasmart to remote UDP server", + "category": "UDP writer", + "condition":{ + "udpwEnabled":"true" + } + }, + { + "name": "udprEnabled", + "label": "enable", + "type": "boolean", + "default": "false", + "description":"enable the UDP reader", + "category":"UDP reader" + }, + { + "name": "udprPort", + "label": "local port", + "type": "number", + "default": "10110", + "check":"checkPort", + "description": "the UDP port we listen on", + "category": "UDP reader", + "condition":{ + "udprEnabled":"true" + } + }, + { + "name": "udprType", + "label": "local address type", + "type": "list", + "default": "0", + "description": "to which networks/addresses do we listen\nall: listen on AP and wifi client network\nap: listen in access point network only\ncli: listen in wifi client network\nmc-all: receive multicast from AP and wifi client network\nmc-ap:receive multicast from AP network\nmc-cli: receive muticast wifi client network", + "list":[ + {"l":"all","v":"0"}, + {"l":"ap","v":"1"}, + {"l":"cli","v":"2"}, + {"l":"mc-all","v":"4"}, + {"l":"mc-ap","v":"5"}, + {"l":"mc-cli","v":"6"} + ], + "category": "UDP reader", + "condition":{ + "udprEnabled":"true" + } + }, + { + "name": "udprToN2k", + "label": "to NMEA2000", + "type": "boolean", + "default": "true", + "description": "convert NMEA0183 from UDP to NMEA2000", + "category": "UDP reader", + "condition":{ + "udprEnabled":"true" + } + }, + { + "name": "udprMC", + "label": "multicast address", + "type": "string", + "default": "224.0.0.1", + "check": "checkMCAddress", + "description": "the multicast address we listen on 224.0.0.0...239.255.255.255", + "category": "UDP reader", + "condition":{ + "udprType":["4","5","6"], + "udprEnabled":"true" + } + }, + { + "name": "udprReadFilter", + "label": "NMEA read Filter", + "type": "filter", + "default": "", + "description": "filter for NMEA0183 data when receiving\nselect aison|aisoff, set a whitelist or a blacklist with NMEA sentences like RMC,RMB", + "category": "UDP reader", + "condition":{ + "udprEnabled":"true" + } }, { "name": "wifiClient", diff --git a/web/index.css b/web/index.css index c3ea7ee..257b739 100644 --- a/web/index.css +++ b/web/index.css @@ -22,7 +22,7 @@ body { overflow: hidden; } -.tabPage{ +#tabPages{ overflow: auto; } @@ -120,6 +120,9 @@ body { .hidden{ display: none !important; } + .dash.invalid{ + display: none; + } #xdrPage .row>.label{ display: none; } @@ -220,6 +223,7 @@ body { } #tabs { display: flex; + flex-wrap: wrap; border-bottom: 1px solid grey; margin-bottom: 0.5em; } @@ -340,4 +344,7 @@ body { } .error{ color: red; +} +input.error{ + background-color: rgba(255, 0, 0, 0.329); } \ No newline at end of file diff --git a/web/index.html b/web/index.html index e7d784d..0862868 100644 --- a/web/index.html +++ b/web/index.html @@ -23,104 +23,106 @@
Update
Help
-
-
-
- VERSION - --- - -
+
+
+
+
+ VERSION + --- + +
-
- Access Point IP - --- +
+ Access Point IP + --- +
+
+ wifi client connected + --- [---] +
+
+ wifi client IP + --- +
+
+ # clients + --- +
+
+ TCP client connected + --- +
+
+ TCP client error + --- +
+
+ Free heap + --- +
+
+ NMEA2000 State + [---]  + UNKNOWN +
-
- wifi client connected - --- [---] -
-
- wifi client IP - --- -
-
- # clients - --- -
-
- TCP client connected - --- -
-
- TCP client error - --- -
-
- Free heap - --- -
-
- NMEA2000 State - [---]  - UNKNOWN -
+
- -
-