From 37d945a0ea53e898e57f982a03973e87581a3dc0 Mon Sep 17 00:00:00 2001 From: free-x Date: Tue, 9 Sep 2025 13:07:16 +0200 Subject: [PATCH 01/48] intermediate GPS 2.0 base --- lib/hardware/GwM5Base.h | 7 ++++++- webinstall/build.yaml | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/hardware/GwM5Base.h b/lib/hardware/GwM5Base.h index a3284ee..edc3a9e 100644 --- a/lib/hardware/GwM5Base.h +++ b/lib/hardware/GwM5Base.h @@ -37,6 +37,11 @@ GWRESOURCE_USE(SERIAL1,M5_GPS_KIT) #define _GWI_SERIAL1 BOARD_LEFT1,-1,GWSERIAL_TYPE_UNI,9600 #endif +#ifdef M5_GPSV2_KIT + GWRESOURCE_USE(BASE,M5_GPS_KIT) + GWRESOURCE_USE(SERIAL1,M5_GPS_KIT) + #define _GWI_SERIAL1 BOARD_LEFT1,-1,GWSERIAL_TYPE_UNI,115200 +#endif //M5 ProtoHub #ifdef M5_PROTO_HUB @@ -68,4 +73,4 @@ #define ESP32_CAN_RX_PIN BOARD_LEFT2 #endif -#endif \ No newline at end of file +#endif diff --git a/webinstall/build.yaml b/webinstall/build.yaml index 0fcca66..8344026 100644 --- a/webinstall/build.yaml +++ b/webinstall/build.yaml @@ -664,6 +664,11 @@ types: label: "Gps Base" url: "https://docs.m5stack.com/en/atom/atomicgps" resource: serial + - value: M5_GPSV2_KIT + description: "M5 Stack Gps Base v2.0" + label: "Gps Base" + url: "https://docs.m5stack.com/en/atom/Atomic_GPS_Base_v2.0" + resource: serial - value: M5_PROTO_HUB description: "M5 Stack HUB PROTO" url: "https://docs.m5stack.com/en/atom/atomhub" @@ -749,4 +754,4 @@ config: - <<: *spisensors base: busname: "2" - bus: "2" \ No newline at end of file + bus: "2" From 370fd47deb1746fcfac485e78948838c1e37314b Mon Sep 17 00:00:00 2001 From: free-x Date: Tue, 9 Sep 2025 15:03:48 +0200 Subject: [PATCH 02/48] add GPS V2 Base to webinstall configs --- webinstall/config/m5stack-atom-gps_v2-canunit.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 webinstall/config/m5stack-atom-gps_v2-canunit.json diff --git a/webinstall/config/m5stack-atom-gps_v2-canunit.json b/webinstall/config/m5stack-atom-gps_v2-canunit.json new file mode 100644 index 0000000..e7f13be --- /dev/null +++ b/webinstall/config/m5stack-atom-gps_v2-canunit.json @@ -0,0 +1 @@ +{"root:board":"m5stack-atom-generic","root:board:m5lightbase":"M5_GPSV2_KIT","root:board:m5groove":"CAN","root:board:m5groove:m5groovecan":"M5_CANUNIT"} From 2c97eacd766a5db5c1ffc883bdda8139dcf68295 Mon Sep 17 00:00:00 2001 From: free-x Date: Thu, 11 Sep 2025 19:36:09 +0200 Subject: [PATCH 03/48] minor corrections --- lib/hardware/GwM5Base.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/hardware/GwM5Base.h b/lib/hardware/GwM5Base.h index edc3a9e..f6371a9 100644 --- a/lib/hardware/GwM5Base.h +++ b/lib/hardware/GwM5Base.h @@ -38,8 +38,8 @@ #define _GWI_SERIAL1 BOARD_LEFT1,-1,GWSERIAL_TYPE_UNI,9600 #endif #ifdef M5_GPSV2_KIT - GWRESOURCE_USE(BASE,M5_GPS_KIT) - GWRESOURCE_USE(SERIAL1,M5_GPS_KIT) + GWRESOURCE_USE(BASE,M5_GPSV2_KIT) + GWRESOURCE_USE(SERIAL1,M5_GPSV2_KIT) #define _GWI_SERIAL1 BOARD_LEFT1,-1,GWSERIAL_TYPE_UNI,115200 #endif @@ -66,7 +66,7 @@ #endif //can kit for M5 Atom -#ifdef M5_CAN_KIT +#if defined (M5_CAN_KIT) GWRESOURCE_USE(BASE,M5_CAN_KIT) GWRESOURCE_USE(CAN,M5_CANKIT) #define ESP32_CAN_TX_PIN BOARD_LEFT1 From da6022cb28883dcf447c5ab7506a0abf161e424d Mon Sep 17 00:00:00 2001 From: free-x Date: Thu, 11 Sep 2025 20:23:52 +0200 Subject: [PATCH 04/48] #110: add GPS v1.1 unit --- lib/hardware/GwM5Grove.in | 7 +++++++ webinstall/build.yaml | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/lib/hardware/GwM5Grove.in b/lib/hardware/GwM5Grove.in index aed70a1..9858783 100644 --- a/lib/hardware/GwM5Grove.in +++ b/lib/hardware/GwM5Grove.in @@ -43,6 +43,13 @@ #define _GWI_SERIAL_GROOVE$GS$ GWSERIAL_TYPE_RX,9600 #endif +#GROVE +//https://docs.m5stack.com/en/unit/Unit-GPS%20v1.1 +#ifdef M5_GPSV11_UNIT$GS$ + GWRESOURCE_USE(GROOVE$G$,M5_GPSV11_UNIT$GS$) + #define _GWI_SERIAL_GROOVE$GS$ GWSERIAL_TYPE_RX,115200 +#endif + #GROVE //CAN via groove #ifdef M5_CANUNIT$GS$ diff --git a/webinstall/build.yaml b/webinstall/build.yaml index 8344026..fe7ec90 100644 --- a/webinstall/build.yaml +++ b/webinstall/build.yaml @@ -179,6 +179,11 @@ types: description: "M5 Gps Unit" url: "https://docs.m5stack.com/en/unit/gps" resource: serial + - label: "Gps Unit v1.1" + value: M5_GPSV11_UNIT#grv# + description: "M5 Gps Unit v1.1" + url: "https://docs.m5stack.com/en/unit/Unit-GPS%20v1.1" + resource: serial - label: "RS232/RS422" value: SERIAL_GROOVE_232#grv# description: "Generic RS232/RS422 Unit (bidirectional)" From 47fcb26961ee7f6defedc9692d87051e9a68368e Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Tue, 16 Sep 2025 20:22:09 +0200 Subject: [PATCH 05/48] add a generic s3 (devkit-m) to cibuild --- platformio.ini | 11 +++++++ webinstall/build.yaml | 71 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/platformio.ini b/platformio.ini index df144b8..4125eb7 100644 --- a/platformio.ini +++ b/platformio.ini @@ -185,3 +185,14 @@ build_flags = ${env.build_flags} upload_port = /dev/esp32 upload_protocol = esptool + +[env:s3devkitm-generic] +extends = sensors +board = esp32-s3-devkitm-1 +lib_deps = + ${env.lib_deps} + ${sensors.lib_deps} +build_flags = + ${env.build_flags} +upload_port = /dev/esp32 +upload_protocol = esptool diff --git a/webinstall/build.yaml b/webinstall/build.yaml index 0fcca66..6b954c8 100644 --- a/webinstall/build.yaml +++ b/webinstall/build.yaml @@ -233,6 +233,46 @@ types: - 33 - 37 - 38 + + - &gpiopinvs3 + - {label: unset,value:} + - {label: "0: boot mode control",value: 0} + - 1 + - 2 + - {label: "3: JTAG control", value: 3} + - 4 + - 5 + - 6 + - 7 + - 8 + - 9 + - 10 + - 11 + - 12 + - 13 + - 14 + - 17 + - 18 + - 19 + - 20 + - 21 + - 33 + - 34 + - 35 + - 36 + - 37 + - 38 + - 39 + - 40 + - 41 + - 42 + - 43 + - 44 + - {label: "45: strapping pin", value: 45} + - {label: "46: strapping pin", value: 46} + - 47 + - 48 + - &gpioinput type: dropdown @@ -728,6 +768,37 @@ config: description: "Node mcu esp32" url: "https://docs.platformio.org/en/stable/boards/espressif32/nodemcu-32s.html" resource: *esp32default + children: + - *serial1 + - *serial2 + - *can + - *resetButton + - *led + - <<: *iicsensors + base: + busname: "1" + bus: "" + - <<: *iicsensors + base: + busname: "2" + bus: "2" + - <<: *spisensors + base: + busname: "1" + bus: "1" + - <<: *spisensors + base: + busname: "2" + bus: "2" + + - value: s3devkitm-generic + label: s3devkitm + description: "esp32 s3 generic, 8MB flash, no PSRAM " + url: "https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/hw-reference/esp32s3/user-guide-devkitm-1.html" + resource: *esp32default + base: + gpiopinv: *gpiopinvs3 + gpioinputv: *gpiopinvs3 children: - *serial1 - *serial2 From 7ebd582ca02d4053a0578fc7e758a7244cfe5218 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Wed, 17 Sep 2025 17:39:57 +0200 Subject: [PATCH 06/48] set serial mode to RX for M5 GPS base --- lib/hardware/GwM5Base.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/hardware/GwM5Base.h b/lib/hardware/GwM5Base.h index f6371a9..2e88f51 100644 --- a/lib/hardware/GwM5Base.h +++ b/lib/hardware/GwM5Base.h @@ -35,12 +35,12 @@ #ifdef M5_GPS_KIT GWRESOURCE_USE(BASE,M5_GPS_KIT) GWRESOURCE_USE(SERIAL1,M5_GPS_KIT) - #define _GWI_SERIAL1 BOARD_LEFT1,-1,GWSERIAL_TYPE_UNI,9600 + #define _GWI_SERIAL1 BOARD_LEFT1,-1,GWSERIAL_TYPE_RX,9600 #endif #ifdef M5_GPSV2_KIT GWRESOURCE_USE(BASE,M5_GPSV2_KIT) GWRESOURCE_USE(SERIAL1,M5_GPSV2_KIT) - #define _GWI_SERIAL1 BOARD_LEFT1,-1,GWSERIAL_TYPE_UNI,115200 + #define _GWI_SERIAL1 BOARD_LEFT1,-1,GWSERIAL_TYPE_RX,115200 #endif //M5 ProtoHub From 3d131c7d98e49ba30f0fa3fe8d37fa8271844f17 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Wed, 17 Sep 2025 17:40:28 +0200 Subject: [PATCH 07/48] correctly set the mode/type for serial channels --- lib/channel/GwChannelInterface.h | 1 + lib/channel/GwChannelList.cpp | 25 +++++++++++++------------ lib/serial/GwSerial.cpp | 3 +++ lib/serial/GwSerial.h | 2 ++ lib/socketserver/GwSocketServer.cpp | 5 ++++- lib/socketserver/GwSocketServer.h | 1 + lib/socketserver/GwTcpClient.cpp | 5 ++++- lib/socketserver/GwTcpClient.h | 1 + lib/socketserver/GwUdpReader.cpp | 4 +++- lib/socketserver/GwUdpReader.h | 1 + lib/socketserver/GwUdpWriter.cpp | 5 ++++- lib/socketserver/GwUdpWriter.h | 1 + 12 files changed, 38 insertions(+), 16 deletions(-) diff --git a/lib/channel/GwChannelInterface.h b/lib/channel/GwChannelInterface.h index 68f519b..a028eff 100644 --- a/lib/channel/GwChannelInterface.h +++ b/lib/channel/GwChannelInterface.h @@ -7,4 +7,5 @@ class GwChannelInterface{ 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";} + virtual int getType()=0; //return the numeric type }; \ No newline at end of file diff --git a/lib/channel/GwChannelList.cpp b/lib/channel/GwChannelList.cpp index c9db748..87755b4 100644 --- a/lib/channel/GwChannelList.cpp +++ b/lib/channel/GwChannelList.cpp @@ -285,8 +285,8 @@ static ChannelParam channelParameters[]={ }; template -GwSerial* createSerial(GwLog *logger, T* s,int id, bool canRead=true){ - return new GwSerialImpl(logger,s,id,canRead); +GwSerial* createSerial(GwLog *logger, T* s,int id, int type, bool canRead=true){ + return new GwSerialImpl(logger,s,id,type,canRead); } static ChannelParam * findChannelParam(int id){ @@ -300,7 +300,7 @@ static ChannelParam * findChannelParam(int id){ return param; } -static GwSerial * createSerialImpl(GwConfigHandler *config,GwLog *logger, int idx,int rx,int tx, bool setLog=false){ +static GwSerial * createSerialImpl(GwConfigHandler *config,GwLog *logger, int idx,int type,int rx,int tx, bool setLog){ LOG_DEBUG(GwLog::DEBUG,"create serial: channel=%d, rx=%d,tx=%d", idx,rx,tx); ChannelParam *param=findChannelParam(idx); @@ -312,13 +312,13 @@ static GwSerial * createSerialImpl(GwConfigHandler *config,GwLog *logger, int id GwLog *streamLog=setLog?nullptr:logger; switch(param->id){ case USB_CHANNEL_ID: - serialStream=createSerial(streamLog,&USBSerial,param->id); + serialStream=createSerial(streamLog,&USBSerial,param->id,type); break; case SERIAL1_CHANNEL_ID: - serialStream=createSerial(streamLog,&Serial1,param->id); + serialStream=createSerial(streamLog,&Serial1,param->id,type); break; case SERIAL2_CHANNEL_ID: - serialStream=createSerial(streamLog,&Serial2,param->id); + serialStream=createSerial(streamLog,&Serial2,param->id,type); break; } if (serialStream == nullptr){ @@ -332,12 +332,13 @@ static GwSerial * createSerialImpl(GwConfigHandler *config,GwLog *logger, int id } return serialStream; } -static GwChannel * createChannel(GwLog *logger, GwConfigHandler *config, int id,GwChannelInterface *impl, int type=GWSERIAL_TYPE_BI){ +static GwChannel * createChannel(GwLog *logger, GwConfigHandler *config, int id,GwChannelInterface *impl){ ChannelParam *param=findChannelParam(id); if (param == nullptr){ LOG_DEBUG(GwLog::ERROR,"invalid channel id %d",id); return nullptr; } + int type=impl->getType(); bool canRead=false; bool canWrite=false; bool validType=false; @@ -425,10 +426,10 @@ void GwChannelList::begin(bool fallbackSerial){ GwChannel *channel=NULL; //usb if (! fallbackSerial){ - GwSerial *usbSerial=createSerialImpl(config, logger,USB_CHANNEL_ID,GWUSB_RX,GWUSB_TX,true); + GwSerial *usbSerial=createSerialImpl(config, logger,USB_CHANNEL_ID,GWSERIAL_TYPE_BI,GWUSB_RX,GWUSB_TX,true); if (usbSerial != nullptr){ usbSerial->enableWriteLock(); //as it is used for logging we need this additionally - GwChannel *usbChannel=createChannel(logger,config,USB_CHANNEL_ID,usbSerial,GWSERIAL_TYPE_BI); + GwChannel *usbChannel=createChannel(logger,config,USB_CHANNEL_ID,usbSerial); if (usbChannel != nullptr){ addChannel(usbChannel); } @@ -445,9 +446,9 @@ void GwChannelList::begin(bool fallbackSerial){ //new serial config handling for (auto &&init:serialInits){ 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); + GwSerial *ser=createSerialImpl(config,logger,init.serial,init.mode,init.rx,init.tx,false); if (ser != nullptr){ - channel=createChannel(logger,config,init.serial,ser,init.mode); + channel=createChannel(logger,config,init.serial,ser); if (channel != nullptr){ addChannel(channel); } @@ -466,8 +467,8 @@ void GwChannelList::begin(bool fallbackSerial){ config->getInt(config->remotePort), config->getBool(config->readTCL) ); + addChannel(createChannel(logger,config,TCP_CLIENT_CHANNEL_ID,client)); } - addChannel(createChannel(logger,config,TCP_CLIENT_CHANNEL_ID,client)); //udp writer if (config->getBool(GwConfigDefinitions::udpwEnabled)){ diff --git a/lib/serial/GwSerial.cpp b/lib/serial/GwSerial.cpp index b23c52f..e6ec320 100644 --- a/lib/serial/GwSerial.cpp +++ b/lib/serial/GwSerial.cpp @@ -79,6 +79,9 @@ String GwSerial::getMode(){ } return "UNKNOWN"; } +int GwSerial::getType() { + return type; +} bool GwSerial::isInitialized() { return initialized; } size_t GwSerial::enqueue(const uint8_t *data, size_t len, bool partial) diff --git a/lib/serial/GwSerial.h b/lib/serial/GwSerial.h index 1932878..51c9100 100644 --- a/lib/serial/GwSerial.h +++ b/lib/serial/GwSerial.h @@ -43,6 +43,7 @@ class GwSerial : public GwChannelInterface{ 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; + virtual int getType() override; friend GwSerialStream; }; @@ -122,6 +123,7 @@ template setError(serial,logger); }; + }; diff --git a/lib/socketserver/GwSocketServer.cpp b/lib/socketserver/GwSocketServer.cpp index 185e3a4..53ccb82 100644 --- a/lib/socketserver/GwSocketServer.cpp +++ b/lib/socketserver/GwSocketServer.cpp @@ -4,6 +4,7 @@ #include "GwBuffer.h" #include "GwSocketConnection.h" #include "GwSocketHelper.h" +#include "GwHardware.h" GwSocketServer::GwSocketServer(const GwConfigHandler *config, GwLog *logger, int minId) { @@ -185,4 +186,6 @@ int GwSocketServer::numClients() } GwSocketServer::~GwSocketServer() { -} \ No newline at end of file +} + +int GwSocketServer::getType() {return GWSERIAL_TYPE_BI;} \ No newline at end of file diff --git a/lib/socketserver/GwSocketServer.h b/lib/socketserver/GwSocketServer.h index 248fc95..dcee8fc 100644 --- a/lib/socketserver/GwSocketServer.h +++ b/lib/socketserver/GwSocketServer.h @@ -27,5 +27,6 @@ class GwSocketServer: public GwChannelInterface{ virtual size_t sendToClients(const char *buf,int sourceId, bool partialWrite=false); int numClients(); virtual void readMessages(GwMessageFetcher *writer); + virtual int getType() override; }; #endif \ No newline at end of file diff --git a/lib/socketserver/GwTcpClient.cpp b/lib/socketserver/GwTcpClient.cpp index 2792300..f3761db 100644 --- a/lib/socketserver/GwTcpClient.cpp +++ b/lib/socketserver/GwTcpClient.cpp @@ -2,6 +2,7 @@ #include #include #include "GwSocketHelper.h" +#include "GwHardware.h" class ResolveArgs{ public: @@ -291,4 +292,6 @@ void GwTcpClient::setResolved(IPAddress addr, bool valid){ GwTcpClient::ResolvedAddress GwTcpClient::getResolved(){ GWSYNCHRONIZED(locker); return resolvedAddress; -} \ No newline at end of file +} + +int GwTcpClient::getType(){return GWSERIAL_TYPE_BI;} \ No newline at end of file diff --git a/lib/socketserver/GwTcpClient.h b/lib/socketserver/GwTcpClient.h index 25cc654..8665e04 100644 --- a/lib/socketserver/GwTcpClient.h +++ b/lib/socketserver/GwTcpClient.h @@ -53,4 +53,5 @@ public: virtual void readMessages(GwMessageFetcher *writer); bool isConnected(); String getError(){return error;} + virtual int getType() override; }; \ No newline at end of file diff --git a/lib/socketserver/GwUdpReader.cpp b/lib/socketserver/GwUdpReader.cpp index 612eb10..f5927be 100644 --- a/lib/socketserver/GwUdpReader.cpp +++ b/lib/socketserver/GwUdpReader.cpp @@ -5,6 +5,7 @@ #include "GwSocketConnection.h" #include "GwSocketHelper.h" #include "GWWifi.h" +#include "GwHardware.h" GwUdpReader::GwUdpReader(const GwConfigHandler *config, GwLog *logger, int minId) @@ -164,4 +165,5 @@ size_t GwUdpReader::sendToClients(const char *buf, int source,bool partial) GwUdpReader::~GwUdpReader() { -} \ No newline at end of file +} +int GwUdpReader::getType(){return GWSERIAL_TYPE_BI;} \ No newline at end of file diff --git a/lib/socketserver/GwUdpReader.h b/lib/socketserver/GwUdpReader.h index 08c56bb..199096d 100644 --- a/lib/socketserver/GwUdpReader.h +++ b/lib/socketserver/GwUdpReader.h @@ -41,5 +41,6 @@ class GwUdpReader: public GwChannelInterface{ 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); + virtual int getType() override; }; #endif \ No newline at end of file diff --git a/lib/socketserver/GwUdpWriter.cpp b/lib/socketserver/GwUdpWriter.cpp index c91880e..3ab9236 100644 --- a/lib/socketserver/GwUdpWriter.cpp +++ b/lib/socketserver/GwUdpWriter.cpp @@ -5,6 +5,7 @@ #include "GwSocketConnection.h" #include "GwSocketHelper.h" #include "GWWifi.h" +#include "GwHardware.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) @@ -200,4 +201,6 @@ size_t GwUdpWriter::sendToClients(const char *buf, int source,bool partial) GwUdpWriter::~GwUdpWriter() { -} \ No newline at end of file +} + +int GwUdpWriter::getType() {return GWSERIAL_TYPE_BI;} \ No newline at end of file diff --git a/lib/socketserver/GwUdpWriter.h b/lib/socketserver/GwUdpWriter.h index e17a17e..3299cde 100644 --- a/lib/socketserver/GwUdpWriter.h +++ b/lib/socketserver/GwUdpWriter.h @@ -69,5 +69,6 @@ class GwUdpWriter: public GwChannelInterface{ 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); + virtual int getType() override; }; #endif \ No newline at end of file From d5a9568b67c4351db19fb74cab590c21c4501976 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Wed, 17 Sep 2025 17:57:03 +0200 Subject: [PATCH 08/48] make birectional channels the default, clean up some channel handling --- lib/channel/GwChannel.cpp | 13 +++++++++++++ lib/channel/GwChannel.h | 3 ++- lib/channel/GwChannelInterface.h | 4 ++-- lib/hardware/GwChannelModes.h | 23 +++++++++++++++++++++++ lib/hardware/GwHardware.h | 6 +----- lib/serial/GwSerial.cpp | 13 ------------- lib/serial/GwSerial.h | 1 - lib/socketserver/GwSocketServer.cpp | 3 --- lib/socketserver/GwSocketServer.h | 1 - lib/socketserver/GwTcpClient.cpp | 3 --- lib/socketserver/GwTcpClient.h | 1 - lib/socketserver/GwUdpReader.cpp | 5 +---- lib/socketserver/GwUdpReader.h | 1 - lib/socketserver/GwUdpWriter.cpp | 3 --- lib/socketserver/GwUdpWriter.h | 1 - 15 files changed, 42 insertions(+), 39 deletions(-) create mode 100644 lib/hardware/GwChannelModes.h diff --git a/lib/channel/GwChannel.cpp b/lib/channel/GwChannel.cpp index 9c79983..1a0d06f 100644 --- a/lib/channel/GwChannel.cpp +++ b/lib/channel/GwChannel.cpp @@ -249,3 +249,16 @@ unsigned long GwChannel::countTx(){ if (! countOut) return 0UL; return countOut->getGlobal(); } +String GwChannel::typeString(int type){ + 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"; +} \ No newline at end of file diff --git a/lib/channel/GwChannel.h b/lib/channel/GwChannel.h index 66fb4ae..6b34432 100644 --- a/lib/channel/GwChannel.h +++ b/lib/channel/GwChannel.h @@ -77,7 +77,8 @@ class GwChannel{ if (maxSourceId < 0) return source == sourceId; return (source >= sourceId && source <= maxSourceId); } - String getMode(){return impl->getMode();} + static String typeString(int type); + String getMode(){return typeString(impl->getType());} int getMinId(){return sourceId;}; }; diff --git a/lib/channel/GwChannelInterface.h b/lib/channel/GwChannelInterface.h index a028eff..30550d9 100644 --- a/lib/channel/GwChannelInterface.h +++ b/lib/channel/GwChannelInterface.h @@ -1,11 +1,11 @@ #pragma once #include "GwBuffer.h" +#include "GwChannelModes.h" class GwChannelInterface{ public: virtual void loop(bool handleRead,bool handleWrite)=0; 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";} - virtual int getType()=0; //return the numeric type + virtual int getType(){ return GWSERIAL_TYPE_BI;} //return the numeric type }; \ No newline at end of file diff --git a/lib/hardware/GwChannelModes.h b/lib/hardware/GwChannelModes.h new file mode 100644 index 0000000..a9d7757 --- /dev/null +++ b/lib/hardware/GwChannelModes.h @@ -0,0 +1,23 @@ +/* + This code is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + This code 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 + Lesser General Public License for more details. + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + defines for the channel modes(types) +*/ +#ifndef _GWCHANNELMODES_H +#define _GWCHANNELMODES_H +#define GWSERIAL_TYPE_UNI 1 +#define GWSERIAL_TYPE_BI 2 +#define GWSERIAL_TYPE_RX 3 +#define GWSERIAL_TYPE_TX 4 +#define GWSERIAL_TYPE_UNK 0 +#endif \ No newline at end of file diff --git a/lib/hardware/GwHardware.h b/lib/hardware/GwHardware.h index 0b14ab3..41bca41 100644 --- a/lib/hardware/GwHardware.h +++ b/lib/hardware/GwHardware.h @@ -20,11 +20,7 @@ #endif #ifndef _GWHARDWARE_H #define _GWHARDWARE_H -#define GWSERIAL_TYPE_UNI 1 -#define GWSERIAL_TYPE_BI 2 -#define GWSERIAL_TYPE_RX 3 -#define GWSERIAL_TYPE_TX 4 -#define GWSERIAL_TYPE_UNK 0 +#include "GwChannelModes.h" #include #include #include "GwAppInfo.h" diff --git a/lib/serial/GwSerial.cpp b/lib/serial/GwSerial.cpp index e6ec320..ed469f9 100644 --- a/lib/serial/GwSerial.cpp +++ b/lib/serial/GwSerial.cpp @@ -66,19 +66,6 @@ GwSerial::~GwSerial() if (lock != nullptr) vSemaphoreDelete(lock); } -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"; -} int GwSerial::getType() { return type; } diff --git a/lib/serial/GwSerial.h b/lib/serial/GwSerial.h index 51c9100..ae2ca1f 100644 --- a/lib/serial/GwSerial.h +++ b/lib/serial/GwSerial.h @@ -42,7 +42,6 @@ class GwSerial : public GwChannelInterface{ 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; virtual int getType() override; friend GwSerialStream; }; diff --git a/lib/socketserver/GwSocketServer.cpp b/lib/socketserver/GwSocketServer.cpp index 53ccb82..0e0b79e 100644 --- a/lib/socketserver/GwSocketServer.cpp +++ b/lib/socketserver/GwSocketServer.cpp @@ -4,7 +4,6 @@ #include "GwBuffer.h" #include "GwSocketConnection.h" #include "GwSocketHelper.h" -#include "GwHardware.h" GwSocketServer::GwSocketServer(const GwConfigHandler *config, GwLog *logger, int minId) { @@ -187,5 +186,3 @@ int GwSocketServer::numClients() GwSocketServer::~GwSocketServer() { } - -int GwSocketServer::getType() {return GWSERIAL_TYPE_BI;} \ No newline at end of file diff --git a/lib/socketserver/GwSocketServer.h b/lib/socketserver/GwSocketServer.h index dcee8fc..248fc95 100644 --- a/lib/socketserver/GwSocketServer.h +++ b/lib/socketserver/GwSocketServer.h @@ -27,6 +27,5 @@ class GwSocketServer: public GwChannelInterface{ virtual size_t sendToClients(const char *buf,int sourceId, bool partialWrite=false); int numClients(); virtual void readMessages(GwMessageFetcher *writer); - virtual int getType() override; }; #endif \ No newline at end of file diff --git a/lib/socketserver/GwTcpClient.cpp b/lib/socketserver/GwTcpClient.cpp index f3761db..a2c68eb 100644 --- a/lib/socketserver/GwTcpClient.cpp +++ b/lib/socketserver/GwTcpClient.cpp @@ -2,7 +2,6 @@ #include #include #include "GwSocketHelper.h" -#include "GwHardware.h" class ResolveArgs{ public: @@ -293,5 +292,3 @@ GwTcpClient::ResolvedAddress GwTcpClient::getResolved(){ GWSYNCHRONIZED(locker); return resolvedAddress; } - -int GwTcpClient::getType(){return GWSERIAL_TYPE_BI;} \ No newline at end of file diff --git a/lib/socketserver/GwTcpClient.h b/lib/socketserver/GwTcpClient.h index 8665e04..25cc654 100644 --- a/lib/socketserver/GwTcpClient.h +++ b/lib/socketserver/GwTcpClient.h @@ -53,5 +53,4 @@ public: virtual void readMessages(GwMessageFetcher *writer); bool isConnected(); String getError(){return error;} - virtual int getType() override; }; \ No newline at end of file diff --git a/lib/socketserver/GwUdpReader.cpp b/lib/socketserver/GwUdpReader.cpp index f5927be..29d854e 100644 --- a/lib/socketserver/GwUdpReader.cpp +++ b/lib/socketserver/GwUdpReader.cpp @@ -5,8 +5,6 @@ #include "GwSocketConnection.h" #include "GwSocketHelper.h" #include "GWWifi.h" -#include "GwHardware.h" - GwUdpReader::GwUdpReader(const GwConfigHandler *config, GwLog *logger, int minId) { @@ -165,5 +163,4 @@ size_t GwUdpReader::sendToClients(const char *buf, int source,bool partial) GwUdpReader::~GwUdpReader() { -} -int GwUdpReader::getType(){return GWSERIAL_TYPE_BI;} \ No newline at end of file +} \ No newline at end of file diff --git a/lib/socketserver/GwUdpReader.h b/lib/socketserver/GwUdpReader.h index 199096d..08c56bb 100644 --- a/lib/socketserver/GwUdpReader.h +++ b/lib/socketserver/GwUdpReader.h @@ -41,6 +41,5 @@ class GwUdpReader: public GwChannelInterface{ 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); - virtual int getType() override; }; #endif \ No newline at end of file diff --git a/lib/socketserver/GwUdpWriter.cpp b/lib/socketserver/GwUdpWriter.cpp index 3ab9236..ef0020c 100644 --- a/lib/socketserver/GwUdpWriter.cpp +++ b/lib/socketserver/GwUdpWriter.cpp @@ -5,7 +5,6 @@ #include "GwSocketConnection.h" #include "GwSocketHelper.h" #include "GWWifi.h" -#include "GwHardware.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) @@ -202,5 +201,3 @@ size_t GwUdpWriter::sendToClients(const char *buf, int source,bool partial) GwUdpWriter::~GwUdpWriter() { } - -int GwUdpWriter::getType() {return GWSERIAL_TYPE_BI;} \ No newline at end of file diff --git a/lib/socketserver/GwUdpWriter.h b/lib/socketserver/GwUdpWriter.h index 3299cde..e17a17e 100644 --- a/lib/socketserver/GwUdpWriter.h +++ b/lib/socketserver/GwUdpWriter.h @@ -69,6 +69,5 @@ class GwUdpWriter: public GwChannelInterface{ 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); - virtual int getType() override; }; #endif \ No newline at end of file From 95df5858accce238f84ea559ee1990bde4315376 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Wed, 17 Sep 2025 20:05:45 +0200 Subject: [PATCH 09/48] #112: clearify licenses to be GPL v2 or later --- Readme.md | 4 ++++ lib/config/GwConverterConfig.h | 2 +- lib/hardware/GwChannelModes.h | 2 +- lib/hardware/GwHardware.h | 2 +- lib/hardware/GwM5Base.h | 2 +- lib/hardware/GwM5Grove.h | 2 +- lib/hardware/GwM5Grove.in | 2 +- lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h | 2 +- lib/sensors/GwSensor.cpp | 2 +- lib/sensors/GwSensor.h | 2 +- lib/spitask/GWDMS22B.cpp | 2 +- lib/spitask/GWDMS22B.h | 2 +- lib/spitask/GwSpiSensor.h | 2 +- lib/spitask/GwSpiTask.cpp | 2 +- lib/spitask/GwSpiTask.h | 2 +- src/main.cpp | 2 +- 16 files changed, 19 insertions(+), 15 deletions(-) diff --git a/Readme.md b/Readme.md index a9a305f..d3fae7f 100644 --- a/Readme.md +++ b/Readme.md @@ -43,6 +43,10 @@ What is included For the details of the mapped PGNs and NMEA sentences refer to [Conversions](doc/Conversions.pdf). +License +------- +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](LICENSE), or (at your option) any later version. + Hardware -------- The software is prepared to run on different kinds of ESP32 based modules and accessoirs. For some of them prebuild binaries are available that only need to be flashed, others would require to add some definitions of the used PINs and features and to build the binary. diff --git a/lib/config/GwConverterConfig.h b/lib/config/GwConverterConfig.h index 4a93a71..25ca39e 100644 --- a/lib/config/GwConverterConfig.h +++ b/lib/config/GwConverterConfig.h @@ -3,7 +3,7 @@ This code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + version 2 of the License, or (at your option) any later version. This code 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 diff --git a/lib/hardware/GwChannelModes.h b/lib/hardware/GwChannelModes.h index a9d7757..e6c42de 100644 --- a/lib/hardware/GwChannelModes.h +++ b/lib/hardware/GwChannelModes.h @@ -2,7 +2,7 @@ This code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + version 2 of the License, or (at your option) any later version. This code 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 diff --git a/lib/hardware/GwHardware.h b/lib/hardware/GwHardware.h index 41bca41..10a7404 100644 --- a/lib/hardware/GwHardware.h +++ b/lib/hardware/GwHardware.h @@ -2,7 +2,7 @@ This code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + version 2 of the License, or (at your option) any later version. This code 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 diff --git a/lib/hardware/GwM5Base.h b/lib/hardware/GwM5Base.h index 2e88f51..67c865e 100644 --- a/lib/hardware/GwM5Base.h +++ b/lib/hardware/GwM5Base.h @@ -2,7 +2,7 @@ This code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + version 2 of the License, or (at your option) any later version. This code 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 diff --git a/lib/hardware/GwM5Grove.h b/lib/hardware/GwM5Grove.h index 220ee01..44761a1 100644 --- a/lib/hardware/GwM5Grove.h +++ b/lib/hardware/GwM5Grove.h @@ -2,7 +2,7 @@ This code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + version 2 of the License, or (at your option) any later version. This code 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 diff --git a/lib/hardware/GwM5Grove.in b/lib/hardware/GwM5Grove.in index 9858783..c98d4d1 100644 --- a/lib/hardware/GwM5Grove.in +++ b/lib/hardware/GwM5Grove.in @@ -2,7 +2,7 @@ This code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + version 2 of the License, or (at your option) any later version. This code 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 diff --git a/lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h b/lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h index 169f181..a627952 100644 --- a/lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h +++ b/lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h @@ -2,7 +2,7 @@ This code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + version 2 of the License, or (at your option) any later version. This code 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 diff --git a/lib/sensors/GwSensor.cpp b/lib/sensors/GwSensor.cpp index d0d580b..abdb508 100644 --- a/lib/sensors/GwSensor.cpp +++ b/lib/sensors/GwSensor.cpp @@ -3,7 +3,7 @@ This code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + version 2 of the License, or (at your option) any later version. This code 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 diff --git a/lib/sensors/GwSensor.h b/lib/sensors/GwSensor.h index 3077360..48eb5b2 100644 --- a/lib/sensors/GwSensor.h +++ b/lib/sensors/GwSensor.h @@ -3,7 +3,7 @@ This code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + version 2 of the License, or (at your option) any later version. This code 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 diff --git a/lib/spitask/GWDMS22B.cpp b/lib/spitask/GWDMS22B.cpp index fca345e..0447fb2 100644 --- a/lib/spitask/GWDMS22B.cpp +++ b/lib/spitask/GWDMS22B.cpp @@ -3,7 +3,7 @@ This code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + version 2 of the License, or (at your option) any later version. This code 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 diff --git a/lib/spitask/GWDMS22B.h b/lib/spitask/GWDMS22B.h index de53804..29a0b1b 100644 --- a/lib/spitask/GWDMS22B.h +++ b/lib/spitask/GWDMS22B.h @@ -3,7 +3,7 @@ This code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + version 2 of the License, or (at your option) any later version. This code 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 diff --git a/lib/spitask/GwSpiSensor.h b/lib/spitask/GwSpiSensor.h index c12a410..ec21afe 100644 --- a/lib/spitask/GwSpiSensor.h +++ b/lib/spitask/GwSpiSensor.h @@ -3,7 +3,7 @@ This code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + version 2 of the License, or (at your option) any later version. This code 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 diff --git a/lib/spitask/GwSpiTask.cpp b/lib/spitask/GwSpiTask.cpp index a5dda8a..54f9f3d 100644 --- a/lib/spitask/GwSpiTask.cpp +++ b/lib/spitask/GwSpiTask.cpp @@ -3,7 +3,7 @@ This code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + version 2 of the License, or (at your option) any later version. This code 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 diff --git a/lib/spitask/GwSpiTask.h b/lib/spitask/GwSpiTask.h index 0714a31..b42b462 100644 --- a/lib/spitask/GwSpiTask.h +++ b/lib/spitask/GwSpiTask.h @@ -3,7 +3,7 @@ This code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + version 2 of the License, or (at your option) any later version. This code 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 diff --git a/src/main.cpp b/src/main.cpp index 6daaa51..de56930 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,7 +3,7 @@ This code is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. + version 2 of the License, or (at your option) any later version. This code 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 From e8c5440a7924ca0c4f6c18fe1840975f06b5b39d Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Fri, 19 Sep 2025 12:13:41 +0200 Subject: [PATCH 10/48] #114: correctly distinguish between type 1..3 ais messages when converting from N2K --- lib/nmea2kto0183/N2kDataToNMEA0183.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp index dbf6af5..23ce436 100644 --- a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp +++ b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp @@ -713,7 +713,6 @@ private: void HandleAISClassAPosReport(const tN2kMsg &N2kMsg) { - unsigned char SID; tN2kAISRepeat _Repeat; uint32_t _UserID; // MMSI double _Latitude =N2kDoubleNA; @@ -732,7 +731,7 @@ private: uint8_t _MessageType = 1; tNMEA0183AISMsg NMEA0183AISMsg; - if (ParseN2kPGN129038(N2kMsg, SID, _Repeat, _UserID, _Latitude, _Longitude, _Accuracy, _RAIM, _Seconds, + if (ParseN2kPGN129038(N2kMsg, _MessageType, _Repeat, _UserID, _Latitude, _Longitude, _Accuracy, _RAIM, _Seconds, _COG, _SOG, _Heading, _ROT, _NavStatus,_AISTransceiverInformation,_SID)) { @@ -770,6 +769,7 @@ private: Serial.println(_NavStatus); #endif + if (_MessageType < 1 || _MessageType > 3) _MessageType=1; //only allow type 1...3 for 129038 if (SetAISClassABMessage1(NMEA0183AISMsg, _MessageType, _Repeat, _UserID, _Latitude, _Longitude, _Accuracy, _RAIM, _Seconds, _COG, _SOG, _Heading, _ROT, _NavStatus)) { From 7fd1457296cb0de70a7c4b92c59603c14ed983b2 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Fri, 19 Sep 2025 20:43:28 +0200 Subject: [PATCH 11/48] add conversion from NMEA0183 AIS type 21 (aton) to PGN 129041 --- lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h | 37 +++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h b/lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h index a627952..70ba754 100644 --- a/lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h +++ b/lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h @@ -27,6 +27,8 @@ const double nmTom = 1.852 * 1000; uint16_t DaysSince1970 = 0; +#define boolbit(b) (b?1:0) + class MyAisDecoder : public AIS::AisDecoder { public: @@ -255,9 +257,40 @@ class MyAisDecoder : public AIS::AisDecoder send(N2kMsg); } - - virtual void onType21(unsigned int , unsigned int , const std::string &, bool , int , int , unsigned int , unsigned int , unsigned int , unsigned int ) override { + //mmsi, aidType, name + nameExt, posAccuracy, posLon, posLat, toBow, toStern, toPort, toStarboard + virtual void onType21(unsigned int mmsi , unsigned int aidType , const std::string & name, bool accuracy, int posLon, int posLat, unsigned int toBow, unsigned int toStern, unsigned int toPort, unsigned int toStarboard) override { //Serial.println("21"); + //in principle we should use tN2kAISAtoNReportData to directly call the library + //function for 129041. But this makes the conversion really complex. + int repeat=0; //TODO: should be part of the parameters + int seconds=0; + bool raim=false; + bool offPosition=false; + bool assignedMode=false; + bool virtualAton=false; + tN2kGNSStype gnssType=tN2kGNSStype::N2kGNSSt_GPS; //canboat considers 0 as undefined... + tN2kAISTransceiverInformation transceiverInfo=tN2kAISTransceiverInformation::N2kaischannel_A_VDL_reception; + tN2kMsg N2kMsg; + N2kMsg.SetPGN(129041); + N2kMsg.Priority=4; + N2kMsg.AddByte((repeat & 0x03) << 6 | (21 & 0x3f)); + N2kMsg.Add4ByteUInt(mmsi); //N2kData.UserID + N2kMsg.Add4ByteDouble(posLon / 600000.0, 1e-07); + N2kMsg.Add4ByteDouble(posLat / 600000.0, 1e-07); + N2kMsg.AddByte((seconds & 0x3f)<<2 | boolbit(raim)<<1 | boolbit(accuracy)); + N2kMsg.Add2ByteUDouble(toBow+toStern, 0.1); + N2kMsg.Add2ByteUDouble(toPort+toStarboard, 0.1); + N2kMsg.Add2ByteUDouble(toStarboard, 0.1); + N2kMsg.Add2ByteUDouble(toBow, 0.1); + N2kMsg.AddByte(boolbit(assignedMode) << 7 + | boolbit(virtualAton) << 6 + | boolbit(offPosition) << 5 + | (aidType & 0x1f)); + N2kMsg.AddByte((gnssType & 0x0F) << 1 | 0xe0); + N2kMsg.AddByte(N2kUInt8NA); //status + N2kMsg.AddByte((transceiverInfo & 0x1f) | 0xe0); + N2kMsg.AddVarStr(name.c_str()); + send(N2kMsg); } virtual void onType24A(unsigned int _uMsgType, unsigned int _repeat, unsigned int _uMmsi, From 6e0d56316b76e6f13895326f4133e43818ed01bc Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Sun, 21 Sep 2025 16:40:24 +0200 Subject: [PATCH 12/48] add some testing tools --- tools/gen3byte.py | 32 ++++++++++++++++++++++++++++++++ tools/sendDelay.py | 19 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100755 tools/gen3byte.py create mode 100755 tools/sendDelay.py diff --git a/tools/gen3byte.py b/tools/gen3byte.py new file mode 100755 index 0000000..2d84518 --- /dev/null +++ b/tools/gen3byte.py @@ -0,0 +1,32 @@ +#! /usr/bin/env python3 +#generate 3 byte codes for the RGB bytes +#refer to https://controllerstech.com/ws2812-leds-using-spi/ +ONE_BIT='110' +ZERO_BIT='100' + +currentStr='' + +def checkAndPrint(curr): + if len(curr) >= 8: + print("0b%s,"%curr[0:8],end='') + return curr[8:] + return curr +first=True + +print("uint8_t colorTo3Byte[256][3]=") +print("{") +for i in range(0,256): + if not first: + print("},") + first=False + print("{/*%02d*/"%i,end='') + mask=0x80 + for b in range(0,8): + if (i & mask) != 0: + currentStr+=ONE_BIT + else: + currentStr+=ZERO_BIT + mask=mask >> 1 + currentStr=checkAndPrint(currentStr) +print("}") +print("};") diff --git a/tools/sendDelay.py b/tools/sendDelay.py new file mode 100755 index 0000000..ab91737 --- /dev/null +++ b/tools/sendDelay.py @@ -0,0 +1,19 @@ +#! /usr/bin/env python3 +import sys +import os +import time + +def usage(): + print(f"usage: {sys.argv[0]} file delay") + sys.exit(1) + +if len(sys.argv) < 3: + usage() + +delay=float(sys.argv[2]) +fn=sys.argv[1] +with open (fn,"r") as fh: + for line in fh: + print(line,end="",flush=True) + time.sleep(delay) + From e578b428c9b10ce0e6bb4da1439d8885583d6f13 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Sun, 21 Sep 2025 19:32:02 +0200 Subject: [PATCH 13/48] more parameters for AIS type 21 --- lib/aisparser/ais_decoder.cpp | 14 ++++--- lib/aisparser/ais_decoder.h | 3 +- lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h | 51 +++++++++++------------ 3 files changed, 35 insertions(+), 33 deletions(-) diff --git a/lib/aisparser/ais_decoder.cpp b/lib/aisparser/ais_decoder.cpp index 6fece95..3c5dc00 100644 --- a/lib/aisparser/ais_decoder.cpp +++ b/lib/aisparser/ais_decoder.cpp @@ -627,7 +627,7 @@ void AisDecoder::decodeType21(PayloadBuffer &_buffer, unsigned int _uMsgType, in } // decode message fields (binary buffer has to go through all fields, but some fields are not used) - _buffer.getUnsignedValue(2); // repeatIndicator + auto repeat=_buffer.getUnsignedValue(2); // repeatIndicator auto mmsi = _buffer.getUnsignedValue(30); auto aidType = _buffer.getUnsignedValue(5); auto name = _buffer.getString(120); @@ -640,11 +640,11 @@ void AisDecoder::decodeType21(PayloadBuffer &_buffer, unsigned int _uMsgType, in auto toStarboard = _buffer.getUnsignedValue(6); _buffer.getUnsignedValue(4); // epfd type - _buffer.getUnsignedValue(6); // timestamp - _buffer.getBoolValue(); // off position + auto timestamp=_buffer.getUnsignedValue(6); // timestamp + auto offPosition=_buffer.getBoolValue(); // off position _buffer.getUnsignedValue(8); // reserved - _buffer.getBoolValue(); // RAIM - _buffer.getBoolValue(); // virtual aid + auto raim=_buffer.getBoolValue(); // RAIM + auto virtualAton=_buffer.getBoolValue(); // virtual aid _buffer.getBoolValue(); // assigned mode _buffer.getUnsignedValue(1); // spare @@ -654,7 +654,9 @@ void AisDecoder::decodeType21(PayloadBuffer &_buffer, unsigned int _uMsgType, in nameExt = _buffer.getString(88); } - onType21(mmsi, aidType, name + nameExt, posAccuracy, posLon, posLat, toBow, toStern, toPort, toStarboard); + onType21(mmsi, aidType, name + nameExt, posAccuracy, posLon, posLat, + toBow, toStern, toPort, toStarboard, + repeat,timestamp, raim, virtualAton, offPosition); } /* decode Voyage Report and Static Data (type nibble already pulled from buffer) */ diff --git a/lib/aisparser/ais_decoder.h b/lib/aisparser/ais_decoder.h index c908839..7872d3b 100644 --- a/lib/aisparser/ais_decoder.h +++ b/lib/aisparser/ais_decoder.h @@ -297,7 +297,8 @@ namespace AIS bool assigned, unsigned int repeat, bool raim) = 0; virtual void onType21(unsigned int _uMmsi, unsigned int _uAidType, const std::string &_strName, bool _bPosAccuracy, int _iPosLon, int _iPosLat, - unsigned int _uToBow, unsigned int _uToStern, unsigned int _uToPort, unsigned int _uToStarboard) = 0; + unsigned int _uToBow, unsigned int _uToStern, unsigned int _uToPort, unsigned int _uToStarboard, + unsigned int repeat,unsigned int timestamp, bool raim, bool virtualAton, bool offPosition) = 0; virtual void onType24A(unsigned int _uMsgType, unsigned int _repeat, unsigned int _uMmsi, const std::string &_strName) = 0; diff --git a/lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h b/lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h index 70ba754..5f4e5ee 100644 --- a/lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h +++ b/lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h @@ -84,25 +84,24 @@ class MyAisDecoder : public AIS::AisDecoder tN2kMsg N2kMsg; - // PGN129038 - - N2kMsg.SetPGN(129038L); - N2kMsg.Priority = 4; - N2kMsg.AddByte((_Repeat & 0x03) << 6 | (_uMsgType & 0x3f)); - N2kMsg.Add4ByteUInt(_uMmsi); - N2kMsg.Add4ByteDouble(_iPosLon / 600000.0, 1e-07); - N2kMsg.Add4ByteDouble(_iPosLat / 600000.0, 1e-07); - N2kMsg.AddByte((_timestamp & 0x3f) << 2 | (_Raim & 0x01) << 1 | (_bPosAccuracy & 0x01)); - N2kMsg.Add2ByteUDouble(decodeCog(_iCog), 1e-04); - N2kMsg.Add2ByteUDouble(_uSog * knToms/10.0, 0.01); - N2kMsg.AddByte(0x00); // Communication State (19 bits) - N2kMsg.AddByte(0x00); - N2kMsg.AddByte(0x00); // AIS transceiver information (5 bits) - N2kMsg.Add2ByteUDouble(decodeHeading(_iHeading), 1e-04); - N2kMsg.Add2ByteDouble(decodeRot(_iRot), 3.125E-05); // 1e-3/32.0 - N2kMsg.AddByte(0xF0 | (_uNavstatus & 0x0f)); - N2kMsg.AddByte(0xff); // Reserved - N2kMsg.AddByte(0xff); // SID (NA) + SetN2kPGN129038( + N2kMsg, + _uMsgType, + (tN2kAISRepeat)_Repeat, + _uMmsi, + _iPosLon/ 600000.0, + _iPosLat / 600000.0, + _bPosAccuracy, + _Raim, + _timestamp, + decodeCog(_iCog), + _uSog * knToms/10.0, + tN2kAISTransceiverInformation::N2kaischannel_A_VDL_reception, + decodeHeading(_iHeading), + decodeRot(_iRot), + (tN2kAISNavStatus)_uNavstatus, + 0xff + ); send(N2kMsg); } @@ -258,16 +257,14 @@ class MyAisDecoder : public AIS::AisDecoder } //mmsi, aidType, name + nameExt, posAccuracy, posLon, posLat, toBow, toStern, toPort, toStarboard - virtual void onType21(unsigned int mmsi , unsigned int aidType , const std::string & name, bool accuracy, int posLon, int posLat, unsigned int toBow, unsigned int toStern, unsigned int toPort, unsigned int toStarboard) override { + virtual void onType21(unsigned int mmsi , unsigned int aidType , const std::string & name, bool accuracy, int posLon, int posLat, unsigned int toBow, + unsigned int toStern, unsigned int toPort, unsigned int toStarboard, + unsigned int repeat,unsigned int timestamp, bool raim, bool virtualAton, bool offPosition) override { //Serial.println("21"); + //the name can be at most 120bit+88bit (35 byte) + termination -> 36 Byte //in principle we should use tN2kAISAtoNReportData to directly call the library //function for 129041. But this makes the conversion really complex. - int repeat=0; //TODO: should be part of the parameters - int seconds=0; - bool raim=false; - bool offPosition=false; bool assignedMode=false; - bool virtualAton=false; tN2kGNSStype gnssType=tN2kGNSStype::N2kGNSSt_GPS; //canboat considers 0 as undefined... tN2kAISTransceiverInformation transceiverInfo=tN2kAISTransceiverInformation::N2kaischannel_A_VDL_reception; tN2kMsg N2kMsg; @@ -277,7 +274,7 @@ class MyAisDecoder : public AIS::AisDecoder N2kMsg.Add4ByteUInt(mmsi); //N2kData.UserID N2kMsg.Add4ByteDouble(posLon / 600000.0, 1e-07); N2kMsg.Add4ByteDouble(posLat / 600000.0, 1e-07); - N2kMsg.AddByte((seconds & 0x3f)<<2 | boolbit(raim)<<1 | boolbit(accuracy)); + N2kMsg.AddByte((timestamp & 0x3f)<<2 | boolbit(raim)<<1 | boolbit(accuracy)); N2kMsg.Add2ByteUDouble(toBow+toStern, 0.1); N2kMsg.Add2ByteUDouble(toPort+toStarboard, 0.1); N2kMsg.Add2ByteUDouble(toStarboard, 0.1); @@ -289,6 +286,8 @@ class MyAisDecoder : public AIS::AisDecoder N2kMsg.AddByte((gnssType & 0x0F) << 1 | 0xe0); N2kMsg.AddByte(N2kUInt8NA); //status N2kMsg.AddByte((transceiverInfo & 0x1f) | 0xe0); + //bit offset 208 (see canboat/pgns.xml) -> 26 bytes from start + //as MaxDataLen is 223 and the string can be at most 36 bytes + 2 byte heading - no further check here N2kMsg.AddVarStr(name.c_str()); send(N2kMsg); } From 3df2571ca25c1bdcccfb1a6eb673b177428bf057 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Sun, 21 Sep 2025 21:02:31 +0200 Subject: [PATCH 14/48] intermediate: prepare AIS class 21 to 129041 --- lib/nmea2kto0183/N2kDataToNMEA0183.cpp | 35 ++ lib/nmea2ktoais/NMEA0183AISMessages.cpp | 574 ------------------------ lib/nmea2ktoais/NMEA0183AISMessages.h | 86 ---- lib/nmea2ktoais/NMEA0183AISMsg.cpp | 297 ------------ lib/nmea2ktoais/NMEA0183AISMsg.h | 92 ---- lib/nmea2ktoais/README.md | 49 -- platformio.ini | 25 ++ 7 files changed, 60 insertions(+), 1098 deletions(-) delete mode 100644 lib/nmea2ktoais/NMEA0183AISMessages.cpp delete mode 100644 lib/nmea2ktoais/NMEA0183AISMessages.h delete mode 100644 lib/nmea2ktoais/NMEA0183AISMsg.cpp delete mode 100644 lib/nmea2ktoais/NMEA0183AISMsg.h delete mode 100644 lib/nmea2ktoais/README.md diff --git a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp index 23ce436..945ad4c 100644 --- a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp +++ b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp @@ -1076,6 +1076,40 @@ private: return; } + //***************************************************************************** + // PGN 129041 Aton + void HandleAISMessage21(const tN2kMsg &N2kMsg) + { + tN2kAISAtoNReportData data; + if (ParseN2kPGN129041(N2kMsg,data)){ + tNMEA0183AISMsg nmea0183Msg; + if (SetAISMessage21( + nmea0183Msg, + data.Repeat, + data.UserID, + data.Latitude, + data.Longitude, + data.Accuracy, + data.RAIM, + data.Seconds, + data.Length, + data.Beam, + data.PositionReferenceStarboard, + data.PositionReferenceTrueNorth, + data.AtoNType, + data.OffPositionIndicator, + data.VirtualAtoNFlag, + data.AssignedModeFlag, + data.GNSSType, + data.AtoNStatus, + data.AISTransceiverInformation, + data.AtoNName + )){ + //TODO: SendMessage(nmea0183Msg); + } + } + } + void HandleSystemTime(const tN2kMsg &msg){ unsigned char sid=-1; uint16_t DaysSince1970=N2kUInt16NA; @@ -1614,6 +1648,7 @@ private: converters.registerConverter(129794UL, &N2kToNMEA0183Functions::HandleAISClassAMessage5); // AIS Class A Ship Static and Voyage related data, Message Type 5 converters.registerConverter(129809UL, &N2kToNMEA0183Functions::HandleAISClassBMessage24A); // AIS Class B "CS" Static Data Report, Part A converters.registerConverter(129810UL, &N2kToNMEA0183Functions::HandleAISClassBMessage24B); // AIS Class B "CS" Static Data Report, Part B + converters.registerConverter(129041UL, &N2kToNMEA0183Functions::HandleAISMessage21); // AIS Aton #endif } diff --git a/lib/nmea2ktoais/NMEA0183AISMessages.cpp b/lib/nmea2ktoais/NMEA0183AISMessages.cpp deleted file mode 100644 index a0f9ec0..0000000 --- a/lib/nmea2ktoais/NMEA0183AISMessages.cpp +++ /dev/null @@ -1,574 +0,0 @@ -/* -NMEA0183AISMessages.cpp - -Copyright (c) 2019 Ronnie Zeiller - -Based on the works of Timo Lappalainen NMEA2000 and NMEA0183 Library -Thanks to Eric S. Raymond (https://gpsd.gitlab.io/gpsd/AIVDM.html) -and Kurt Schwehr for their informations on AIS encoding. - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the -Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE -OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -*/ - -#include -#include -#include -#include -//#include -//#include -#include -#include -#include - -const double pi=3.1415926535897932384626433832795; -const double kmhToms=1000.0/3600.0; -const double knToms=1852.0/3600.0; -const double degToRad=pi/180.0; -const double radToDeg=180.0/pi; -const double msTokmh=3600.0/1000.0; -const double msTokn=3600.0/1852.0; -const double nmTom=1.852*1000; -const double mToFathoms=0.546806649; -const double mToFeet=3.2808398950131; -const double radsToDegMin = 60 * 360.0 / (2 * pi); // [rad/s -> degree/minute] -const char Prefix='!'; - -std::vector vships; - -int numShips(){return vships.size();} -// ************************ Helper for AIS *********************************** -static bool AddMessageType(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageType); -static bool AddRepeat(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t Repeat); -static bool AddUserID(tNMEA0183AISMsg &NMEA0183AISMsg, uint32_t UserID); -static bool AddIMONumber(tNMEA0183AISMsg &NMEA0183AISMsg, uint32_t &IMONumber); -static bool AddText(tNMEA0183AISMsg &NMEA0183AISMsg, char *FieldVal, uint8_t length); -static bool AddDimensions(tNMEA0183AISMsg &NMEA0183AISMsg, double Length, double Beam, double PosRefStbd, double PosRefBow); -static bool AddNavStatus(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t &NavStatus); -static bool AddROT(tNMEA0183AISMsg &NMEA0183AISMsg, double &rot); -static bool AddSOG (tNMEA0183AISMsg &NMEA0183AISMsg, double &sog); -static bool AddLongitude(tNMEA0183AISMsg &NMEA0183AISMsg, double &Longitude); -static bool AddLatitude(tNMEA0183AISMsg &NMEA0183AISMsg, double &Latitude); -static bool AddHeading (tNMEA0183AISMsg &NMEA0183AISMsg, double &heading); -static bool AddCOG(tNMEA0183AISMsg &NMEA0183AISMsg, double cog); -static bool AddSeconds (tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t &Seconds); -static bool AddEPFDFixType(tNMEA0183AISMsg &NMEA0183AISMsg, tN2kGNSStype &GNSStype); -static bool AddStaticDraught(tNMEA0183AISMsg &NMEA0183AISMsg, double &Draught); -static bool AddETADateTime(tNMEA0183AISMsg &NMEA0183AISMsg, uint16_t &ETAdate, double &ETAtime); - -//***************************************************************************** -// Types 1, 2 and 3: Position Report Class A or B -> https://gpsd.gitlab.io/gpsd/AIVDM.html -// total of 168 bits, occupying one AIVDM sentence -// Example: !AIVDM,1,1,,A,133m@ogP00PD;88MD5MTDww@2D7k,0*46 -// Payload: Payload: 133m@ogP00PD;88MD5MTDww@2D7k -// Message type 1 has a payload length of 168 bits. -// because AIS encodes messages using a 6-bits ASCII mechanism and 168 divided by 6 is 28. -// -// Got values from: ParseN2kPGN129038() -bool SetAISClassABMessage1( tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageType, uint8_t Repeat, - uint32_t UserID, double Latitude, double Longitude, bool Accuracy, bool RAIM, uint8_t Seconds, - double COG, double SOG, double Heading, double ROT, uint8_t NavStatus ) { - - NMEA0183AISMsg.ClearAIS(); - if ( !AddMessageType(NMEA0183AISMsg, MessageType) ) return false; // 0 - 5 | 6 Message Type -> Constant: 1 - if ( !AddRepeat(NMEA0183AISMsg, Repeat) ) return false; // 6 - 7 | 2 Repeat Indicator: 0 = default; 3 = do not repeat any more - if ( !AddUserID(NMEA0183AISMsg, UserID) ) return false; // 8 - 37 | 30 MMSI - if ( !AddNavStatus(NMEA0183AISMsg, NavStatus) ) return false; // 38-41 | 4 Navigational Status e.g.: "Under way sailing" - if ( !AddROT(NMEA0183AISMsg, ROT) ) return false; // 42-49 | 8 Rate of Turn (ROT) - if ( !AddSOG(NMEA0183AISMsg, SOG) ) return false; // 50-59 | 10 [m/s -> kts] SOG with one digit x10, 1023 = N/A - if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Accuracy, 1) ) return false;// 60 | 1 GPS Accuracy 1 oder 0, Default 0 - if ( !AddLongitude(NMEA0183AISMsg, Longitude) ) return false; // 61-88 | 28 Longitude in Minutes / 10000 - if ( !AddLatitude(NMEA0183AISMsg, Latitude) ) return false; // 89-115 | 27 Latitude in Minutes / 10000 - if ( !AddCOG(NMEA0183AISMsg, COG) ) return false; // 116-127 | 12 Course over ground will be 3600 (0xE10) if that data is not available. - if ( !AddHeading (NMEA0183AISMsg, Heading) ) return false; // 128-136 | 9 True Heading (HDG) - if ( !AddSeconds(NMEA0183AISMsg, Seconds) ) return false; // 137-142 | 6 Seconds in UTC timestamp) - if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 2) ) return false; // 143-144 | 2 Maneuver Indicator: 0 (default) 1, 2 (not delivered within this PGN) - if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 3) ) return false; // 145-147 | 3 Spare - if ( !NMEA0183AISMsg.AddBoolToPayloadBin(RAIM, 1) ) return false; // 148-148 | 1 RAIM flag 0 = RAIM not in use (default), 1 = RAIM in use - if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 19) ) return false; // 149-167 | 19 Radio Status (-> 0 NOT SENT WITH THIS PGN!!!!!) - - if ( !NMEA0183AISMsg.Init("VDM","AI", Prefix) ) return false; - if ( !NMEA0183AISMsg.AddStrField("1") ) return false; - if ( !NMEA0183AISMsg.AddStrField("1") ) return false; - if ( !NMEA0183AISMsg.AddEmptyField() ) return false; - if ( !NMEA0183AISMsg.AddStrField("A") ) return false; - if ( !NMEA0183AISMsg.AddStrField( NMEA0183AISMsg.GetPayload() ) ) return false; - if ( !NMEA0183AISMsg.AddStrField("0") ) return false; // Message 1,2,3 has always Zero Padding - - return true; -} - -// ***************************************************************************** -// https://www.navcen.uscg.gov/?pageName=AISMessagesAStatic# -// AIS class A Static and Voyage Related Data -// Values derived from ParseN2kPGN129794(); -bool SetAISClassAMessage5(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, - uint32_t UserID, uint32_t IMONumber, char *Callsign, char *Name, - uint8_t VesselType, double Length, double Beam, double PosRefStbd, - double PosRefBow, uint16_t ETAdate, double ETAtime, double Draught, - char *Destination, tN2kGNSStype GNSStype, uint8_t DTE ) { - - // AIS Type 5 Message - NMEA0183AISMsg.ClearAIS(); - if ( !AddMessageType(NMEA0183AISMsg, 5) ) return false; // 0 - 5 | 6 Message Type -> Constant: 5 - if ( !AddRepeat(NMEA0183AISMsg, Repeat) ) return false; // 6 - 7 | 2 Repeat Indicator: 0 = default; 3 = do not repeat any more - if ( !AddUserID(NMEA0183AISMsg, UserID) ) return false; // 8 - 37 | 30 MMSI - if ( !NMEA0183AISMsg.AddIntToPayloadBin(1, 2) ) return false; // 38 - 39 | 2 AIS Version -> 0 oder 1 NOT DERIVED FROM N2k, Always 1!!!! - if ( !AddIMONumber(NMEA0183AISMsg, IMONumber) ) return false; // 40 - 69 | 30 IMO Number unisgned - if ( !AddText(NMEA0183AISMsg, Callsign, 42) ) return false; // 70 - 111 | 42 Call Sign WDE4178 -> 7 6-bit characters -> Ascii lt. Table) - if ( !AddText(NMEA0183AISMsg, Name, 120) ) return false; // 112-231 | 120 Vessel Name POINT FERMIN -> 20 6-bit characters -> Ascii lt. Table - if ( !NMEA0183AISMsg.AddIntToPayloadBin(VesselType, 8) ) return false; // 232-239 | 8 Ship Type 0....255 e.g. 31 Towing - if ( !AddDimensions(NMEA0183AISMsg, Length, Beam, PosRefStbd, PosRefBow) ) return false; // 240 - 269 | 30 Dimensions - if ( !AddEPFDFixType(NMEA0183AISMsg, GNSStype) ) return false; // 270-273 | 4 Position Fix Type, 0 (default) - if ( !AddETADateTime(NMEA0183AISMsg, ETAdate, ETAtime) ) return false; // 274 -293 | 20 Estimated time of arrival; MMDDHHMM UTC - if ( !AddStaticDraught(NMEA0183AISMsg, Draught) ) return false; // 294-301 | 8 Maximum Present Static Draught - if ( !AddText(NMEA0183AISMsg, Destination, 120) ) return false; // 302-421 | 120 | 20 Destination 20 6-bit characters - if ( !NMEA0183AISMsg.AddIntToPayloadBin(DTE, 1) ) return false; // 422 | 1 | Data terminal equipment (DTE) ready (0 = available, 1 = not available = default) - if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 1) ) return false; // 423 | 1 | spare - - return true; -} - -// **************************************************************************** -// AIS position report (class B 129039) -> Type 18: Standard Class B CS Position Report -// ParseN2kPGN129039(const tN2kMsg &N2kMsg, uint8_t &MessageID, tN2kAISRepeat &Repeat, uint32_t &UserID, -// double &Latitude, double &Longitude, bool &Accuracy, bool &RAIM, -// uint8_t &Seconds, double &COG, double &SOG, double &Heading, tN2kAISUnit &Unit, -// bool &Display, bool &DSC, bool &Band, bool &Msg22, tN2kAISMode &Mode, bool &State) -// VDM, VDO (AIS VHF Data-link message 18) -bool SetAISClassBMessage18(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, uint32_t UserID, - double Latitude, double Longitude, bool Accuracy, bool RAIM, - uint8_t Seconds, double COG, double SOG, double Heading, tN2kAISUnit Unit, - bool Display, bool DSC, bool Band, bool Msg22, bool Mode, bool State) { - // - NMEA0183AISMsg.ClearAIS(); - if ( !AddMessageType(NMEA0183AISMsg, MessageID) ) return false; // 0 - 5 | 6 Message Type -> Constant: 18 - if ( !AddRepeat(NMEA0183AISMsg, Repeat) ) return false; // 6 - 7 | 2 Repeat Indicator: 0 = default; 3 = do not repeat any more - if ( !AddUserID(NMEA0183AISMsg, UserID) ) return false; // 8 - 37 | 30 MMSI - if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 8) ) return false; // 38-45 | 8 Regional Reserved - if ( !AddSOG(NMEA0183AISMsg, SOG) ) return false; // 46-55 | 10 [m/s -> kts] SOG with one digit x10, 1023 = N/A - if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Accuracy, 1)) return false; // 56 | 1 GPS Accuracy 1 oder 0, Default 0 - if ( !AddLongitude(NMEA0183AISMsg, Longitude) ) return false; // 57-84 | 28 Longitude in Minutes / 10000 - if ( !AddLatitude(NMEA0183AISMsg, Latitude) ) return false; // 85-111 | 27 Latitude in Minutes / 10000 - if ( !AddCOG(NMEA0183AISMsg, COG) ) return false; // 112-123 | 12 Course over ground will be 3600 (0xE10) if that data is not available. - if ( !AddHeading (NMEA0183AISMsg, Heading) ) return false; // 124-132 | 9 True Heading (HDG) - if ( !AddSeconds(NMEA0183AISMsg, Seconds) ) return false; // 133-138 | 6 Seconds in UTC timestamp) - if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 2) ) return false; // 139-140 | 2 Regional Reserved - if ( !NMEA0183AISMsg.AddIntToPayloadBin(Unit, 1) ) return false; // 141 | 1 0=Class B SOTDMA unit 1=Class B CS (Carrier Sense) unit - if ( !NMEA0183AISMsg.AddIntToPayloadBin(Display, 1) ) return false; // 142 | 1 0=No visual display, 1=Has display, (Probably not reliable). - if ( !NMEA0183AISMsg.AddBoolToPayloadBin(DSC, 1) ) return false; // 143 | 1 If 1, unit is attached to a VHF voice radio with DSC capability. - if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Band, 1) ) return false; // 144 | 1 If this flag is 1, the unit can use any part of the marine channel. - if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Msg22, 1) ) return false; // 145 | 1 If 1, unit can accept a channel assignment via Message Type 22. - if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Mode, 1) ) return false; // 146 | 1 Assigned-mode flag: 0 = autonomous mode (default), 1 = assigned mode - if ( !NMEA0183AISMsg.AddBoolToPayloadBin(RAIM, 1) ) return false; // 147 | 1 as for Message Type 1,2,3 - if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 20) ) return false; // 148-167 | 20 Radio Status not in PGN 129039 - - if ( !NMEA0183AISMsg.Init("VDM","AI", Prefix) ) return false; - if ( !NMEA0183AISMsg.AddStrField("1") ) return false; - if ( !NMEA0183AISMsg.AddStrField("1") ) return false; - if ( !NMEA0183AISMsg.AddEmptyField() ) return false; - if ( !NMEA0183AISMsg.AddStrField("B") ) return false; - if ( !NMEA0183AISMsg.AddStrField( NMEA0183AISMsg.GetPayload() ) ) return false; - if ( !NMEA0183AISMsg.AddStrField("0") ) return false; // Message 18, has always Zero Padding - - return true; -} - -// **************************************************************************** -// Type 24: Static Data Report -// Equivalent of a Type 5 message for ships using Class B equipment. Also used to associate an MMSI -// with a name on either class A or class B equipment. -// -// A "Type 24" may be in part A or part B format; According to the standard, parts A and B are expected -// to be broadcast in adjacent pairs; in the real world they may (due to quirks in various aggregation methods) -// be separated by other sentences or even interleaved with different Type 24 pairs; decoders must cope with this. -// The interpretation of some fields in Type B format changes depending on the range of the Type B MMSI field. -// -// 160 bits for part A, 168 bits for part B. -// According to the standard, both the A and B parts are supposed to be 168 bits. -// However, in the wild, A parts are often transmitted with only 160 bits, omitting the spare 7 bits at the end. -// Implementers should be permissive about this. -// -// If the Part Number field is 0, the rest of the message is interpreted as a Part A; -// If it is 1, the rest of the message is interpreted as a Part B; values 2 and 3 are not allowed. -// -// PGN 129809 AIS Class B "CS" Static Data Report, Part A -> AIS VHF Data-link message 24 -// PGN 129810 AIS Class B "CS" Static Data Report, Part B -> AIS VHF Data-link message 24 -// ParseN2kPGN129809 (const tN2kMsg &N2kMsg, uint8_t &MessageID, tN2kAISRepeat &Repeat, uint32_t &UserID, char *Name) -> store to vector -// ParseN2kPGN129810(const tN2kMsg &N2kMsg, uint8_t &MessageID, tN2kAISRepeat &Repeat, uint32_t &UserID, -// uint8_t &VesselType, char *Vendor, char *Callsign, double &Length, double &Beam, -// double &PosRefStbd, double &PosRefBow, uint32_t &MothershipID); -// -// Part A: MessageID, Repeat, UserID, ShipName -> store in vector to call on Part B arrivals!!! -// Part B: MessageID, Repeat, UserID, VesselType (5), Callsign (5), Length & Beam, PosRefBow,.. (5) -bool SetAISClassBMessage24PartA(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, uint32_t UserID, char *Name) { - - bool found = false; - for (size_t i = 0; i < vships.size(); i++) { - if ( vships[i]->_userID == UserID ) { - found = true; - break; - } - } - if ( ! found ) { - std::string nm; - nm+= Name; - vships.push_back(new ship(UserID, nm)); - } - return true; -} - -// *************************************************************************************************************** -bool SetAISClassBMessage24(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, - uint32_t UserID, uint8_t VesselType, char *VendorID, char *Callsign, - double Length, double Beam, double PosRefStbd, double PosRefBow, uint32_t MothershipID ) { - - uint8_t PartNr = 0; // Identifier for the message part number; always 0 for Part A - char *ShipName = (char*)" "; // get from vector to look up for sent Messages Part A - - uint8_t i; - for ( i = 0; i < vships.size(); i++) { - if ( vships[i]->_userID == UserID ) { - ShipName = const_cast( vships[i]->_shipName.c_str() ); - } - } - if ( i > MAX_SHIP_IN_VECTOR ) { - std::vector::iterator it=vships.begin(); - delete *it; - vships.erase(it); - } - - // AIS Type 24 Message - NMEA0183AISMsg.ClearAIS(); - // Common for PART A AND Part B Bit 0 - 39 / len 40 - if ( !AddMessageType(NMEA0183AISMsg, 24) ) return false; // 0 - 5 | 6 Message Type -> Constant: 24 - if ( !AddRepeat(NMEA0183AISMsg, Repeat) ) return false; // 6 - 7 | 2 Repeat Indicator: 0 = default; 3 = do not repeat any more - if ( !AddUserID(NMEA0183AISMsg, UserID) ) return false; // 8 - 37 | 30 MMSI - if ( !NMEA0183AISMsg.AddIntToPayloadBin(PartNr, 2) ) return false; // 38-39 | 2 Part Number 0-1 -> - - // Part A: 40 + 128 = len 168 - if ( !AddText(NMEA0183AISMsg, ShipName, 120) ) return false; // 40-159 | 120 Vessel Name 20 6-bit characters -> Ascii Table - if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 8) ) return false; // 160-167 | 8 Spare - - // https://www.navcen.uscg.gov/?pageName=AISMessagesB - // PART B: 40 + 128 = len 168 - if ( !NMEA0183AISMsg.AddIntToPayloadBin(VesselType, 8) ) return false; // 168-175 | 40-47 | 8 Ship Type 0....99 - if ( !AddText(NMEA0183AISMsg, VendorID, 42) ) return false; // 176-217 | 48-89 | 42 Vendor ID + Unit Model Code + Serial Number - if ( !AddText(NMEA0183AISMsg, Callsign, 42) ) return false; // 218-259 | 90-131 | 42 Call Sign WDE4178 -> 7 6-bit characters, as in Msg Type 5 - if ( !AddDimensions(NMEA0183AISMsg, Length, Beam, PosRefStbd, PosRefBow) ) return false; // 260-289 | 132-161 | 30 Dimensions - if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 6) ) return false; // 290-295 | 162-167 | 6 Spare - - return true; -} - -//****************************************************************************** -// Validations and Unit Transformations -//****************************************************************************** - -// ***************************************************************************** -// 6bit Message Type -> Constant: 1 or 3, 5, 24 etc. -bool AddMessageType(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageType) { - - if (MessageType < 0 || MessageType > 24 ) MessageType = 1; - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(MessageType, 6) ) return false; - return true; -} - -// ***************************************************************************** -// 2bit Repeat Indicator: 0 = default; 3 = do not repeat any more -bool AddRepeat(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t Repeat) { - - if (Repeat < 0 || Repeat > 3) Repeat = 0; - if ( !NMEA0183AISMsg.AddIntToPayloadBin(Repeat, 2) ) return false; - return true; -} - -// ***************************************************************************** -// 30bit UserID = MMSI (9 decimal digits) -bool AddUserID(tNMEA0183AISMsg &NMEA0183AISMsg, uint32_t UserID) { - - if (UserID < 0||UserID > 999999999) UserID = 0; - if ( !NMEA0183AISMsg.AddIntToPayloadBin(UserID, 30) ) return false; - return true; -} - -// ***************************************************************************** -// 30 bit IMO Number -// 0 = not available = default – Not applicable to SAR aircraft -// 0000000001-0000999999 not used -// 0001000000-0009999999 = valid IMO number; -// 0010000000-1073741823 = official flag state number. -bool AddIMONumber(tNMEA0183AISMsg &NMEA0183AISMsg, uint32_t &IMONumber) { - uint32_t iTemp; - ( (IMONumber >= 999999 && IMONumber <= 9999999)||(IMONumber >= 10000000 && IMONumber <= 1073741823) )? iTemp = IMONumber : iTemp = 0; - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 30) ) return false; - return true; -} - -// ***************************************************************************** -// 42bit Callsign alphanumeric value, max 7 six-bit characters -// 120bit Name or Destination -bool AddText(tNMEA0183AISMsg &NMEA0183AISMsg, char *FieldVal, uint8_t length) { - uint8_t len = length/6; - - if ( strlen(FieldVal) > len ) FieldVal[len] = 0; - if ( !NMEA0183AISMsg.AddEncodedCharToPayloadBin(FieldVal, length) ) return false; - return true; -} - -// ***************************************************************************** -// Calculate Dimension A, B, C, D -// double PosRefBow 240-248 | 9 [m] Dimension to Bow, reference for pos. A -// Length - PosRefBow 249-257 | 9 [m] Dimension to Stern, reference for pos. B -// Beam - PosRefStbd 258-263 | 6 [m] Dimension to Port, reference for pos. C -// PosRefStbd 264-269 | 6 [m] Dimension to Starboard, reference for pos. D -// Ship dimensions will be 0 if not available. For the dimensions to bow and stern, -// the special value 511 indicates 511 meters or greater; -// for the dimensions to port and starboard, the special value 63 indicates 63 meters or greater. -// 30 Bit -bool AddDimensions(tNMEA0183AISMsg &NMEA0183AISMsg, double Length, double Beam, double PosRefStbd, double PosRefBow) { - uint16_t _PosRefBow = 0; - uint16_t _PosRefStern = 0; - uint16_t _PosRefStbd = 0; - uint16_t _PosRefPort = 0; - - if (PosRefBow < 0) PosRefBow=0; //could be N2kIsNA - if ( PosRefBow <= 511.0 ) { - _PosRefBow = round(PosRefBow); - } else { - _PosRefBow = 511; - } - if (PosRefStbd < 0 ) PosRefStbd=0; //could be N2kIsNA - if (PosRefStbd <= 63.0 ) { - _PosRefStbd = round(PosRefStbd); - } else { - _PosRefStbd = 63; - } - - if ( !N2kIsNA(Length) ) { - if (Length >= PosRefBow){ - _PosRefStern=round(Length - PosRefBow); - } - if ( _PosRefStern > 511 ) _PosRefStern = 511; - } - if ( !N2kIsNA(Beam) ) { - if (Beam >= PosRefStbd){ - _PosRefPort = round( Beam - PosRefStbd); - } - if ( _PosRefPort > 63 ) _PosRefPort = 63; - } - - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(_PosRefBow, 9) ) return false; - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(_PosRefStern, 9) ) return false; - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(_PosRefPort, 6) ) return false; - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(_PosRefStbd, 6) ) return false; - return true; -} - -// ***************************************************************************** -// 4 Bit Navigational Status e.g.: "Under way sailing" -// Same values used as in tN2kAISNavStatus, so we can use direct numbers -bool AddNavStatus(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t &NavStatus) { - uint8_t iTemp; - (NavStatus >= 0 && NavStatus <= 15 )? iTemp = NavStatus : iTemp = 15; - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 4) ) return false; - return true; -} - -// ***************************************************************************** -// 8bit [rad/s -> degree/minute] Rate of Turn ROT 128 = N/A -// 0 = not turning -// 1…126 = turning right at up to 708 degrees per minute or higher -// 1…-126 = turning left at up to 708 degrees per minute or higher -// 127 = turning right at more than 5deg/30s (No TI available) -// -127 = turning left at more than 5deg/30s (No TI available) -// 128 (80 hex) indicates no turn information available (default) -bool AddROT(tNMEA0183AISMsg &NMEA0183AISMsg, double &rot) { - int8_t iTemp; - if ( N2kIsNA(rot)) iTemp = 128; - else { - rot *= radsToDegMin; - (rot > -128.0 && rot < 128.0)? iTemp = aRoundToInt(rot) : iTemp = 128; - } - - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 8) ) return false; - return true; -} - -// ***************************************************************************** -// 10 bit [m/s -> kts] SOG x10, 1023 = N/A -// Speed over ground is in 0.1-knot resolution from 0 to 102 knots. -// Value 1023 indicates speed is not available, value 1022 indicates 102.2 knots or higher. -bool AddSOG (tNMEA0183AISMsg &NMEA0183AISMsg, double &sog) { - int16_t iTemp; - if ( sog < 0.0 ) iTemp = 1023; - else { - sog *= msTokn; - if (sog > 102.2) iTemp = 1023; - else iTemp = aRoundToInt( 10 * sog ); - } - - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 10) ) return false; - return true; -} - -// ***************************************************************************** -// 28 bit @TODO check negative values -// Values up to plus or minus 180 degrees, East = positive, West = negative. -// A value of 181 degrees (0x6791AC0 hex) indicates that longitude is not available and is the default. -// AIS Longitude is given in in 1/10000 min; divide by 600000.0 to obtain degrees. -bool AddLongitude(tNMEA0183AISMsg &NMEA0183AISMsg, double &Longitude) { - int32_t iTemp; - (Longitude >= -180.0 && Longitude <= 180.0)? iTemp = (int) (Longitude * 600000) : iTemp = 181 * 600000; - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 28) ) return false; - return true; -} - -// ***************************************************************************** -// 27 bit -// Values up to plus or minus 90 degrees, North = positive, South = negative. -// A value of 91 degrees (0x3412140 hex) indicates latitude is not available and is the default. -bool AddLatitude(tNMEA0183AISMsg &NMEA0183AISMsg, double &Latitude) { - int32_t iTemp; - (Latitude >= -90.0 && Latitude <= 90.0)? iTemp = (int) (Latitude * 600000) : iTemp = 91 * 600000; - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 27) ) return false; - return true; -} - -// **************************************************************************** -// 9 bit True Heading (HDG) 0 to 359 degrees, 511 = not available. -bool AddHeading (tNMEA0183AISMsg &NMEA0183AISMsg, double &heading) { - uint16_t iTemp; - if ( N2kIsNA(heading) ) iTemp = 511; - else { - heading *= radToDeg; - (heading >= 0.0 && heading <= 359.0 )? iTemp = aRoundToInt( heading ) : iTemp = 511; - } - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 9) ) return false; - return true; -} - -// ***************************************************************************** -// 12bit Relative to true north, to 0.1 degree precision -bool AddCOG(tNMEA0183AISMsg &NMEA0183AISMsg, double cog) { - int16_t iTemp; - cog *= radToDeg; - if ( cog >= 0.0 && cog < 360.0 ) { iTemp = aRoundToInt( cog * 10 ); } else { iTemp = 3600; } - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 12) ) return false; - return true; -} - -// ***************************************************************************** -// 6bit Seconds in UTC timestamp should be 0-59, except for these special values: -// 60 if time stamp is not available (default) -// 61 if positioning system is in manual input mode -// 62 if Electronic Position Fixing System operates in estimated (dead reckoning) mode, -// 63 if the positioning system is inoperative. -bool AddSeconds (tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t &Seconds) { - uint8_t iTemp; - (Seconds >= 0 && Seconds <= 63 )? iTemp = Seconds : iTemp = 60; - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 6) ) return false; - return true; -} - -// ***************************************************************************** -// 4 bit Position Fix Type, See "EPFD Fix Types" 0 (default) -bool AddEPFDFixType(tNMEA0183AISMsg &NMEA0183AISMsg, tN2kGNSStype &GNSStype) { - // Translate tN2kGNSStype to AIS conventions - // 3 & 4 not defined in AIS -> we take 1 for GPS - uint8_t fixType = 0; - switch (GNSStype) { - case 0: // GPS - case 3: // GPS+SBAS/WAAS - case 4: // GPS+SBAS/WAAS+GLONASS - fixType = 1; break; - case 1: // GLONASS - fixType = 2; break; - case 2: // GPS+GLONASS - fixType = 3; break; - case 5: // Chayka - fixType = 5; break; - case 6: // integrated - fixType = 6; break; - case 7: // surveyed - fixType = 7; break; - case 8: // Galileo - fixType = 8; break; - default: - fixType = 0; - } - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(fixType, 4) ) return false; - return true; -} - -// ***************************************************************************** -// 8 bit Maxiumum present static draught -// In 1/10 m, 255 = draught 25.5 m or greater, 0 = not available = default; in accordance with IMO Resolution A.851 -bool AddStaticDraught(tNMEA0183AISMsg &NMEA0183AISMsg, double &Draught) { - uint8_t staticDraught; - if ( N2kIsNA(Draught) ) staticDraught = 0; - else if (Draught < 0.0) staticDraught = 0; - else if (Draught>25.5) staticDraught = 255; - else staticDraught = (int) ceil( 10.0 * Draught); - - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(staticDraught, 8) ) return false; - return true; -} - -// ***************************************************************************** -// 20bit Estimated time of arrival; MMDDHHMM UTC -// 4 Bits 19-16: month; 1-12; 0 = not available = default -// 5 Bits 15-11: day; 1-31; 0 = not available = default -// 5 Bits 10-6: hour; 0-23; 24 = not available = default -// 6 Bits 5-0: minute; 0-59; 60 = not available = default -// N2k Field #7: ETA Time - Seconds since midnight Bits: 32 Units: s -// Type: Time Resolution: 0.0001 Signed: false e.g. 36000.00 -// N2k Field #8: ETA Date - Days since January 1, 1970 Bits: 16 -// Units: days Type: Date Resolution: 1 Signed: false e.g. 18184 -bool AddETADateTime(tNMEA0183AISMsg &NMEA0183AISMsg, uint16_t &ETAdate, double &ETAtime) { - - uint8_t month = 0; - uint8_t day = 0; - uint8_t hour = 24; - uint8_t minute = 60; - - if (!N2kIsNA(ETAdate) && ETAdate > 0 ) { - tmElements_t tm; - #ifndef _Time_h - time_t t=NMEA0183AISMsg.daysToTime_t(ETAdate); - #else - time_t t=ETAdate*86400; - #endif - NMEA0183AISMsg.breakTime(t, tm); - month = (uint8_t) NMEA0183AISMsg.GetMonth(tm); - day = (uint8_t) NMEA0183AISMsg.GetDay(tm); - } - if ( !N2kIsNA(ETAtime) && ETAtime >= 0 ) { - double temp = ETAtime / 3600; - hour = (int) temp; - minute = (int) ((temp - hour) * 60); - } else { - hour = 24; - minute = 60; - } - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(month, 4) ) return false; - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(day, 5) ) return false; - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(hour, 5) ) return false; - if ( ! NMEA0183AISMsg.AddIntToPayloadBin(minute, 6) ) return false; - return true; -} diff --git a/lib/nmea2ktoais/NMEA0183AISMessages.h b/lib/nmea2ktoais/NMEA0183AISMessages.h deleted file mode 100644 index a124574..0000000 --- a/lib/nmea2ktoais/NMEA0183AISMessages.h +++ /dev/null @@ -1,86 +0,0 @@ -/* -NMEA0183AISMessages.h - -Copyright (c) 2019 Ronnie Zeiller, www.zeiller.eu - -Based on the works of Timo Lappalainen and Eric S. Raymond and Kurt Schwehr https://gpsd.gitlab.io/gpsd/AIVDM.html - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the -Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE -OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -*/ - -#ifndef _tNMEA0183AISMessages_H_ -#define _tNMEA0183AISMessages_H_ - -#include -#include -#include -#include -#include -#include -#include -#include - -#define MAX_SHIP_IN_VECTOR 200 -class ship { -public: - uint32_t _userID; - std::string _shipName; - - ship(uint32_t UserID, std::string ShipName) : _userID(UserID), _shipName(ShipName) {} -}; - - -// Types 1, 2 and 3: Position Report Class A or B -bool SetAISClassABMessage1(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageType, uint8_t Repeat, - uint32_t UserID, double Latitude, double Longitude, bool Accuracy, bool RAIM, uint8_t Seconds, - double COG, double SOG, double Heading, double ROT, uint8_t NavStatus); - -//***************************************************************************** -// AIS Class A Static and Voyage Related Data Message Type 5 -bool SetAISClassAMessage5(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, - uint32_t UserID, uint32_t IMONumber, char *Callsign, char *Name, - uint8_t VesselType, double Length, double Beam, double PosRefStbd, - double PosRefBow, uint16_t ETAdate, double ETAtime, double Draught, - char *Destination, tN2kGNSStype GNSStype, uint8_t DTE ); - -//***************************************************************************** -// AIS position report (class B 129039) -> Standard Class B CS Position Report Message Type 18 Part B -bool SetAISClassBMessage18(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, uint32_t UserID, - double Latitude, double Longitude, bool Accuracy, bool RAIM, - uint8_t Seconds, double COG, double SOG, double Heading, tN2kAISUnit Unit, - bool Display, bool DSC, bool Band, bool Msg22, bool Mode, bool State); - -//***************************************************************************** -// Static Data Report Class B, Message Type 24 -// PGN 129809 Handle AIS Class B "CS" Static Data Report, Part A -bool SetAISClassBMessage24PartA(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, uint32_t UserID, char *Name); - -//***************************************************************************** -// Static Data Report Class B, Message Type 24 -bool SetAISClassBMessage24(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, - uint32_t UserID, uint8_t VesselType, char *VendorID, char *Callsign, - double Length, double Beam, double PosRefStbd, double PosRefBow, uint32_t MothershipID ); - -int numShips(); -inline int32_t aRoundToInt(double x) { - return x >= 0 - ? (int32_t) floor(x + 0.5) - : (int32_t) ceil(x - 0.5); -} -#endif diff --git a/lib/nmea2ktoais/NMEA0183AISMsg.cpp b/lib/nmea2ktoais/NMEA0183AISMsg.cpp deleted file mode 100644 index 6abcf42..0000000 --- a/lib/nmea2ktoais/NMEA0183AISMsg.cpp +++ /dev/null @@ -1,297 +0,0 @@ -/* -NMEA0183AISMsg.cpp - -Copyright (c) 2019 Ronnie Zeiller, www.zeiller.eu -Based on the works of Timo Lappalainen NMEA2000 and NMEA0183 Library - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the -Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE -OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -*/ - -#include "NMEA0183AISMsg.h" -#include -#include -#include -#include -#include -#include -#include - -const char AsciiChar[] = "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_ !\"#$%&\'()*+,-./0123456789:;<=>?"; -const char *tNMEA0183AISMsg::EmptyAISField = "000000"; - -//***************************************************************************** -tNMEA0183AISMsg::tNMEA0183AISMsg() { - ClearAIS(); -} - -//***************************************************************************** -void tNMEA0183AISMsg::ClearAIS() { - - PayloadBin[0]=0; - Payload[0]=0; - iAddPldBin=0; - iAddPld=0; -} - -//***************************************************************************** -// Add 6bit with no data. -bool tNMEA0183AISMsg::AddEmptyFieldToPayloadBin(uint8_t iBits) { - - if ( (iAddPldBin + iBits * 6) >= AIS_BIN_MAX_LEN ) return false; // Is there room for any data - - for (uint8_t i=0;i= AIS_BIN_MAX_LEN ) return false; // Is there room for any data - - AISBitSet bset(ival); - - PayloadBin[iAddPldBin]=0; - uint16_t iAdd=iAddPldBin; - - for(int i = countBits-1; i >= 0 ; i--) { - PayloadBin[iAdd] = bset[i]?'1':'0'; - iAdd++; - } - - iAddPldBin += countBits; - PayloadBin[iAddPldBin]=0; - - return true; -} - -// **************************************************************************** -bool tNMEA0183AISMsg::AddBoolToPayloadBin(bool &bval, uint8_t size) { - int8_t iTemp; - (bval == true)? iTemp = 1 : iTemp = 0; - if ( ! AddIntToPayloadBin(iTemp, size) ) return false; - return true; -} - -// ***************************************************************************** -// converts sval into binary 6-bit AScii encoded string and appends it to PayloadBin -// filled up with "@" == "000000" to given bit-size -bool tNMEA0183AISMsg::AddEncodedCharToPayloadBin(char *sval, size_t countBits) { - - if ( (iAddPldBin + countBits ) >= AIS_BIN_MAX_LEN ) return false; // Is there room for any data - - PayloadBin[iAddPldBin]=0; - std::bitset<6> bs; - char * ptr; - size_t len = strlen(sval); // e.g.: should be 7 for Callsign - if ( len * 6 > countBits ) len = countBits / 6; - - for (int i = 0; i= 0){ - AddIntToPayloadBin(index, 6); - } - } else { - AddIntToPayloadBin(0, 6); - } - } - - PayloadBin[iAddPldBin+1]=0; - - // fill up with "@", also covers empty sval - if ( len * 6 < countBits ) { - for (int i=0;i<(countBits/6-len);i++) { - AddIntToPayloadBin(0, 6); - } - } - PayloadBin[iAddPldBin]=0; - return true; -} - -// ***************************************************************************** -bool tNMEA0183AISMsg::ConvertBinaryAISPayloadBinToAscii(const char *payloadbin) { - uint16_t len; - - len = strlen( payloadbin ) / 6; // 28 - uint32_t offset; - char s[7]; - uint8_t dec; - int i; - for ( i=0; i -#include -#include -#include -#include -#include -#include - - -#ifndef AIS_MSG_MAX_LEN -#define AIS_MSG_MAX_LEN 100 // maximum length of AIS Payload -#endif - -#ifndef AIS_BIN_MAX_LEN -#define AIS_BIN_MAX_LEN 500 // maximum length of AIS Binary Payload (before encoding to Ascii) -#endif - -#define BITSET_LENGTH 120 - -typedef std::bitset AISBitSet; -class tNMEA0183AISMsg : public tNMEA0183Msg { - - protected: // AIS-NMEA - static const char *EmptyAISField; // 6bits 0 not used yet..... - static const char *AsciChar; - - uint16_t iAddPldBin; - char Payload[AIS_MSG_MAX_LEN]; - uint8_t iAddPld; - - public: - char PayloadBin[AIS_BIN_MAX_LEN]; - char PayloadBin2[AIS_BIN_MAX_LEN]; - // Clear message - void ClearAIS(); - - public: - tNMEA0183AISMsg(); - const char *GetPayload(); - const char *GetPayloadType5_Part1(); - const char *GetPayloadType5_Part2(); - const char *GetPayloadType24_PartA(); - const char *GetPayloadType24_PartB(); - const char *GetPayloadBin() const { return PayloadBin; } - - const tNMEA0183AISMsg& BuildMsg5Part1(tNMEA0183AISMsg &AISMsg); - const tNMEA0183AISMsg& BuildMsg5Part2(tNMEA0183AISMsg &AISMsg); - const tNMEA0183AISMsg& BuildMsg24PartA(tNMEA0183AISMsg &AISMsg); - const tNMEA0183AISMsg& BuildMsg24PartB(tNMEA0183AISMsg &AISMsg); - - // Generally Used - bool AddIntToPayloadBin(int32_t ival, uint16_t countBits); - bool AddBoolToPayloadBin(bool &bval, uint8_t size); - bool AddEncodedCharToPayloadBin(char *sval, size_t Length); - bool AddEmptyFieldToPayloadBin(uint8_t iBits); - bool ConvertBinaryAISPayloadBinToAscii(const char *payloadbin); - - // AIS Helper functions - protected: - inline int32_t aRoundToInt(double x) { - return (x >= 0) ? (int32_t) floor(x + 0.5) : (int32_t) ceil(x - 0.5); - } -}; -#endif diff --git a/lib/nmea2ktoais/README.md b/lib/nmea2ktoais/README.md deleted file mode 100644 index 9d83553..0000000 --- a/lib/nmea2ktoais/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# NMEA2000 -> NMEA0183 AIS converter v1.0.0 - -Import from https://github.com/ronzeiller/NMEA0183-AIS - -NMEA0183 AIS library © Ronnie Zeiller, www.zeiller.eu - -Addendum for NMEA2000 and NMEA0183 Library from Timo Lappalainen https://github.com/ttlappalainen - - -## Conversions: - -- NMEA2000 PGN 129038 => AIS CLASS A Position Report (Message Type 1) 1.) 2.) 3.) -- NMEA2000 PGN 129039 => AIS Class B Position Report, Message Type 18 -- NMEA2000 PGN 129794 => AIS Class A Ship Static and Voyage related data, Message Type 5 4.) -- NMEA2000 PGN 129809 => AIS Class B "CS" Static Data Report, making a list of UserID (MMSI) and Ship Names used for Message 24 Part A -- NMEA2000 PGN 129810 => AIS Class B "CS" Static Data Report, Message 24 Part A+B - -### Remarks -1. Message Type could be set to 1 or 3 (identical messages) on demand -2. Maneuver Indicator (not part of NMEA2000 PGN 129038) => will be set to 0 (default) -3. Radio Status (not part of NMEA2000 PGN 129038) => will be set to 0 -4. AIS Version (not part of NMEA2000 PGN 129794) => will be set to 1 - -## Dependencies - -To use this library you need also: - - - NMEA2000 library - - - NMEA0183 library - - - Related CAN libraries. - -## License - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the -Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF -CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE -OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/platformio.ini b/platformio.ini index 4125eb7..4e0773a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -28,7 +28,21 @@ lib_deps = ESPmDNS WiFi Update + nmea2kto183ais=https://github.com/ronzeiller/NMEA0183-AIS#7d2bfab54e3e5bfaab36fe6aa356241baa7251c2 +[devdeps] +lib_deps= + 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 + FS + Preferences + ESPmDNS + WiFi + Update + nmea2kto183ais=symlink://../esp32n2kto183ais [env] platform = espressif32 @ 6.8.1 framework = arduino @@ -67,6 +81,17 @@ lib_deps = adafruit/Adafruit BusIO @ 1.14.5 adafruit/Adafruit Unified Sensor @ 1.1.13 +[env:m5stack-atom-dev] +board = m5stack-atom +lib_deps = + ${devdeps.lib_deps} + fastled/FastLED @ 3.6.0 + +build_flags = + -D BOARD_M5ATOM + ${env.build_flags} +upload_port = /dev/esp32 +upload_protocol = esptool [env:m5stack-atom] board = m5stack-atom lib_deps = ${env.lib_deps} From ec807c6925e52a6e5c6cb2c0112a333b8e6cc229 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Tue, 23 Sep 2025 13:05:28 +0200 Subject: [PATCH 15/48] intermediate: adapt handling to new n2ktoais lib --- lib/nmea2kto0183/N2kDataToNMEA0183.cpp | 200 ++----------------------- 1 file changed, 11 insertions(+), 189 deletions(-) diff --git a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp index 945ad4c..ade2c3f 100644 --- a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp +++ b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp @@ -735,39 +735,7 @@ private: _COG, _SOG, _Heading, _ROT, _NavStatus,_AISTransceiverInformation,_SID)) { -// Debug -#ifdef SERIAL_PRINT_AIS_FIELDS - Serial.println("–––––––––––––––––––––––– Msg 1 ––––––––––––––––––––––––––––––––"); - const double pi = 3.1415926535897932384626433832795; - const double radToDeg = 180.0 / pi; - const double msTokn = 3600.0 / 1852.0; - const double radsToDegMin = 60 * 360.0 / (2 * pi); // [rad/s -> degree/minute] - Serial.print("Repeat: "); - Serial.println(_Repeat); - Serial.print("UserID: "); - Serial.println(_UserID); - Serial.print("Latitude: "); - Serial.println(_Latitude); - Serial.print("Longitude: "); - Serial.println(_Longitude); - Serial.print("Accuracy: "); - Serial.println(_Accuracy); - Serial.print("RAIM: "); - Serial.println(_RAIM); - Serial.print("Seconds: "); - Serial.println(_Seconds); - Serial.print("COG: "); - Serial.println(_COG * radToDeg); - Serial.print("SOG: "); - Serial.println(_SOG * msTokn); - Serial.print("Heading: "); - Serial.println(_Heading * radToDeg); - Serial.print("ROT: "); - Serial.println(_ROT * radsToDegMin); - Serial.print("NavStatus: "); - Serial.println(_NavStatus); -#endif if (_MessageType < 1 || _MessageType > 3) _MessageType=1; //only allow type 1...3 for 129038 if (SetAISClassABMessage1(NMEA0183AISMsg, _MessageType, _Repeat, _UserID, _Latitude, _Longitude, _Accuracy, @@ -776,20 +744,6 @@ private: SendMessage(NMEA0183AISMsg); -#ifdef SERIAL_PRINT_AIS_NMEA - // Debug Print AIS-NMEA - Serial.print(NMEA0183AISMsg.GetPrefix()); - Serial.print(NMEA0183AISMsg.Sender()); - Serial.print(NMEA0183AISMsg.MessageCode()); - for (int i = 0; i < NMEA0183AISMsg.FieldCount(); i++) - { - Serial.print(","); - Serial.print(NMEA0183AISMsg.Field(i)); - } - char buf[7]; - sprintf(buf, "*%02X\r\n", NMEA0183AISMsg.GetCheckSum()); - Serial.print(buf); -#endif } } } // end 129038 AIS Class A Position Report Message 1/3 @@ -826,83 +780,18 @@ private: _AISversion, _GNSStype, _DTE, _AISinfo,_SID)) { -#ifdef SERIAL_PRINT_AIS_FIELDS - // Debug Print N2k Values - Serial.println("––––––––––––––––––––––– Msg 5 –––––––––––––––––––––––––––––––––"); - Serial.print("MessageID: "); - Serial.println(_MessageID); - Serial.print("Repeat: "); - Serial.println(_Repeat); - Serial.print("UserID: "); - Serial.println(_UserID); - Serial.print("IMONumber: "); - Serial.println(_IMONumber); - Serial.print("Callsign: "); - Serial.println(_Callsign); - Serial.print("VesselType: "); - Serial.println(_VesselType); - Serial.print("Name: "); - Serial.println(_Name); - Serial.print("Length: "); - Serial.println(_Length); - Serial.print("Beam: "); - Serial.println(_Beam); - Serial.print("PosRefStbd: "); - Serial.println(_PosRefStbd); - Serial.print("PosRefBow: "); - Serial.println(_PosRefBow); - Serial.print("ETAdate: "); - Serial.println(_ETAdate); - Serial.print("ETAtime: "); - Serial.println(_ETAtime); - Serial.print("Draught: "); - Serial.println(_Draught); - Serial.print("Destination: "); - Serial.println(_Destination); - Serial.print("GNSStype: "); - Serial.println(_GNSStype); - Serial.print("DTE: "); - Serial.println(_DTE); - Serial.println("––––––––––––––––––––––– Msg 5 –––––––––––––––––––––––––––––––––"); -#endif if (SetAISClassAMessage5(NMEA0183AISMsg, _MessageID, _Repeat, _UserID, _IMONumber, _Callsign, _Name, _VesselType, _Length, _Beam, _PosRefStbd, _PosRefBow, _ETAdate, _ETAtime, _Draught, _Destination, _GNSStype, _DTE)) { - - SendMessage(NMEA0183AISMsg.BuildMsg5Part1(NMEA0183AISMsg)); - -#ifdef SERIAL_PRINT_AIS_NMEA - // Debug Print AIS-NMEA Message Type 5, Part 1 - char buf[7]; - Serial.print(NMEA0183AISMsg.GetPrefix()); - Serial.print(NMEA0183AISMsg.Sender()); - Serial.print(NMEA0183AISMsg.MessageCode()); - for (int i = 0; i < NMEA0183AISMsg.FieldCount(); i++) - { - Serial.print(","); - Serial.print(NMEA0183AISMsg.Field(i)); + if (NMEA0183AISMsg.BuildMsg5Part1()){ + SendMessage(NMEA0183AISMsg); } - sprintf(buf, "*%02X\r\n", NMEA0183AISMsg.GetCheckSum()); - Serial.print(buf); -#endif - - SendMessage(NMEA0183AISMsg.BuildMsg5Part2(NMEA0183AISMsg)); - -#ifdef SERIAL_PRINT_AIS_NMEA - // Print AIS-NMEA Message Type 5, Part 2 - Serial.print(NMEA0183AISMsg.GetPrefix()); - Serial.print(NMEA0183AISMsg.Sender()); - Serial.print(NMEA0183AISMsg.MessageCode()); - for (int i = 0; i < NMEA0183AISMsg.FieldCount(); i++) - { - Serial.print(","); - Serial.print(NMEA0183AISMsg.Field(i)); + if (NMEA0183AISMsg.BuildMsg5Part2()){ + SendMessage(NMEA0183AISMsg); } - sprintf(buf, "*%02X\r\n", NMEA0183AISMsg.GetCheckSum()); - Serial.print(buf); -#endif + } } } @@ -941,20 +830,6 @@ private: SendMessage(NMEA0183AISMsg); -#ifdef SERIAL_PRINT_AIS_NMEA - // Debug Print AIS-NMEA - Serial.print(NMEA0183AISMsg.GetPrefix()); - Serial.print(NMEA0183AISMsg.Sender()); - Serial.print(NMEA0183AISMsg.MessageCode()); - for (int i = 0; i < NMEA0183AISMsg.FieldCount(); i++) - { - Serial.print(","); - Serial.print(NMEA0183AISMsg.Field(i)); - } - char buf[7]; - sprintf(buf, "*%02X\r\n", NMEA0183AISMsg.GetCheckSum()); - Serial.print(buf); -#endif } } return; @@ -1005,72 +880,19 @@ private: _Length, _Beam, _PosRefStbd, _PosRefBow, _MothershipID,_AISInfo,_SID)) { -// -#ifdef SERIAL_PRINT_AIS_FIELDS - // Debug Print N2k Values - Serial.println("––––––––––––––––––––––– Msg 24 ––––––––––––––––––––––––––––––––"); - Serial.print("MessageID: "); - Serial.println(_MessageID); - Serial.print("Repeat: "); - Serial.println(_Repeat); - Serial.print("UserID: "); - Serial.println(_UserID); - Serial.print("VesselType: "); - Serial.println(_VesselType); - Serial.print("Vendor: "); - Serial.println(_Vendor); - Serial.print("Callsign: "); - Serial.println(_Callsign); - Serial.print("Length: "); - Serial.println(_Length); - Serial.print("Beam: "); - Serial.println(_Beam); - Serial.print("PosRefStbd: "); - Serial.println(_PosRefStbd); - Serial.print("PosRefBow: "); - Serial.println(_PosRefBow); - Serial.print("MothershipID: "); - Serial.println(_MothershipID); - Serial.println("––––––––––––––––––––––– Msg 24 ––––––––––––––––––––––––––––––––"); -#endif - tNMEA0183AISMsg NMEA0183AISMsg; if (SetAISClassBMessage24(NMEA0183AISMsg, _MessageID, _Repeat, _UserID, _VesselType, _Vendor, _Callsign, _Length, _Beam, _PosRefStbd, _PosRefBow, _MothershipID)) { - - SendMessage(NMEA0183AISMsg.BuildMsg24PartA(NMEA0183AISMsg)); - -#ifdef SERIAL_PRINT_AIS_NMEA - // Debug Print AIS-NMEA - char buf[7]; - Serial.print(NMEA0183AISMsg.GetPrefix()); - Serial.print(NMEA0183AISMsg.Sender()); - Serial.print(NMEA0183AISMsg.MessageCode()); - for (int i = 0; i < NMEA0183AISMsg.FieldCount(); i++) - { - Serial.print(","); - Serial.print(NMEA0183AISMsg.Field(i)); + if (NMEA0183AISMsg.BuildMsg24PartA()){ + SendMessage(NMEA0183AISMsg); } - sprintf(buf, "*%02X\r\n", NMEA0183AISMsg.GetCheckSum()); - Serial.print(buf); -#endif - SendMessage(NMEA0183AISMsg.BuildMsg24PartB(NMEA0183AISMsg)); - -#ifdef SERIAL_PRINT_AIS_NMEA - Serial.print(NMEA0183AISMsg.GetPrefix()); - Serial.print(NMEA0183AISMsg.Sender()); - Serial.print(NMEA0183AISMsg.MessageCode()); - for (int i = 0; i < NMEA0183AISMsg.FieldCount(); i++) - { - Serial.print(","); - Serial.print(NMEA0183AISMsg.Field(i)); + if (NMEA0183AISMsg.BuildMsg24PartB()){ + SendMessage(NMEA0183AISMsg); } - sprintf(buf, "*%02X\r\n", NMEA0183AISMsg.GetCheckSum()); - Serial.print(buf); -#endif + } } return; @@ -1105,7 +927,7 @@ private: data.AISTransceiverInformation, data.AtoNName )){ - //TODO: SendMessage(nmea0183Msg); + SendMessage(nmea0183Msg); } } } From 13eac9508d554a043d4099678352c55888ba82b7 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Tue, 23 Sep 2025 18:29:43 +0200 Subject: [PATCH 16/48] add some helper tools for converting candumps --- tools/getPgnType.py | 29 +++ tools/sendN2K.py | 527 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 556 insertions(+) create mode 100755 tools/getPgnType.py create mode 100644 tools/sendN2K.py diff --git a/tools/getPgnType.py b/tools/getPgnType.py new file mode 100755 index 0000000..edbdbb2 --- /dev/null +++ b/tools/getPgnType.py @@ -0,0 +1,29 @@ +#! /usr/bin/env python3 +import sys +import json + +def err(txt): + print(txt,file=sys.stderr) + sys.exit(1) + +HDR=''' +PGNM_Fast=0 +PGNM_Single=1 +PGNM_ISO=2 +PGN_MODES={ +''' +FOOTER=''' + } +''' +with open(sys.argv[1],"r") as ih: + data=json.load(ih) + pgns=data.get('PGNs') + if pgns is None: + err("no pgns") + print(HDR) + for p in pgns: + t=p['Type'] + pgn=p['PGN'] + if t and pgn: + print(f" {pgn}: PGNM_{t},") + print(FOOTER) \ No newline at end of file diff --git a/tools/sendN2K.py b/tools/sendN2K.py new file mode 100644 index 0000000..c05bbda --- /dev/null +++ b/tools/sendN2K.py @@ -0,0 +1,527 @@ +#! /usr/bin/env python3 +import re +import sys +import os +import datetime + +###generated with getPgnType.py from canboat pgns.json +PGNM_Fast=0 +PGNM_Single=1 +PGNM_ISO=2 +PGN_MODES={ + + 59392: PGNM_Single, + 59904: PGNM_Single, + 60160: PGNM_Single, + 60416: PGNM_Single, + 60416: PGNM_Single, + 60416: PGNM_Single, + 60416: PGNM_Single, + 60416: PGNM_Single, + 60928: PGNM_Single, + 61184: PGNM_Single, + 61184: PGNM_Single, + 61184: PGNM_Single, + 65001: PGNM_Single, + 65002: PGNM_Single, + 65003: PGNM_Single, + 65004: PGNM_Single, + 65005: PGNM_Single, + 65006: PGNM_Single, + 65007: PGNM_Single, + 65008: PGNM_Single, + 65009: PGNM_Single, + 65010: PGNM_Single, + 65011: PGNM_Single, + 65012: PGNM_Single, + 65013: PGNM_Single, + 65014: PGNM_Single, + 65015: PGNM_Single, + 65016: PGNM_Single, + 65017: PGNM_Single, + 65018: PGNM_Single, + 65019: PGNM_Single, + 65020: PGNM_Single, + 65021: PGNM_Single, + 65022: PGNM_Single, + 65023: PGNM_Single, + 65024: PGNM_Single, + 65025: PGNM_Single, + 65026: PGNM_Single, + 65027: PGNM_Single, + 65028: PGNM_Single, + 65029: PGNM_Single, + 65030: PGNM_Single, + 65240: PGNM_ISO, + 65280: PGNM_Single, + 65284: PGNM_Single, + 65285: PGNM_Single, + 65285: PGNM_Single, + 65286: PGNM_Single, + 65286: PGNM_Single, + 65287: PGNM_Single, + 65287: PGNM_Single, + 65288: PGNM_Single, + 65289: PGNM_Single, + 65290: PGNM_Single, + 65292: PGNM_Single, + 65293: PGNM_Single, + 65293: PGNM_Single, + 65302: PGNM_Single, + 65305: PGNM_Single, + 65305: PGNM_Single, + 65305: PGNM_Single, + 65305: PGNM_Single, + 65305: PGNM_Single, + 65309: PGNM_Single, + 65312: PGNM_Single, + 65340: PGNM_Single, + 65341: PGNM_Single, + 65345: PGNM_Single, + 65350: PGNM_Single, + 65359: PGNM_Single, + 65360: PGNM_Single, + 65361: PGNM_Single, + 65371: PGNM_Single, + 65374: PGNM_Single, + 65379: PGNM_Single, + 65408: PGNM_Single, + 65409: PGNM_Single, + 65410: PGNM_Single, + 65420: PGNM_Single, + 65480: PGNM_Single, + 126208: PGNM_Fast, + 126208: PGNM_Fast, + 126208: PGNM_Fast, + 126208: PGNM_Fast, + 126208: PGNM_Fast, + 126208: PGNM_Fast, + 126208: PGNM_Fast, + 126464: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126720: PGNM_Fast, + 126983: PGNM_Fast, + 126984: PGNM_Fast, + 126985: PGNM_Fast, + 126986: PGNM_Fast, + 126987: PGNM_Fast, + 126988: PGNM_Fast, + 126992: PGNM_Single, + 126993: PGNM_Single, + 126996: PGNM_Fast, + 126998: PGNM_Fast, + 127233: PGNM_Fast, + 127237: PGNM_Fast, + 127245: PGNM_Single, + 127250: PGNM_Single, + 127251: PGNM_Single, + 127252: PGNM_Single, + 127257: PGNM_Single, + 127258: PGNM_Single, + 127488: PGNM_Single, + 127489: PGNM_Fast, + 127490: PGNM_Fast, + 127491: PGNM_Fast, + 127493: PGNM_Single, + 127494: PGNM_Fast, + 127495: PGNM_Fast, + 127496: PGNM_Fast, + 127497: PGNM_Fast, + 127498: PGNM_Fast, + 127500: PGNM_Single, + 127501: PGNM_Single, + 127502: PGNM_Single, + 127503: PGNM_Fast, + 127504: PGNM_Fast, + 127505: PGNM_Single, + 127506: PGNM_Fast, + 127507: PGNM_Fast, + 127508: PGNM_Single, + 127509: PGNM_Fast, + 127510: PGNM_Fast, + 127511: PGNM_Single, + 127512: PGNM_Single, + 127513: PGNM_Fast, + 127514: PGNM_Single, + 127744: PGNM_Single, + 127745: PGNM_Single, + 127746: PGNM_Single, + 127750: PGNM_Single, + 127751: PGNM_Single, + 128000: PGNM_Single, + 128001: PGNM_Single, + 128002: PGNM_Single, + 128003: PGNM_Single, + 128006: PGNM_Single, + 128007: PGNM_Single, + 128008: PGNM_Single, + 128259: PGNM_Single, + 128267: PGNM_Single, + 128275: PGNM_Fast, + 128520: PGNM_Fast, + 128538: PGNM_Fast, + 128768: PGNM_Single, + 128769: PGNM_Single, + 128776: PGNM_Single, + 128777: PGNM_Single, + 128778: PGNM_Single, + 128780: PGNM_Single, + 129025: PGNM_Single, + 129026: PGNM_Single, + 129027: PGNM_Single, + 129028: PGNM_Single, + 129029: PGNM_Fast, + 129033: PGNM_Single, + 129038: PGNM_Fast, + 129039: PGNM_Fast, + 129040: PGNM_Fast, + 129041: PGNM_Fast, + 129044: PGNM_Fast, + 129045: PGNM_Fast, + 129283: PGNM_Single, + 129284: PGNM_Fast, + 129285: PGNM_Fast, + 129291: PGNM_Single, + 129301: PGNM_Fast, + 129302: PGNM_Fast, + 129538: PGNM_Fast, + 129539: PGNM_Single, + 129540: PGNM_Fast, + 129541: PGNM_Fast, + 129542: PGNM_Fast, + 129545: PGNM_Fast, + 129546: PGNM_Single, + 129547: PGNM_Fast, + 129549: PGNM_Fast, + 129550: PGNM_Fast, + 129551: PGNM_Fast, + 129556: PGNM_Fast, + 129792: PGNM_Fast, + 129793: PGNM_Fast, + 129794: PGNM_Fast, + 129795: PGNM_Fast, + 129796: PGNM_Fast, + 129797: PGNM_Fast, + 129798: PGNM_Fast, + 129799: PGNM_Fast, + 129800: PGNM_Fast, + 129801: PGNM_Fast, + 129802: PGNM_Fast, + 129803: PGNM_Fast, + 129804: PGNM_Fast, + 129805: PGNM_Fast, + 129806: PGNM_Fast, + 129807: PGNM_Fast, + 129808: PGNM_Fast, + 129808: PGNM_Fast, + 129809: PGNM_Fast, + 129810: PGNM_Fast, + 130052: PGNM_Fast, + 130053: PGNM_Fast, + 130054: PGNM_Fast, + 130060: PGNM_Fast, + 130061: PGNM_Fast, + 130064: PGNM_Fast, + 130065: PGNM_Fast, + 130066: PGNM_Fast, + 130067: PGNM_Fast, + 130068: PGNM_Fast, + 130069: PGNM_Fast, + 130070: PGNM_Fast, + 130071: PGNM_Fast, + 130072: PGNM_Fast, + 130073: PGNM_Fast, + 130074: PGNM_Fast, + 130306: PGNM_Single, + 130310: PGNM_Single, + 130311: PGNM_Single, + 130312: PGNM_Single, + 130313: PGNM_Single, + 130314: PGNM_Single, + 130315: PGNM_Single, + 130316: PGNM_Single, + 130320: PGNM_Fast, + 130321: PGNM_Fast, + 130322: PGNM_Fast, + 130323: PGNM_Fast, + 130324: PGNM_Fast, + 130330: PGNM_Fast, + 130560: PGNM_Single, + 130561: PGNM_Fast, + 130562: PGNM_Fast, + 130563: PGNM_Fast, + 130564: PGNM_Fast, + 130565: PGNM_Fast, + 130566: PGNM_Fast, + 130567: PGNM_Fast, + 130569: PGNM_Fast, + 130570: PGNM_Fast, + 130571: PGNM_Fast, + 130572: PGNM_Fast, + 130573: PGNM_Fast, + 130574: PGNM_Fast, + 130576: PGNM_Single, + 130577: PGNM_Fast, + 130578: PGNM_Fast, + 130579: PGNM_Single, + 130580: PGNM_Fast, + 130581: PGNM_Fast, + 130582: PGNM_Single, + 130583: PGNM_Fast, + 130584: PGNM_Fast, + 130585: PGNM_Single, + 130586: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130816: PGNM_Fast, + 130817: PGNM_Fast, + 130817: PGNM_Fast, + 130818: PGNM_Fast, + 130819: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130820: PGNM_Fast, + 130821: PGNM_Fast, + 130821: PGNM_Fast, + 130822: PGNM_Fast, + 130823: PGNM_Fast, + 130824: PGNM_Fast, + 130824: PGNM_Fast, + 130825: PGNM_Fast, + 130827: PGNM_Fast, + 130828: PGNM_Fast, + 130831: PGNM_Fast, + 130832: PGNM_Fast, + 130833: PGNM_Fast, + 130834: PGNM_Fast, + 130835: PGNM_Fast, + 130836: PGNM_Fast, + 130836: PGNM_Fast, + 130837: PGNM_Fast, + 130837: PGNM_Fast, + 130838: PGNM_Fast, + 130839: PGNM_Fast, + 130840: PGNM_Fast, + 130842: PGNM_Fast, + 130842: PGNM_Fast, + 130842: PGNM_Fast, + 130843: PGNM_Fast, + 130843: PGNM_Fast, + 130845: PGNM_Fast, + 130845: PGNM_Fast, + 130846: PGNM_Fast, + 130846: PGNM_Fast, + 130847: PGNM_Fast, + 130850: PGNM_Fast, + 130850: PGNM_Fast, + 130850: PGNM_Fast, + 130851: PGNM_Fast, + 130856: PGNM_Fast, + 130860: PGNM_Fast, + 130880: PGNM_Fast, + 130881: PGNM_Fast, + 130944: PGNM_Fast, + + } + + + + +def logError(fmt,*args): + print("ERROR:" +fmt%(args)) + +def dataToSep(data,maxbytes=None): + pd=None + dl=int(len(data)/2) + if maxbytes is not None and maxbytes < dl: + dl=maxbytes + for p in range(0,dl): + i=2*p + if pd is None: + pd=data[i:i+2] + else: + pd+=","+data[i:i+2] + return pd + +class CanFrame: + DUMP_PAT=re.compile(r'\(([^)]*)\) *([^ ]*) *([^#]*)#(.*)') + + def __init__(self,ts,pgn,src=1,dst=255,prio=1,data=None): + self.pgn=pgn + self.mode=PGN_MODES.get(pgn) + self.ts=ts + self.src=src + self.dst=dst + self.data=data + self.prio=prio + self.sequence=None + self.frame=None + if self.mode == PGNM_Fast and data is not None and len(self.data) >= 2: + fb=int(data[0:2],16) + self.frame=fb & 0x1f + self.sequence=fb >> 5 + + def key(self): + if self.sequence is None or self.pgn == 0: + return None + return f"{self.pgn}-{self.sequence}" + def getFPNum(self,bytes=False): + if self.frame != 0: + return None + if len(self.data) < 4: + return None + numbytes=int(self.data[2:4],16) + if bytes: + return numbytes + frames=int((numbytes-6-1)/7)+1+1 if numbytes > 6 else 1 + return frames + + def __str__(self): + return f"{self.ts},{self.prio},{self.pgn},{self.src},{self.dst},{int(len(self.data)/2 if self.data else 0)},{dataToSep(self.data)}" + + + @classmethod + def fromDump(cls,line): + '''(1658058069.867835) can0 09F80103#ACAF6C20B79AAC06''' + match=cls.DUMP_PAT.search(line) + if match is None: + logError("no dump pattern in line %s",line) + return + ts=match[1] + dt=datetime.datetime.fromtimestamp(float(ts),tz=datetime.UTC) + tstr=dt.strftime("%F-%T.")+dt.strftime("%f")[0:3] + data=match[4] + hdr=match[3] + hdrval=int(hdr,16) + #see candump2analyzer + src=hdrval & 0xff + prio=(hdrval >> 26) & 0x7 + PF=(hdrval >> 16) & 0xff + PS=(hdrval >> 8) & 0xff + RDP=(hdrval >> 24) & 3 + pgn=0 + if PF < 240: + dst=PS + pgn=(RDP << 16) + (PF << 8) + else: + dst=0xff + pgn=(RDP << 16) + (PF << 8)+PS + return CanFrame(tstr,pgn,src=src,dst=dst,prio=prio,data=data) + +class MultiFrame: + def __init__(self,firstFrame: CanFrame): + self.bytes="" + self.firstFrame=firstFrame + self.numFrames=firstFrame.getFPNum(bytes=False) + self.numBytes=firstFrame.getFPNum(bytes=True) + self.finished=False + self.addFrame(firstFrame) + def addFrame(self,frame:CanFrame): + if self.finished: + return False + if frame.frame is None: + return False + if frame.frame == 0: + self.bytes+=frame.data[4:] + else: + self.bytes+=frame.data[2:] + if frame.frame >= (self.numFrames-1): + self.finished=True + return True + + def __str__(self): + return f"{self.firstFrame.ts},{self.firstFrame.prio},{self.firstFrame.pgn},{self.firstFrame.src},{self.firstFrame.dst},{self.numBytes},{dataToSep(self.bytes,self.numBytes)}" + + +if __name__ == '__main__': + with open (sys.argv[1],"r") as fh: + buffer={} + lnr=0 + for line in fh: + lnr+=1 + frame=CanFrame.fromDump(line) + if frame.sequence is None: + print(frame) + else: + key=frame.key() + mf=buffer.get(key) + mustDelete=False + if mf is None: + if frame.frame != 0: + print(f"floating multi frame in line {lnr}: {frame}",file=sys.stderr) + continue + mf=MultiFrame(frame) + if not mf.finished: + buffer[key]=mf + else: + mf.addFrame(frame) + mustDelete=True + if mf.finished: + print(mf) + del buffer[key] + \ No newline at end of file From 4b03fa5a236ed16010c0ff1dc9fe600e8db948ce Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Tue, 23 Sep 2025 19:07:53 +0200 Subject: [PATCH 17/48] add filter to sendN2K --- tools/sendN2K.py | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) mode change 100644 => 100755 tools/sendN2K.py diff --git a/tools/sendN2K.py b/tools/sendN2K.py old mode 100644 new mode 100755 index c05bbda..0b84c96 --- a/tools/sendN2K.py +++ b/tools/sendN2K.py @@ -3,6 +3,7 @@ import re import sys import os import datetime +import getopt ###generated with getPgnType.py from canboat pgns.json PGNM_Fast=0 @@ -429,7 +430,7 @@ class CanFrame: def key(self): if self.sequence is None or self.pgn == 0: return None - return f"{self.pgn}-{self.sequence}" + return f"{self.pgn}-{self.sequence}-{self.src}" def getFPNum(self,bytes=False): if self.frame != 0: return None @@ -497,14 +498,38 @@ class MultiFrame: def __str__(self): return f"{self.firstFrame.ts},{self.firstFrame.prio},{self.firstFrame.pgn},{self.firstFrame.src},{self.firstFrame.dst},{self.numBytes},{dataToSep(self.bytes,self.numBytes)}" +def usage(): + print(f"usage: {sys.argv[0]} [-q] [-p pgn,pgn,...] file") + sys.exit(1) if __name__ == '__main__': - with open (sys.argv[1],"r") as fh: + try: + opts,args=getopt.getopt(sys.argv[1:],"hp:q") + except getopt.GetoptError as err: + err(err) + pgnlist=[] + quiet=False + for o,a in opts: + if o == '-h': + usage() + elif o == '-q': + quiet=True + elif o == '-p': + pgns=(int(x) for x in a.split(",")) + pgnlist.extend(pgns) + if len(args) < 1: + usage() + hasFilter=len(pgnlist) > 0 + if not quiet and hasFilter: + print(f"PGNs: {','.join(str(x) for x in pgnlist)}") + with open (args[0],"r") as fh: buffer={} lnr=0 for line in fh: lnr+=1 frame=CanFrame.fromDump(line) + if hasFilter and not frame.pgn in pgnlist: + continue if frame.sequence is None: print(frame) else: @@ -513,7 +538,8 @@ if __name__ == '__main__': mustDelete=False if mf is None: if frame.frame != 0: - print(f"floating multi frame in line {lnr}: {frame}",file=sys.stderr) + if not quiet: + print(f"floating multi frame in line {lnr}: {frame}",file=sys.stderr) continue mf=MultiFrame(frame) if not mf.finished: From e5c4f0b17958b5c640d98aa03bb2660a10efb491 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Tue, 23 Sep 2025 20:33:34 +0200 Subject: [PATCH 18/48] add actisense mode to sendN2K --- tools/sendN2K.py | 126 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 112 insertions(+), 14 deletions(-) diff --git a/tools/sendN2K.py b/tools/sendN2K.py index 0b84c96..2d3d7d3 100755 --- a/tools/sendN2K.py +++ b/tools/sendN2K.py @@ -4,6 +4,7 @@ import sys import os import datetime import getopt +import time ###generated with getPgnType.py from canboat pgns.json PGNM_Fast=0 @@ -422,6 +423,7 @@ class CanFrame: self.prio=prio self.sequence=None self.frame=None + self.len=8 if self.mode == PGNM_Fast and data is not None and len(self.data) >= 2: fb=int(data[0:2],16) self.frame=fb & 0x1f @@ -443,7 +445,7 @@ class CanFrame: return frames def __str__(self): - return f"{self.ts},{self.prio},{self.pgn},{self.src},{self.dst},{int(len(self.data)/2 if self.data else 0)},{dataToSep(self.data)}" + return f"{self.ts},{self.prio},{self.pgn},{self.src},{self.dst},{self.len},{dataToSep(self.data)}" @classmethod @@ -476,10 +478,14 @@ class CanFrame: class MultiFrame: def __init__(self,firstFrame: CanFrame): - self.bytes="" - self.firstFrame=firstFrame + self.data="" + self.prio=firstFrame.prio + self.pgn=firstFrame.pgn + self.src=firstFrame.src + self.dst=firstFrame.dst + self.ts=firstFrame.ts self.numFrames=firstFrame.getFPNum(bytes=False) - self.numBytes=firstFrame.getFPNum(bytes=True) + self.len=firstFrame.getFPNum(bytes=True) self.finished=False self.addFrame(firstFrame) def addFrame(self,frame:CanFrame): @@ -488,27 +494,102 @@ class MultiFrame: if frame.frame is None: return False if frame.frame == 0: - self.bytes+=frame.data[4:] + self.data+=frame.data[4:] else: - self.bytes+=frame.data[2:] + self.data+=frame.data[2:] if frame.frame >= (self.numFrames-1): self.finished=True return True def __str__(self): - return f"{self.firstFrame.ts},{self.firstFrame.prio},{self.firstFrame.pgn},{self.firstFrame.src},{self.firstFrame.dst},{self.numBytes},{dataToSep(self.bytes,self.numBytes)}" + return f"{self.ts},{self.prio},{self.pgn},{self.src},{self.dst},{self.len},{dataToSep(self.data,self.numBytes)}" def usage(): - print(f"usage: {sys.argv[0]} [-q] [-p pgn,pgn,...] file") + print(f"usage: {sys.argv[0]} [-q] [-p pgn,pgn,...] [-w waitsec] [ -f plain|actisense] file") sys.exit(1) +F_PLAIN=0 +F_ACT=1 +FORMATS={ + 'plain':F_PLAIN, + 'actisense':F_ACT +} + +MAX_ACT=400 +ACT_ESC=0x10 +ACT_START=0x2 +ACT_N2K=0x93 +ACT_END=0x3 + +class ActBuffer: + def __init__(self): + self.buf=bytearray(MAX_ACT) + self.sum=0 + self.idx=0 + self.clear() + def clear(self): + self.sum=0 + self.idx=2 + self.buf[0:2]=(ACT_ESC,ACT_START) + def add(self,val): + #TODO: len check? + val=val & 0xff + self.buf[self.idx]=val + self.sum = (self.sum + val) & 0xff + self.idx+=1 + if val == ACT_ESC: + self.buf[self.idx]=ACT_ESC + self.idx+=1 + def finalize(self): + self.sum=self.sum % 256 + self.sum = 256 - self.sum if self.sum != 0 else 0 + self.add(self.sum) + self.buf[self.idx]=ACT_ESC + self.idx+=1 + self.buf[self.idx]=ACT_END + self.idx+=1 + +actBuffer=ActBuffer() + +def send_act(frame_like,quiet): + try: + actBuffer.clear() + actBuffer.add(ACT_N2K) + actBuffer.add(frame_like.len+11) + actBuffer.add(frame_like.prio) + pgn=frame_like.pgn + actBuffer.add(pgn) + pgn = pgn >> 8 + actBuffer.add(pgn) + pgn = pgn >> 8; + actBuffer.add(pgn) + actBuffer.add(frame_like.dst) + actBuffer.add(frame_like.src) + #Time + actBuffer.add(0) + actBuffer.add(0) + actBuffer.add(0) + actBuffer.add(0) + + actBuffer.add(frame_like.len) + for i in range(0,frame_like.len*2,2): + actBuffer.add(int(frame_like.data[i:i+2],16)) + actBuffer.finalize() + sys.stdout.buffer.write(memoryview(actBuffer.buf)[0:actBuffer.idx]) + sys.stdout.buffer.flush() + except Exception as e: + if not quiet: + print(f"Error writing actisense for pgn {frame_like.pgn}, idx={actBuffer.idx}: {e}",file=sys.stderr) + if __name__ == '__main__': try: - opts,args=getopt.getopt(sys.argv[1:],"hp:q") - except getopt.GetoptError as err: - err(err) + opts,args=getopt.getopt(sys.argv[1:],"hp:qw:f:") + except getopt.GetoptError as e: + logError(e) pgnlist=[] quiet=False + delay=0.0 + format=F_PLAIN for o,a in opts: if o == '-h': usage() @@ -517,6 +598,12 @@ if __name__ == '__main__': elif o == '-p': pgns=(int(x) for x in a.split(",")) pgnlist.extend(pgns) + elif o == '-w': + delay=float(a) + elif o == '-f': + format=FORMATS.get(a) + if format is None: + logError(f"invalid format {a}, allowed {','.join(FORMATS.keys())}") if len(args) < 1: usage() hasFilter=len(pgnlist) > 0 @@ -531,7 +618,12 @@ if __name__ == '__main__': if hasFilter and not frame.pgn in pgnlist: continue if frame.sequence is None: - print(frame) + if format == F_PLAIN: + print(frame) + else: + send_act(frame,quiet) + if delay > 0: + time.sleep(delay) else: key=frame.key() mf=buffer.get(key) @@ -548,6 +640,12 @@ if __name__ == '__main__': mf.addFrame(frame) mustDelete=True if mf.finished: - print(mf) - del buffer[key] + if format == F_PLAIN: + print(mf) + else: + send_act(mf,quiet) + if mustDelete: + del buffer[key] + if delay > 0: + time.sleep(delay) \ No newline at end of file From e5968b84805e12c6dfedb8f86382baa195d94deb Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Wed, 24 Sep 2025 18:10:06 +0200 Subject: [PATCH 19/48] some error handling and stats to sendN2K --- tools/sendN2K.py | 119 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 87 insertions(+), 32 deletions(-) diff --git a/tools/sendN2K.py b/tools/sendN2K.py index 2d3d7d3..194bbd7 100755 --- a/tools/sendN2K.py +++ b/tools/sendN2K.py @@ -394,8 +394,10 @@ PGN_MODES={ -def logError(fmt,*args): - print("ERROR:" +fmt%(args)) +def logError(fmt,*args,keep=False): + print("ERROR:" +fmt%(args),file=sys.stderr) + if not keep: + sys.exit(1) def dataToSep(data,maxbytes=None): pd=None @@ -453,7 +455,7 @@ class CanFrame: '''(1658058069.867835) can0 09F80103#ACAF6C20B79AAC06''' match=cls.DUMP_PAT.search(line) if match is None: - logError("no dump pattern in line %s",line) + logError("no dump pattern in line %s",line,keep=True) return ts=match[1] dt=datetime.datetime.fromtimestamp(float(ts),tz=datetime.UTC) @@ -476,18 +478,16 @@ class CanFrame: pgn=(RDP << 16) + (PF << 8)+PS return CanFrame(tstr,pgn,src=src,dst=dst,prio=prio,data=data) -class MultiFrame: +class MultiFrame(CanFrame): def __init__(self,firstFrame: CanFrame): + super().__init__(firstFrame.ts,firstFrame.pgn, + src=firstFrame.src,dst=firstFrame.dst,prio=firstFrame.prio) self.data="" - self.prio=firstFrame.prio - self.pgn=firstFrame.pgn - self.src=firstFrame.src - self.dst=firstFrame.dst - self.ts=firstFrame.ts self.numFrames=firstFrame.getFPNum(bytes=False) self.len=firstFrame.getFPNum(bytes=True) self.finished=False self.addFrame(firstFrame) + def addFrame(self,frame:CanFrame): if self.finished: return False @@ -502,18 +502,27 @@ class MultiFrame: return True def __str__(self): - return f"{self.ts},{self.prio},{self.pgn},{self.src},{self.dst},{self.len},{dataToSep(self.data,self.numBytes)}" + return f"{self.ts},{self.prio},{self.pgn},{self.src},{self.dst},{self.len},{dataToSep(self.data,self.len)}" def usage(): print(f"usage: {sys.argv[0]} [-q] [-p pgn,pgn,...] [-w waitsec] [ -f plain|actisense] file") sys.exit(1) -F_PLAIN=0 -F_ACT=1 -FORMATS={ - 'plain':F_PLAIN, - 'actisense':F_ACT -} + +class Format: + F_PLAIN=0 + N_PLAIN='plain' + F_ACT=1 + N_ACT='actisense' + def __init__(self,name,key,merge=True): + self.key=key + self.name=name + self.merge=merge + +FORMATS=[ + Format(Format.N_PLAIN,Format.F_PLAIN), + Format(Format.N_ACT,Format.F_ACT) +] MAX_ACT=400 ACT_ESC=0x10 @@ -551,7 +560,7 @@ class ActBuffer: actBuffer=ActBuffer() -def send_act(frame_like,quiet): +def send_act(frame_like:CanFrame,quiet): try: actBuffer.clear() actBuffer.add(ACT_N2K) @@ -575,11 +584,59 @@ def send_act(frame_like,quiet): for i in range(0,frame_like.len*2,2): actBuffer.add(int(frame_like.data[i:i+2],16)) actBuffer.finalize() - sys.stdout.buffer.write(memoryview(actBuffer.buf)[0:actBuffer.idx]) + written=sys.stdout.buffer.write(memoryview(actBuffer.buf)[0:actBuffer.idx]) + if (written != actBuffer.idx): + if not quiet: + logError(f"actisense not all bytes written {written}/{actBuffer.idx} for pgn={frame_like.pgn} ts={frame_like.ts}",keep=True) sys.stdout.buffer.flush() + return True except Exception as e: if not quiet: print(f"Error writing actisense for pgn {frame_like.pgn}, idx={actBuffer.idx}: {e}",file=sys.stderr) + return False + +class Counters: + C_OK=1 + C_FAIL=2 + C_FRAME=3 + TITLES={ + C_OK:'OK', + C_FAIL:'FAIL', + C_FRAME:'FRAMES' + } + def __init__(self): + self.counters={} + for i in self.TITLES.keys(): + self.counters[i]=0 + def add(self,idx:int): + if idx not in self.TITLES.keys(): + return + self.counters[idx]+=1 + def __str__(self): + rt=None + for i in self.TITLES.keys(): + v=f"{self.TITLES[i]}:{self.counters[i]}" + if rt is None: + rt=v + else: + rt+=","+v + return rt + +def writeOut(frame:CanFrame,format:Format,quiet:bool,counters:Counters): + rt=False + if format.key == Format.F_ACT: + rt= send_act(frame,quiet) + elif format.key == Format.F_PLAIN: + print(frame) + rt=True + counters.add(Counters.C_OK if rt else Counters.C_FAIL) + return rt + +def findFormat(name:str)->Format: + for f in FORMATS: + if f.name == name: + return f + return None if __name__ == '__main__': try: @@ -589,7 +646,7 @@ if __name__ == '__main__': pgnlist=[] quiet=False delay=0.0 - format=F_PLAIN + format=findFormat(Format.N_PLAIN) for o,a in opts: if o == '-h': usage() @@ -601,14 +658,15 @@ if __name__ == '__main__': elif o == '-w': delay=float(a) elif o == '-f': - format=FORMATS.get(a) + format=findFormat(a) if format is None: - logError(f"invalid format {a}, allowed {','.join(FORMATS.keys())}") + logError(f"invalid format {a}, allowed {','.join(x.name for x in FORMATS)}") if len(args) < 1: usage() hasFilter=len(pgnlist) > 0 if not quiet and hasFilter: - print(f"PGNs: {','.join(str(x) for x in pgnlist)}") + print(f"PGNs: {','.join(str(x) for x in pgnlist)}",file=sys.stderr) + counters=Counters() with open (args[0],"r") as fh: buffer={} lnr=0 @@ -617,11 +675,9 @@ if __name__ == '__main__': frame=CanFrame.fromDump(line) if hasFilter and not frame.pgn in pgnlist: continue - if frame.sequence is None: - if format == F_PLAIN: - print(frame) - else: - send_act(frame,quiet) + counters.add(Counters.C_FRAME) + if frame.sequence is None or not format.merge: + writeOut(frame,format,quiet,counters=counters) if delay > 0: time.sleep(delay) else: @@ -640,12 +696,11 @@ if __name__ == '__main__': mf.addFrame(frame) mustDelete=True if mf.finished: - if format == F_PLAIN: - print(mf) - else: - send_act(mf,quiet) + writeOut(mf,format,quiet,counters=counters) if mustDelete: del buffer[key] if delay > 0: - time.sleep(delay) + time.sleep(delay) + if not quiet: + print(f"STATISTICS: {counters}",file=sys.stderr) \ No newline at end of file From b7cd8c6bdd971d885ea6575ed9648c57f8a2ad43 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Wed, 24 Sep 2025 18:29:22 +0200 Subject: [PATCH 20/48] add pass format to sendN2K --- tools/sendN2K.py | 53 +++++++++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/tools/sendN2K.py b/tools/sendN2K.py index 194bbd7..4d7c11f 100755 --- a/tools/sendN2K.py +++ b/tools/sendN2K.py @@ -411,11 +411,22 @@ def dataToSep(data,maxbytes=None): else: pd+=","+data[i:i+2] return pd +class Format: + F_PLAIN=0 + N_PLAIN='plain' + F_ACT=1 + N_ACT='actisense' + F_PASS=2 + N_PASS='pass' + def __init__(self,name,key,merge=True): + self.key=key + self.name=name + self.merge=merge class CanFrame: DUMP_PAT=re.compile(r'\(([^)]*)\) *([^ ]*) *([^#]*)#(.*)') - def __init__(self,ts,pgn,src=1,dst=255,prio=1,data=None): + def __init__(self,ts,pgn,src=1,dst=255,prio=1,dev=None,hdr=None,data=None): self.pgn=pgn self.mode=PGN_MODES.get(pgn) self.ts=ts @@ -426,6 +437,8 @@ class CanFrame: self.sequence=None self.frame=None self.len=8 + self.dev=dev + self.hdr=hdr if self.mode == PGNM_Fast and data is not None and len(self.data) >= 2: fb=int(data[0:2],16) self.frame=fb & 0x1f @@ -446,9 +459,19 @@ class CanFrame: frames=int((numbytes-6-1)/7)+1+1 if numbytes > 6 else 1 return frames + def _formatTs(self): + dt=datetime.datetime.fromtimestamp(self.ts,tz=datetime.UTC) + return dt.strftime("%F-%T.")+dt.strftime("%f")[0:3] + def __str__(self): - return f"{self.ts},{self.prio},{self.pgn},{self.src},{self.dst},{self.len},{dataToSep(self.data)}" + return f"{self._formatTs()},{self.prio},{self.pgn},{self.src},{self.dst},{self.len},{dataToSep(self.data)}" + def printOut(self,format:Format): + if format.key == Format.F_PASS: + return f"({self.ts}) {self.dev} {self.hdr}#{self.data}" + else: + return str(self) + @classmethod def fromDump(cls,line): @@ -458,8 +481,6 @@ class CanFrame: logError("no dump pattern in line %s",line,keep=True) return ts=match[1] - dt=datetime.datetime.fromtimestamp(float(ts),tz=datetime.UTC) - tstr=dt.strftime("%F-%T.")+dt.strftime("%f")[0:3] data=match[4] hdr=match[3] hdrval=int(hdr,16) @@ -476,12 +497,13 @@ class CanFrame: else: dst=0xff pgn=(RDP << 16) + (PF << 8)+PS - return CanFrame(tstr,pgn,src=src,dst=dst,prio=prio,data=data) + return CanFrame(float(ts),pgn,src=src,dst=dst,prio=prio,data=data,dev=match[2],hdr=hdr) class MultiFrame(CanFrame): def __init__(self,firstFrame: CanFrame): super().__init__(firstFrame.ts,firstFrame.pgn, - src=firstFrame.src,dst=firstFrame.dst,prio=firstFrame.prio) + src=firstFrame.src,dst=firstFrame.dst,prio=firstFrame.prio, + dev=firstFrame.dev,hdr=firstFrame.hdr) self.data="" self.numFrames=firstFrame.getFPNum(bytes=False) self.len=firstFrame.getFPNum(bytes=True) @@ -502,26 +524,19 @@ class MultiFrame(CanFrame): return True def __str__(self): - return f"{self.ts},{self.prio},{self.pgn},{self.src},{self.dst},{self.len},{dataToSep(self.data,self.len)}" + return f"{self._formatTs()},{self.prio},{self.pgn},{self.src},{self.dst},{self.len},{dataToSep(self.data,self.len)}" def usage(): print(f"usage: {sys.argv[0]} [-q] [-p pgn,pgn,...] [-w waitsec] [ -f plain|actisense] file") sys.exit(1) -class Format: - F_PLAIN=0 - N_PLAIN='plain' - F_ACT=1 - N_ACT='actisense' - def __init__(self,name,key,merge=True): - self.key=key - self.name=name - self.merge=merge + FORMATS=[ Format(Format.N_PLAIN,Format.F_PLAIN), - Format(Format.N_ACT,Format.F_ACT) + Format(Format.N_ACT,Format.F_ACT), + Format(Format.N_PASS,Format.F_PASS,False) ] MAX_ACT=400 @@ -626,8 +641,8 @@ def writeOut(frame:CanFrame,format:Format,quiet:bool,counters:Counters): rt=False if format.key == Format.F_ACT: rt= send_act(frame,quiet) - elif format.key == Format.F_PLAIN: - print(frame) + else: + print(frame.printOut(format)) rt=True counters.add(Counters.C_OK if rt else Counters.C_FAIL) return rt From 78aafd308a54753ed97da0ea9a9d264487fbf462 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Wed, 24 Sep 2025 20:30:44 +0200 Subject: [PATCH 21/48] seasmart for sendN2K --- tools/sendN2K.py | 73 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 5 deletions(-) diff --git a/tools/sendN2K.py b/tools/sendN2K.py index 4d7c11f..d3dca5b 100755 --- a/tools/sendN2K.py +++ b/tools/sendN2K.py @@ -418,6 +418,8 @@ class Format: N_ACT='actisense' F_PASS=2 N_PASS='pass' + F_SEASMART=3 + N_SEASMART="seasmart" def __init__(self,name,key,merge=True): self.key=key self.name=name @@ -536,7 +538,8 @@ def usage(): FORMATS=[ Format(Format.N_PLAIN,Format.F_PLAIN), Format(Format.N_ACT,Format.F_ACT), - Format(Format.N_PASS,Format.F_PASS,False) + Format(Format.N_PASS,Format.F_PASS,False), + Format(Format.N_SEASMART,Format.F_SEASMART) ] MAX_ACT=400 @@ -575,7 +578,41 @@ class ActBuffer: actBuffer=ActBuffer() -def send_act(frame_like:CanFrame,quiet): + +LB=b'0000000000000' +B_STAR=0x2a +class SeasmartBuffer: + def __init__(self): + self.buf=bytearray(500) + self.idx=0 + self.clear() + def clear(self): + self.idx=0 + def addB(self,bv): + l=len(bv) + self.buf[self.idx:self.idx+l]=bv + self.idx+=l + def addVal(self,val,blen=2): + hs=hex(val)[2:].encode() + if len(hs) != blen: + hs=(LB+hs)[-blen:] + self.addB(hs) + + def finalize(self): + sum=0 + self.buf[self.idx]=B_STAR + self.idx+=1 + for b in memoryview(self.buf)[1:]: + if b == B_STAR: + break + sum ^= b + sum = sum & 0xff + self.addVal(sum) + self.addB(b'\x0d\x0a') + +seasmartBuffer=SeasmartBuffer() + +def send_act(frame_like:CanFrame,quiet,stream): try: actBuffer.clear() actBuffer.add(ACT_N2K) @@ -599,17 +636,41 @@ def send_act(frame_like:CanFrame,quiet): for i in range(0,frame_like.len*2,2): actBuffer.add(int(frame_like.data[i:i+2],16)) actBuffer.finalize() - written=sys.stdout.buffer.write(memoryview(actBuffer.buf)[0:actBuffer.idx]) + written=stream.write(memoryview(actBuffer.buf)[0:actBuffer.idx]) if (written != actBuffer.idx): if not quiet: logError(f"actisense not all bytes written {written}/{actBuffer.idx} for pgn={frame_like.pgn} ts={frame_like.ts}",keep=True) - sys.stdout.buffer.flush() + stream.flush() return True except Exception as e: if not quiet: print(f"Error writing actisense for pgn {frame_like.pgn}, idx={actBuffer.idx}: {e}",file=sys.stderr) return False +BK=b',' +def send_seasmart(frame_like:CanFrame,quiet,stream): + try: + seasmartBuffer.clear() + seasmartBuffer.addB(b'$PCDIN,') + seasmartBuffer.addVal(frame_like.pgn,6) + seasmartBuffer.addB(BK) + seasmartBuffer.addVal(int(time.time()),8) + seasmartBuffer.addB(BK) + seasmartBuffer.addVal(frame_like.src) + seasmartBuffer.addB(BK) + seasmartBuffer.addB(frame_like.data.encode()) + seasmartBuffer.finalize() + written=stream.write(memoryview(seasmartBuffer.buf)[0:seasmartBuffer.idx]) + if (written != seasmartBuffer.idx): + if not quiet: + raise Exception(f"seasmart not all bytes written {written}/{seasmartBuffer.idx} for pgn={frame_like.pgn} ts={frame_like.ts}") + stream.flush() + return True + except Exception as e: + if not quiet: + logError(f"writing seasmart for pgn {frame_like.pgn}, idx={seasmartBuffer.idx}: {e}",keep=True) + return False + class Counters: C_OK=1 C_FAIL=2 @@ -640,7 +701,9 @@ class Counters: def writeOut(frame:CanFrame,format:Format,quiet:bool,counters:Counters): rt=False if format.key == Format.F_ACT: - rt= send_act(frame,quiet) + rt= send_act(frame,quiet,sys.stdout.buffer) + elif format.key == Format.F_SEASMART: + rt= send_seasmart(frame,quiet,sys.stdout.buffer) else: print(frame.printOut(format)) rt=True From 448af708d4f1a58d987e286460955dd0ee8a031f Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Thu, 25 Sep 2025 17:37:59 +0200 Subject: [PATCH 22/48] fill timestamp for actisense with frame timestamp in sendN2K --- tools/sendN2K.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tools/sendN2K.py b/tools/sendN2K.py index d3dca5b..8921348 100755 --- a/tools/sendN2K.py +++ b/tools/sendN2K.py @@ -529,7 +529,7 @@ class MultiFrame(CanFrame): return f"{self._formatTs()},{self.prio},{self.pgn},{self.src},{self.dst},{self.len},{dataToSep(self.data,self.len)}" def usage(): - print(f"usage: {sys.argv[0]} [-q] [-p pgn,pgn,...] [-w waitsec] [ -f plain|actisense] file") + print(f"usage: {sys.argv[0]} [-q] [-p pgn,pgn,...] [-w waitsec] [ -f plain|actisense] file",file=sys.stderr) sys.exit(1) @@ -627,10 +627,11 @@ def send_act(frame_like:CanFrame,quiet,stream): actBuffer.add(frame_like.dst) actBuffer.add(frame_like.src) #Time - actBuffer.add(0) - actBuffer.add(0) - actBuffer.add(0) - actBuffer.add(0) + ts=int(frame_like.ts) + actBuffer.add(ts>>24) + actBuffer.add(ts>>16) + actBuffer.add(ts>>8) + actBuffer.add(ts) actBuffer.add(frame_like.len) for i in range(0,frame_like.len*2,2): @@ -654,7 +655,7 @@ def send_seasmart(frame_like:CanFrame,quiet,stream): seasmartBuffer.addB(b'$PCDIN,') seasmartBuffer.addVal(frame_like.pgn,6) seasmartBuffer.addB(BK) - seasmartBuffer.addVal(int(time.time()),8) + seasmartBuffer.addVal(int(frame_like.ts),8) seasmartBuffer.addB(BK) seasmartBuffer.addVal(frame_like.src) seasmartBuffer.addB(BK) From 24502e423eb2dc547fe246c4ed5f108726adab29 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Thu, 25 Sep 2025 19:25:54 +0200 Subject: [PATCH 23/48] set talker/channel when converting AIS from N2K, new lib version n2ktoais --- lib/nmea2kto0183/N2kDataToNMEA0183.cpp | 43 +++++++++++++++++++++----- platformio.ini | 2 +- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp index ade2c3f..1a865e2 100644 --- a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp +++ b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp @@ -708,6 +708,32 @@ private: } } + //helper for converting the AIS transceiver info to talker/channel + + void setTalkerChannel(tNMEA0183AISMsg &msg, tN2kAISTransceiverInformation &transceiver){ + bool channelA=true; + bool own=false; + switch (transceiver){ + case tN2kAISTransceiverInformation::N2kaischannel_A_VDL_reception: + channelA=true; + own=false; + break; + case tN2kAISTransceiverInformation::N2kaischannel_B_VDL_reception: + channelA=false; + own=false; + break; + case tN2kAISTransceiverInformation::N2kaischannel_A_VDL_transmission: + channelA=true; + own=true; + break; + case tN2kAISTransceiverInformation::N2kaischannel_B_VDL_transmission: + channelA=false; + own=true; + break; + } + msg.SetChannelAndTalker(channelA,own); + } + //***************************************************************************** // 129038 AIS Class A Position Report (Message 1, 2, 3) void HandleAISClassAPosReport(const tN2kMsg &N2kMsg) @@ -736,7 +762,7 @@ private: { - + setTalkerChannel(NMEA0183AISMsg,_AISTransceiverInformation); if (_MessageType < 1 || _MessageType > 3) _MessageType=1; //only allow type 1...3 for 129038 if (SetAISClassABMessage1(NMEA0183AISMsg, _MessageType, _Repeat, _UserID, _Latitude, _Longitude, _Accuracy, _RAIM, _Seconds, _COG, _SOG, _Heading, _ROT, _NavStatus)) @@ -779,11 +805,10 @@ private: _Length, _Beam, _PosRefStbd, _PosRefBow, _ETAdate, _ETAtime, _Draught, _Destination,21, _AISversion, _GNSStype, _DTE, _AISinfo,_SID)) { - - + setTalkerChannel(NMEA0183AISMsg,_AISinfo); if (SetAISClassAMessage5(NMEA0183AISMsg, _MessageID, _Repeat, _UserID, _IMONumber, _Callsign, _Name, _VesselType, _Length, _Beam, _PosRefStbd, _PosRefBow, _ETAdate, _ETAtime, _Draught, _Destination, - _GNSStype, _DTE)) + _GNSStype, _DTE,_AISversion)) { if (NMEA0183AISMsg.BuildMsg5Part1()){ SendMessage(NMEA0183AISMsg); @@ -815,15 +840,15 @@ private: tN2kAISUnit _Unit; bool _Display, _DSC, _Band, _Msg22, _State; tN2kAISMode _Mode; - tN2kAISTransceiverInformation _AISTranceiverInformation; + tN2kAISTransceiverInformation _AISTransceiverInformation; 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,_SID)) + _Seconds, _COG, _SOG, _AISTransceiverInformation, _Heading, _Unit, _Display, _DSC, _Band, _Msg22, _Mode, _State,_SID)) { tNMEA0183AISMsg NMEA0183AISMsg; - + setTalkerChannel(NMEA0183AISMsg,_AISTransceiverInformation); if (SetAISClassBMessage18(NMEA0183AISMsg, _MessageID, _Repeat, _UserID, _Latitude, _Longitude, _Accuracy, _RAIM, _Seconds, _COG, _SOG, _Heading, _Unit, _Display, _DSC, _Band, _Msg22, _Mode, _State)) { @@ -851,6 +876,7 @@ private: { tNMEA0183AISMsg NMEA0183AISMsg; + setTalkerChannel(NMEA0183AISMsg,_AISInfo); if (SetAISClassBMessage24PartA(NMEA0183AISMsg, _MessageID, _Repeat, _UserID, _Name)) { } @@ -881,7 +907,7 @@ private: { tNMEA0183AISMsg NMEA0183AISMsg; - + setTalkerChannel(NMEA0183AISMsg,_AISInfo); if (SetAISClassBMessage24(NMEA0183AISMsg, _MessageID, _Repeat, _UserID, _VesselType, _Vendor, _Callsign, _Length, _Beam, _PosRefStbd, _PosRefBow, _MothershipID)) { @@ -905,6 +931,7 @@ private: tN2kAISAtoNReportData data; if (ParseN2kPGN129041(N2kMsg,data)){ tNMEA0183AISMsg nmea0183Msg; + setTalkerChannel(nmea0183Msg,data.AISTransceiverInformation); if (SetAISMessage21( nmea0183Msg, data.Repeat, diff --git a/platformio.ini b/platformio.ini index 4e0773a..89187e0 100644 --- a/platformio.ini +++ b/platformio.ini @@ -28,7 +28,7 @@ lib_deps = ESPmDNS WiFi Update - nmea2kto183ais=https://github.com/ronzeiller/NMEA0183-AIS#7d2bfab54e3e5bfaab36fe6aa356241baa7251c2 + nmea2kto183ais=https://github.com/wellenvogel/esp32n2kto183ais.git#20250925 [devdeps] lib_deps= From 9633abc4811ad6dc3952b6f1b5dccbe83168d4d5 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Fri, 26 Sep 2025 19:56:44 +0200 Subject: [PATCH 24/48] correct timestamp for pass format --- tools/sendN2K.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/tools/sendN2K.py b/tools/sendN2K.py index 8921348..e5c55db 100755 --- a/tools/sendN2K.py +++ b/tools/sendN2K.py @@ -470,14 +470,16 @@ class CanFrame: def printOut(self,format:Format): if format.key == Format.F_PASS: - return f"({self.ts}) {self.dev} {self.hdr}#{self.data}" + return f"({self.ts:.6f}) {self.dev} {self.hdr}#{self.data}" else: return str(self) @classmethod - def fromDump(cls,line): + def fromDump(cls,line:str): '''(1658058069.867835) can0 09F80103#ACAF6C20B79AAC06''' + if line is None or line == '': + return None match=cls.DUMP_PAT.search(line) if match is None: logError("no dump pattern in line %s",line,keep=True) @@ -588,9 +590,13 @@ class SeasmartBuffer: self.clear() def clear(self): self.idx=0 - def addB(self,bv): + def addB(self,bv,mlen=None): l=len(bv) - self.buf[self.idx:self.idx+l]=bv + if mlen is not None and mlen < l: + l=mlen + self.buf[self.idx:self.idx+l]=memoryview(bv)[0:l] + else: + self.buf[self.idx:self.idx+l]=bv self.idx+=l def addVal(self,val,blen=2): hs=hex(val)[2:].encode() @@ -659,7 +665,7 @@ def send_seasmart(frame_like:CanFrame,quiet,stream): seasmartBuffer.addB(BK) seasmartBuffer.addVal(frame_like.src) seasmartBuffer.addB(BK) - seasmartBuffer.addB(frame_like.data.encode()) + seasmartBuffer.addB(frame_like.data.encode(),mlen=frame_like.len*2) seasmartBuffer.finalize() written=stream.write(memoryview(seasmartBuffer.buf)[0:seasmartBuffer.idx]) if (written != seasmartBuffer.idx): @@ -752,6 +758,8 @@ if __name__ == '__main__': for line in fh: lnr+=1 frame=CanFrame.fromDump(line) + if frame is None: + continue if hasFilter and not frame.pgn in pgnlist: continue counters.add(Counters.C_FRAME) From 60d06cd9eea8a863fdccb3d21b2beeec96b751d3 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Fri, 26 Sep 2025 19:57:08 +0200 Subject: [PATCH 25/48] remove wrong addEmptyField --- lib/nmea2kto0183/N2kDataToNMEA0183.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp index 1a865e2..eeeea60 100644 --- a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp +++ b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp @@ -951,7 +951,6 @@ private: data.AssignedModeFlag, data.GNSSType, data.AtoNStatus, - data.AISTransceiverInformation, data.AtoNName )){ SendMessage(nmea0183Msg); From 3f22164b1d435095fcaedac23dab8fbdc904f353 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Fri, 26 Sep 2025 20:00:30 +0200 Subject: [PATCH 26/48] use forked NMEA2000 lib --- platformio.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/platformio.ini b/platformio.ini index 89187e0..53bdb61 100644 --- a/platformio.ini +++ b/platformio.ini @@ -18,7 +18,7 @@ extra_configs= [basedeps] lib_deps = - ttlappalainen/NMEA2000-library @ 4.22.0 + ttlappalainen_NMEA2000=https://github.com/wellenvogel/NMEA2000.git#20250926 ttlappalainen/NMEA0183 @ 1.10.1 ArduinoJson @ 6.18.5 AsyncTCP-esphome @ 2.0.1 @@ -28,11 +28,11 @@ lib_deps = ESPmDNS WiFi Update - nmea2kto183ais=https://github.com/wellenvogel/esp32n2kto183ais.git#20250925 + nmea2kto183ais=https://github.com/wellenvogel/esp32n2kto183ais.git#20250926 [devdeps] lib_deps= - ttlappalainen/NMEA2000-library @ 4.22.0 + ttlappalainen_NMEA2000=symlink://../NMEA2000 ttlappalainen/NMEA0183 @ 1.10.1 ArduinoJson @ 6.18.5 AsyncTCP-esphome @ 2.0.1 From d0966159c02e1ee946572e029734ff333b3ce793 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Sun, 28 Sep 2025 18:37:26 +0200 Subject: [PATCH 27/48] separate building AIS class 24 --- lib/nmea2kto0183/N2kDataToNMEA0183.cpp | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp index eeeea60..e2b61fd 100644 --- a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp +++ b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp @@ -879,6 +879,7 @@ private: setTalkerChannel(NMEA0183AISMsg,_AISInfo); if (SetAISClassBMessage24PartA(NMEA0183AISMsg, _MessageID, _Repeat, _UserID, _Name)) { + SendMessage(NMEA0183AISMsg); } } return; @@ -908,17 +909,10 @@ private: tNMEA0183AISMsg NMEA0183AISMsg; setTalkerChannel(NMEA0183AISMsg,_AISInfo); - if (SetAISClassBMessage24(NMEA0183AISMsg, _MessageID, _Repeat, _UserID, _VesselType, _Vendor, _Callsign, + if (SetAISClassBMessage24PartB(NMEA0183AISMsg, _MessageID, _Repeat, _UserID, _VesselType, _Vendor, _Callsign, _Length, _Beam, _PosRefStbd, _PosRefBow, _MothershipID)) { - if (NMEA0183AISMsg.BuildMsg24PartA()){ - SendMessage(NMEA0183AISMsg); - } - - if (NMEA0183AISMsg.BuildMsg24PartB()){ - SendMessage(NMEA0183AISMsg); - } - + SendMessage(NMEA0183AISMsg); } } return; From df9b377b316603902c78de0d72bc077f6a35a5b3 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Sun, 28 Sep 2025 18:47:17 +0200 Subject: [PATCH 28/48] move the nmea2ktoais functions back into our code base. --- lib/nmea2ktoais/NMEA0183AISMessages.cpp | 601 ++++++++++++++++++++++++ lib/nmea2ktoais/NMEA0183AISMessages.h | 87 ++++ lib/nmea2ktoais/NMEA0183AISMsg.cpp | 201 ++++++++ lib/nmea2ktoais/NMEA0183AISMsg.h | 97 ++++ lib/nmea2ktoais/README.md | 73 +++ 5 files changed, 1059 insertions(+) create mode 100644 lib/nmea2ktoais/NMEA0183AISMessages.cpp create mode 100644 lib/nmea2ktoais/NMEA0183AISMessages.h create mode 100644 lib/nmea2ktoais/NMEA0183AISMsg.cpp create mode 100644 lib/nmea2ktoais/NMEA0183AISMsg.h create mode 100644 lib/nmea2ktoais/README.md diff --git a/lib/nmea2ktoais/NMEA0183AISMessages.cpp b/lib/nmea2ktoais/NMEA0183AISMessages.cpp new file mode 100644 index 0000000..7a64d70 --- /dev/null +++ b/lib/nmea2ktoais/NMEA0183AISMessages.cpp @@ -0,0 +1,601 @@ +/* +NMEA0183AISMessages.cpp + +Copyright (c) 2019 Ronnie Zeiller + +Based on the works of Timo Lappalainen NMEA2000 and NMEA0183 Library +Thanks to Eric S. Raymond (https://gpsd.gitlab.io/gpsd/AIVDM.html) +and Kurt Schwehr for their informations on AIS encoding. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + +#include "NMEA0183AISMessages.h" +#include +#include +#include +//#include +//#include +#include +#include +#include "NMEA0183AISMsg.h" + +const double pi=3.1415926535897932384626433832795; +const double kmhToms=1000.0/3600.0; +const double knToms=1852.0/3600.0; +const double degToRad=pi/180.0; +const double radToDeg=180.0/pi; +const double msTokmh=3600.0/1000.0; +const double msTokn=3600.0/1852.0; +const double nmTom=1.852*1000; +const double mToFathoms=0.546806649; +const double mToFeet=3.2808398950131; +const double radsToDegMin = 60 * 360.0 / (2 * pi); // [rad/s -> degree/minute] + + +// ************************ Helper for AIS *********************************** +static bool AddMessageType(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageType); +static bool AddRepeat(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t Repeat); +static bool AddUserID(tNMEA0183AISMsg &NMEA0183AISMsg, uint32_t UserID); +static bool AddIMONumber(tNMEA0183AISMsg &NMEA0183AISMsg, uint32_t &IMONumber); +static bool AddText(tNMEA0183AISMsg &NMEA0183AISMsg, char *FieldVal, uint8_t length); +//static bool AddVesselType(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t VesselType); +static bool AddDimensions(tNMEA0183AISMsg &NMEA0183AISMsg, double Length, double Beam, double PosRefStbd, double PosRefBow); +static bool AddNavStatus(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t &NavStatus); +static bool AddROT(tNMEA0183AISMsg &NMEA0183AISMsg, double &rot); +static bool AddSOG (tNMEA0183AISMsg &NMEA0183AISMsg, double &sog); +static bool AddLongitude(tNMEA0183AISMsg &NMEA0183AISMsg, double &Longitude); +static bool AddLatitude(tNMEA0183AISMsg &NMEA0183AISMsg, double &Latitude); +static bool AddHeading (tNMEA0183AISMsg &NMEA0183AISMsg, double &heading); +static bool AddCOG(tNMEA0183AISMsg &NMEA0183AISMsg, double cog); +static bool AddSeconds (tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t &Seconds); +static bool AddEPFDFixType(tNMEA0183AISMsg &NMEA0183AISMsg, tN2kGNSStype &GNSStype); +static bool AddStaticDraught(tNMEA0183AISMsg &NMEA0183AISMsg, double &Draught); +static bool AddETADateTime(tNMEA0183AISMsg &NMEA0183AISMsg, uint16_t &ETAdate, double &ETAtime); + +//***************************************************************************** +// Types 1, 2 and 3: Position Report Class A or B -> https://gpsd.gitlab.io/gpsd/AIVDM.html +// total of 168 bits, occupying one AIVDM sentence +// Example: !AIVDM,1,1,,A,133m@ogP00PD;88MD5MTDww@2D7k,0*46 +// Payload: Payload: 133m@ogP00PD;88MD5MTDww@2D7k +// Message type 1 has a payload length of 168 bits. +// because AIS encodes messages using a 6-bits ASCII mechanism and 168 divided by 6 is 28. +// +// Got values from: ParseN2kPGN129038() +bool SetAISClassABMessage1( tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageType, uint8_t Repeat, + uint32_t UserID, double Latitude, double Longitude, bool Accuracy, bool RAIM, uint8_t Seconds, + double COG, double SOG, double Heading, double ROT, uint8_t NavStatus ) { + + NMEA0183AISMsg.ClearAIS(); + if ( !AddMessageType(NMEA0183AISMsg, MessageType) ) return false; // 0 - 5 | 6 Message Type -> Constant: 1 + if ( !AddRepeat(NMEA0183AISMsg, Repeat) ) return false; // 6 - 7 | 2 Repeat Indicator: 0 = default; 3 = do not repeat any more + if ( !AddUserID(NMEA0183AISMsg, UserID) ) return false; // 8 - 37 | 30 MMSI + if ( !AddNavStatus(NMEA0183AISMsg, NavStatus) ) return false; // 38-41 | 4 Navigational Status e.g.: "Under way sailing" + if ( !AddROT(NMEA0183AISMsg, ROT) ) return false; // 42-49 | 8 Rate of Turn (ROT) + if ( !AddSOG(NMEA0183AISMsg, SOG) ) return false; // 50-59 | 10 [m/s -> kts] SOG with one digit x10, 1023 = N/A + if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Accuracy) ) return false;// 60 | 1 GPS Accuracy 1 oder 0, Default 0 + if ( !AddLongitude(NMEA0183AISMsg, Longitude) ) return false; // 61-88 | 28 Longitude in Minutes / 10000 + if ( !AddLatitude(NMEA0183AISMsg, Latitude) ) return false; // 89-115 | 27 Latitude in Minutes / 10000 + if ( !AddCOG(NMEA0183AISMsg, COG) ) return false; // 116-127 | 12 Course over ground will be 3600 (0xE10) if that data is not available. + if ( !AddHeading (NMEA0183AISMsg, Heading) ) return false; // 128-136 | 9 True Heading (HDG) + if ( !AddSeconds(NMEA0183AISMsg, Seconds) ) return false; // 137-142 | 6 Seconds in UTC timestamp) + if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 2) ) return false; // 143-144 | 2 Maneuver Indicator: 0 (default) 1, 2 (not delivered within this PGN) + if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 3) ) return false; // 145-147 | 3 Spare + if ( !NMEA0183AISMsg.AddBoolToPayloadBin(RAIM) ) return false; // 148-148 | 1 RAIM flag 0 = RAIM not in use (default), 1 = RAIM in use + if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 19) ) return false; // 149-167 | 19 Radio Status (-> 0 NOT SENT WITH THIS PGN!!!!!) + if ( !NMEA0183AISMsg.InitAis()) return false; + int padBits=0; + if ( !NMEA0183AISMsg.AddStrField( NMEA0183AISMsg.GetPayloadFix(padBits) ) ) return false; + if ( !NMEA0183AISMsg.AddUInt32Field(padBits) ) return false; + return true; +} + +// ***************************************************************************** +// https://www.navcen.uscg.gov/?pageName=AISMessagesAStatic# +// AIS class A Static and Voyage Related Data +// Values derived from ParseN2kPGN129794(); +bool SetAISClassAMessage5(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, + uint32_t UserID, uint32_t IMONumber, char *Callsign, char *Name, + uint8_t VesselType, double Length, double Beam, double PosRefStbd, + double PosRefBow, uint16_t ETAdate, double ETAtime, double Draught, + char *Destination, tN2kGNSStype GNSStype, uint8_t DTE, + tN2kAISVersion AISversion) { + + // AIS Type 5 Message + NMEA0183AISMsg.ClearAIS(); + if ( !AddMessageType(NMEA0183AISMsg, 5) ) return false; // 0 - 5 | 6 Message Type -> Constant: 5 + if ( !AddRepeat(NMEA0183AISMsg, Repeat) ) return false; // 6 - 7 | 2 Repeat Indicator: 0 = default; 3 = do not repeat any more + if ( !AddUserID(NMEA0183AISMsg, UserID) ) return false; // 8 - 37 | 30 MMSI + if ( !NMEA0183AISMsg.AddIntToPayloadBin((uint32_t)AISversion, 2) ) + return false; // 38 - 39 | 2 AIS Version -> 0 oder 1 NOT DERIVED FROM N2k, Always 1!!!! + if ( !AddIMONumber(NMEA0183AISMsg, IMONumber) ) return false; // 40 - 69 | 30 IMO Number unisgned + if ( !AddText(NMEA0183AISMsg, Callsign, 42) ) return false; // 70 - 111 | 42 Call Sign WDE4178 -> 7 6-bit characters -> Ascii lt. Table) + if ( !AddText(NMEA0183AISMsg, Name, 120) ) return false; // 112-231 | 120 Vessel Name POINT FERMIN -> 20 6-bit characters -> Ascii lt. Table + if ( !NMEA0183AISMsg.AddIntToPayloadBin(VesselType, 8) ) return false; // 232-239 | 8 Ship Type 0....255 e.g. 31 Towing + if ( !AddDimensions(NMEA0183AISMsg, Length, Beam, PosRefStbd, PosRefBow) ) return false; // 240 - 269 | 30 Dimensions + if ( !AddEPFDFixType(NMEA0183AISMsg, GNSStype) ) return false; // 270-273 | 4 Position Fix Type, 0 (default) + if ( !AddETADateTime(NMEA0183AISMsg, ETAdate, ETAtime) ) return false; // 274 -293 | 20 Estimated time of arrival; MMDDHHMM UTC + if ( !AddStaticDraught(NMEA0183AISMsg, Draught) ) return false; // 294-301 | 8 Maximum Present Static Draught + if ( !AddText(NMEA0183AISMsg, Destination, 120) ) return false; // 302-421 | 120 | 20 Destination 20 6-bit characters + if ( !NMEA0183AISMsg.AddIntToPayloadBin(DTE, 1) ) return false; // 422 | 1 | Data terminal equipment (DTE) ready (0 = available, 1 = not available = default) + if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 1) ) return false; // 423 | 1 | spare + + return true; +} + +// **************************************************************************** +// AIS position report (class B 129039) -> Type 18: Standard Class B CS Position Report +// PGN129039 +// ParseN2kAISClassBPosition(const tN2kMsg &N2kMsg, uint8_t &MessageID, tN2kAISRepeat &Repeat, uint32_t &UserID, +// double &Latitude, double &Longitude, bool &Accuracy, bool &RAIM, +// uint8_t &Seconds, double &COG, double &SOG, tN2kAISTransceiverInformation &AISTransceiverInformation, +// double &Heading, tN2kAISUnit &Unit, bool &Display, bool &DSC, bool &Band, bool &Msg22, tN2kAISMode &Mode, +// bool &State) +// VDM, VDO (AIS VHF Data-link message 18) +bool SetAISClassBMessage18(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, uint32_t UserID, + double Latitude, double Longitude, bool Accuracy, bool RAIM, + uint8_t Seconds, double COG, double SOG, double Heading, tN2kAISUnit Unit, + bool Display, bool DSC, bool Band, bool Msg22, bool Mode, bool State) { + // + NMEA0183AISMsg.ClearAIS(); + if ( !AddMessageType(NMEA0183AISMsg, MessageID) ) return false; // 0 - 5 | 6 Message Type -> Constant: 18 + if ( !AddRepeat(NMEA0183AISMsg, Repeat) ) return false; // 6 - 7 | 2 Repeat Indicator: 0 = default; 3 = do not repeat any more + if ( !AddUserID(NMEA0183AISMsg, UserID) ) return false; // 8 - 37 | 30 MMSI + if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 8) ) return false; // 38-45 | 8 Regional Reserved + if ( !AddSOG(NMEA0183AISMsg, SOG) ) return false; // 46-55 | 10 [m/s -> kts] SOG with one digit x10, 1023 = N/A + if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Accuracy)) return false; // 56 | 1 GPS Accuracy 1 oder 0, Default 0 + if ( !AddLongitude(NMEA0183AISMsg, Longitude) ) return false; // 57-84 | 28 Longitude in Minutes / 10000 + if ( !AddLatitude(NMEA0183AISMsg, Latitude) ) return false; // 85-111 | 27 Latitude in Minutes / 10000 + if ( !AddCOG(NMEA0183AISMsg, COG) ) return false; // 112-123 | 12 Course over ground will be 3600 (0xE10) if that data is not available. + if ( !AddHeading (NMEA0183AISMsg, Heading) ) return false; // 124-132 | 9 True Heading (HDG) + if ( !AddSeconds(NMEA0183AISMsg, Seconds) ) return false; // 133-138 | 6 Seconds in UTC timestamp) + if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 2) ) return false; // 139-140 | 2 Regional Reserved + if ( !NMEA0183AISMsg.AddIntToPayloadBin(Unit, 1) ) return false; // 141 | 1 0=Class B SOTDMA unit 1=Class B CS (Carrier Sense) unit + if ( !NMEA0183AISMsg.AddIntToPayloadBin(Display, 1) ) return false; // 142 | 1 0=No visual display, 1=Has display, (Probably not reliable). + if ( !NMEA0183AISMsg.AddBoolToPayloadBin(DSC) ) return false; // 143 | 1 If 1, unit is attached to a VHF voice radio with DSC capability. + if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Band) ) return false; // 144 | 1 If this flag is 1, the unit can use any part of the marine channel. + if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Msg22)) return false; // 145 | 1 If 1, unit can accept a channel assignment via Message Type 22. + if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Mode) ) return false; // 146 | 1 Assigned-mode flag: 0 = autonomous mode (default), 1 = assigned mode + if ( !NMEA0183AISMsg.AddBoolToPayloadBin(RAIM) ) return false; // 147 | 1 as for Message Type 1,2,3 + if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 20) ) return false; // 148-167 | 20 Radio Status not in PGN 129039 + if ( !NMEA0183AISMsg.InitAis()) return false; + int padBits=0; + if ( !NMEA0183AISMsg.AddStrField( NMEA0183AISMsg.GetPayloadFix(padBits) ) ) return false; + if ( !NMEA0183AISMsg.AddUInt32Field(padBits) ) return false; + + return true; +} + +// **************************************************************************** +// Type 24: Static Data Report +// Equivalent of a Type 5 message for ships using Class B equipment. Also used to associate an MMSI +// with a name on either class A or class B equipment. +// +// A "Type 24" may be in part A or part B format; According to the standard, parts A and B are expected +// to be broadcast in adjacent pairs; in the real world they may (due to quirks in various aggregation methods) +// be separated by other sentences or even interleaved with different Type 24 pairs; decoders must cope with this. +// The interpretation of some fields in Type B format changes depending on the range of the Type B MMSI field. +// +// 160 bits for part A, 168 bits for part B. +// According to the standard, both the A and B parts are supposed to be 168 bits. +// However, in the wild, A parts are often transmitted with only 160 bits, omitting the spare 7 bits at the end. +// Implementers should be permissive about this. +// +// If the Part Number field is 0, the rest of the message is interpreted as a Part A; +// If it is 1, the rest of the message is interpreted as a Part B; values 2 and 3 are not allowed. +// +// PGN 129809 AIS Class B "CS" Static Data Report, Part A -> AIS VHF Data-link message 24 +// PGN 129810 AIS Class B "CS" Static Data Report, Part B -> AIS VHF Data-link message 24 +// ParseN2kPGN129809 (const tN2kMsg &N2kMsg, uint8_t &MessageID, tN2kAISRepeat &Repeat, uint32_t &UserID, char *Name) -> store to vector +// ParseN2kPGN129810(const tN2kMsg &N2kMsg, uint8_t &MessageID, tN2kAISRepeat &Repeat, uint32_t &UserID, +// uint8_t &VesselType, char *Vendor, char *Callsign, double &Length, double &Beam, +// double &PosRefStbd, double &PosRefBow, uint32_t &MothershipID); +// +// Part A: MessageID, Repeat, UserID, ShipName -> store in vector to call on Part B arrivals!!! +// Part B: MessageID, Repeat, UserID, VesselType (5), Callsign (5), Length & Beam, PosRefBow,.. (5) +bool SetAISClassBMessage24PartA(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, uint32_t UserID, char *Name) { + // AIS Type 24 Message + NMEA0183AISMsg.ClearAIS(); + // Common for PART A AND Part B Bit 0 - 39 / len 40 + if ( !AddMessageType(NMEA0183AISMsg, 24) ) return false; // 0 - 5 | 6 Message Type -> Constant: 24 + if ( !AddRepeat(NMEA0183AISMsg, Repeat) ) return false; // 6 - 7 | 2 Repeat Indicator: 0 = default; 3 = do not repeat any more + if ( !AddUserID(NMEA0183AISMsg, UserID) ) return false; // 8 - 37 | 30 MMSI + if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 2) ) return false; // 38-39 | 2 Part Number 0-1 -> + // Part A: 40 + 128 = len 168 + if ( !AddText(NMEA0183AISMsg, Name, 120) ) return false; // 40-159 | 120 Vessel Name 20 6-bit characters -> Ascii Table + if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 8) ) return false; // 160-167 | 8 Spare + if ( !NMEA0183AISMsg.InitAis() ) return false; + int padBits=0; + if ( !NMEA0183AISMsg.AddStrField( NMEA0183AISMsg.GetPayloadFix(padBits) ) ) return false; + if ( !NMEA0183AISMsg.AddUInt32Field(padBits) ) return false; + return true; +} + +// *************************************************************************************************************** +bool SetAISClassBMessage24PartB(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, + uint32_t UserID, uint8_t VesselType, char *VendorID, char *Callsign, + double Length, double Beam, double PosRefStbd, double PosRefBow, uint32_t MothershipID ) { + + + // AIS Type 24 Message + NMEA0183AISMsg.ClearAIS(); + // Common for PART A AND Part B Bit 0 - 39 / len 40 + if ( !AddMessageType(NMEA0183AISMsg, 24) ) return false; // 0 - 5 | 6 Message Type -> Constant: 24 + if ( !AddRepeat(NMEA0183AISMsg, Repeat) ) return false; // 6 - 7 | 2 Repeat Indicator: 0 = default; 3 = do not repeat any more + if ( !AddUserID(NMEA0183AISMsg, UserID) ) return false; // 8 - 37 | 30 MMSI + if ( !NMEA0183AISMsg.AddIntToPayloadBin(1, 2) ) return false; // 38-39 | 2 Part Number 0-1 -> + + // https://www.navcen.uscg.gov/?pageName=AISMessagesB + // PART B: 40 + 128 = len 168 + if ( !NMEA0183AISMsg.AddIntToPayloadBin(VesselType, 8) ) return false; // 168-175 | 40-47 | 8 Ship Type 0....99 + if ( !AddText(NMEA0183AISMsg, VendorID, 42) ) return false; // 176-217 | 48-89 | 42 Vendor ID + Unit Model Code + Serial Number + if ( !AddText(NMEA0183AISMsg, Callsign, 42) ) return false; // 218-259 | 90-131 | 42 Call Sign WDE4178 -> 7 6-bit characters, as in Msg Type 5 + if ( !AddDimensions(NMEA0183AISMsg, Length, Beam, PosRefStbd, PosRefBow) ) return false; // 260-289 | 132-161 | 30 Dimensions + if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 6) ) return false; // 290-295 | 162-167 | 6 Spare + if ( !NMEA0183AISMsg.InitAis() ) return false; + int padBits=0; + if ( !NMEA0183AISMsg.AddStrField( NMEA0183AISMsg.GetPayloadFix(padBits) ) ) return false; + if ( !NMEA0183AISMsg.AddUInt32Field(padBits) ) return false; + return true; +} + +// **************************************************************************** +// AIS ATON report (129041) -> Type 21: Position and status report for aids-to-navigation +// PGN129041 + +bool SetAISMessage21(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t Repeat, uint32_t UserID, + double Latitude, double Longitude, bool Accuracy, bool RAIM, + uint8_t Seconds, double Length, double Beam, double PositionReferenceStarboard, + double PositionReferenceTrueNord, tN2kAISAtoNType Type, bool OffPositionIndicator, + bool VirtualAtoNFlag, bool AssignedModeFlag, tN2kGNSStype GNSSType, uint8_t AtoNStatus, + char * atonName ) { + // + NMEA0183AISMsg.ClearAIS(); + if ( !AddMessageType(NMEA0183AISMsg, 21) ) return false; // 0 - 5 | 6 Message Type -> Constant: 18 + if ( !AddRepeat(NMEA0183AISMsg, Repeat) ) return false; // 6 - 7 | 2 Repeat Indicator: 0 = default; 3 = do not repeat any more + if ( !AddUserID(NMEA0183AISMsg, UserID) ) return false; // 8 - 37 | 30 MMSI + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(Type,5)) return false; // | 5 aid type + //the name must be split: + //if it's > 120 bits the rest goes to the last parameter + if ( !NMEA0183AISMsg.AddEncodedCharToPayloadBin(atonName,120)) + return false; // | 120 name + if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Accuracy) ) return false; // | 1 accuracy + if ( !AddLongitude(NMEA0183AISMsg,Longitude)) return false; // | 28 lon + if ( !AddLatitude(NMEA0183AISMsg,Latitude)) return false; // | 27 lat + if ( !AddDimensions(NMEA0183AISMsg, Length, Beam, + PositionReferenceStarboard, PositionReferenceTrueNord)) return false; // | 30 dim + if ( !AddEPFDFixType(NMEA0183AISMsg,GNSSType)) return false; // | 4 fix type + if ( !AddSeconds(NMEA0183AISMsg,Seconds)) return false; // | 6 second + if ( !NMEA0183AISMsg.AddBoolToPayloadBin(OffPositionIndicator)) + return false; // | 1 off + if ( !NMEA0183AISMsg.AddIntToPayloadBin(0,8)) return false; // | 8 reserverd + if ( !NMEA0183AISMsg.AddBoolToPayloadBin(RAIM)) return false; // | 1 raim + if ( !NMEA0183AISMsg.AddBoolToPayloadBin(VirtualAtoNFlag)) + return false; // | 1 virt + if ( !NMEA0183AISMsg.AddBoolToPayloadBin(AssignedModeFlag)) + return false; // | 1 assigned + if ( !NMEA0183AISMsg.AddIntToPayloadBin(0,1)) return false; // | 1 spare + size_t l=strlen(atonName); + if (l >=20){ + uint8_t bitlen=(l-20)*6; + if (bitlen > 88) bitlen=88; + if ( !NMEA0183AISMsg.AddEncodedCharToPayloadBin(atonName+20,bitlen)) return false; // | name + } + if ( !NMEA0183AISMsg.InitAis() ) return false; + int padBits=0; + if ( !NMEA0183AISMsg.AddStrField( NMEA0183AISMsg.GetPayload(padBits) ) ) return false; + if ( !NMEA0183AISMsg.AddUInt32Field(padBits) ) return false; + + return true; +} + +//****************************************************************************** +// Validations and Unit Transformations +//****************************************************************************** + +// ***************************************************************************** +// 6bit Message Type -> Constant: 1 or 3, 5, 24 etc. +bool AddMessageType(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageType) { + + if (MessageType < 0 || MessageType > 24 ) MessageType = 1; + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(MessageType, 6) ) return false; + return true; +} + +// ***************************************************************************** +// 2bit Repeat Indicator: 0 = default; 3 = do not repeat any more +bool AddRepeat(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t Repeat) { + + if (Repeat < 0 || Repeat > 3) Repeat = 0; + if ( !NMEA0183AISMsg.AddIntToPayloadBin(Repeat, 2) ) return false; + return true; +} + +// ***************************************************************************** +// 30bit UserID = MMSI (9 decimal digits) +bool AddUserID(tNMEA0183AISMsg &NMEA0183AISMsg, uint32_t UserID) { + + if (UserID < 0||UserID > 999999999) UserID = 0; + if ( !NMEA0183AISMsg.AddIntToPayloadBin(UserID, 30) ) return false; + return true; +} + +// ***************************************************************************** +// 30 bit IMO Number +// 0 = not available = default – Not applicable to SAR aircraft +// 0000000001-0000999999 not used +// 0001000000-0009999999 = valid IMO number; +// 0010000000-1073741823 = official flag state number. +bool AddIMONumber(tNMEA0183AISMsg &NMEA0183AISMsg, uint32_t &IMONumber) { + uint32_t iTemp; + ( (IMONumber >= 999999 && IMONumber <= 9999999)||(IMONumber >= 10000000 && IMONumber <= 1073741823) )? iTemp = IMONumber : iTemp = 0; + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 30) ) return false; + return true; +} + +// ***************************************************************************** +// 42bit Callsign alphanumeric value, max 7 six-bit characters +// 120bit Name or Destination +bool AddText(tNMEA0183AISMsg &NMEA0183AISMsg, char *FieldVal, uint8_t length) { + uint8_t len = length/6; + if ( strlen(FieldVal) > len ) FieldVal[len] = 0; + if ( !NMEA0183AISMsg.AddEncodedCharToPayloadBin(FieldVal, length) ) return false; + return true; +} + +// ***************************************************************************** +// Calculate Dimension A, B, C, D +// double PosRefBow 240-248 | 9 [m] Dimension to Bow, reference for pos. A +// Length - PosRefBow 249-257 | 9 [m] Dimension to Stern, reference for pos. B +// Beam - PosRefStbd 258-263 | 6 [m] Dimension to Port, reference for pos. C +// PosRefStbd 264-269 | 6 [m] Dimension to Starboard, reference for pos. D +// Ship dimensions will be 0 if not available. For the dimensions to bow and stern, +// the special value 511 indicates 511 meters or greater; +// for the dimensions to port and starboard, the special value 63 indicates 63 meters or greater. +// 30 Bit +bool AddDimensions(tNMEA0183AISMsg &NMEA0183AISMsg, double Length, double Beam, double PosRefStbd, double PosRefBow) { + uint16_t _PosRefBow = 0; + uint16_t _PosRefStern = 0; + uint16_t _PosRefStbd = 0; + uint16_t _PosRefPort = 0; + + if ( PosRefBow >= 0.0 && PosRefBow <= 511.0 ) { + _PosRefBow = ceil(PosRefBow); + } else { + _PosRefBow = 511; + } + + if ( PosRefStbd >= 0.0 && PosRefStbd <= 63.0 ) { + _PosRefStbd = ceil(PosRefStbd); + } else { + _PosRefStbd = 63; + } + + if ( !N2kIsNA(Length) ) { + _PosRefStern = ceil( Length ) - _PosRefBow; + if ( _PosRefStern < 0 ) _PosRefStern = 0; + if ( _PosRefStern > 511 ) _PosRefStern = 511; + } + if ( !N2kIsNA(Beam) ) { + _PosRefPort = ceil( Beam ) - _PosRefStbd; + if ( _PosRefPort < 0 ) _PosRefPort = 0; + if ( _PosRefPort > 63 ) _PosRefPort = 63; + } + + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(_PosRefBow, 9) ) return false; + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(_PosRefStern, 9) ) return false; + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(_PosRefPort, 6) ) return false; + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(_PosRefStbd, 6) ) return false; + return true; +} + +// ***************************************************************************** +// 4 Bit Navigational Status e.g.: "Under way sailing" +// Same values used as in tN2kAISNavStatus, so we can use direct numbers +bool AddNavStatus(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t &NavStatus) { + uint8_t iTemp; + (NavStatus >= 0 && NavStatus <= 15 )? iTemp = NavStatus : iTemp = 15; + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 4) ) return false; + return true; +} + +// ***************************************************************************** +// 8bit [rad/s -> degree/minute] Rate of Turn ROT 128 = N/A +// 0 = not turning +// 1…126 = turning right at up to 708 degrees per minute or higher +// 1…-126 = turning left at up to 708 degrees per minute or higher +// 127 = turning right at more than 5deg/30s (No TI available) +// -127 = turning left at more than 5deg/30s (No TI available) +// 128 (80 hex) indicates no turn information available (default) +bool AddROT(tNMEA0183AISMsg &NMEA0183AISMsg, double &rot) { + int8_t iTemp; + if ( N2kIsNA(rot)) iTemp = 128; + else { + rot *= radsToDegMin; + (rot > -128.0 && rot < 128.0)? iTemp = aRoundToInt(rot) : iTemp = 128; + } + + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 8) ) return false; + return true; +} + +// ***************************************************************************** +// 10 bit [m/s -> kts] SOG x10, 1023 = N/A +// Speed over ground is in 0.1-knot resolution from 0 to 102 knots. +// Value 1023 indicates speed is not available, value 1022 indicates 102.2 knots or higher. +bool AddSOG (tNMEA0183AISMsg &NMEA0183AISMsg, double &sog) { + int16_t iTemp; + if ( sog < 0.0 ) iTemp = 1023; + else { + sog *= msTokn; + if (sog > 102.2) iTemp = 1023; + else iTemp = aRoundToInt( 10 * sog ); + } + + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 10) ) return false; + return true; +} + +// ***************************************************************************** +// 28 bit @TODO check negative values +// Values up to plus or minus 180 degrees, East = positive, West = negative. +// A value of 181 degrees (0x6791AC0 hex) indicates that longitude is not available and is the default. +// AIS Longitude is given in in 1/10000 min; divide by 600000.0 to obtain degrees. +bool AddLongitude(tNMEA0183AISMsg &NMEA0183AISMsg, double &Longitude) { + int32_t iTemp; + (Longitude >= -180.0 && Longitude <= 180.0)? iTemp = (int) (Longitude * 600000) : iTemp = 181 * 600000; + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 28) ) return false; + return true; +} + +// ***************************************************************************** +// 27 bit +// Values up to plus or minus 90 degrees, North = positive, South = negative. +// A value of 91 degrees (0x3412140 hex) indicates latitude is not available and is the default. +bool AddLatitude(tNMEA0183AISMsg &NMEA0183AISMsg, double &Latitude) { + int32_t iTemp; + (Latitude >= -90.0 && Latitude <= 90.0)? iTemp = (int) (Latitude * 600000) : iTemp = 91 * 600000; + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 27) ) return false; + return true; +} + +// **************************************************************************** +// 9 bit True Heading (HDG) 0 to 359 degrees, 511 = not available. +bool AddHeading (tNMEA0183AISMsg &NMEA0183AISMsg, double &heading) { + uint16_t iTemp; + if ( N2kIsNA(heading) ) iTemp = 511; + else { + heading *= radToDeg; + (heading >= 0.0 && heading <= 359.0 )? iTemp = aRoundToInt( heading ) : iTemp = 511; + } + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 9) ) return false; + return true; +} + +// ***************************************************************************** +// 12bit Relative to true north, to 0.1 degree precision +bool AddCOG(tNMEA0183AISMsg &NMEA0183AISMsg, double cog) { + int16_t iTemp; + cog *= radToDeg; + if ( cog >= 0.0 && cog < 360.0 ) { iTemp = aRoundToInt( cog * 10 ); } else { iTemp = 3600; } + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 12) ) return false; + return true; +} + +// ***************************************************************************** +// 6bit Seconds in UTC timestamp should be 0-59, except for these special values: +// 60 if time stamp is not available (default) +// 61 if positioning system is in manual input mode +// 62 if Electronic Position Fixing System operates in estimated (dead reckoning) mode, +// 63 if the positioning system is inoperative. +bool AddSeconds (tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t &Seconds) { + uint8_t iTemp; + (Seconds >= 0 && Seconds <= 63 )? iTemp = Seconds : iTemp = 60; + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(iTemp, 6) ) return false; + return true; +} + +// ***************************************************************************** +// 4 bit Position Fix Type, See "EPFD Fix Types" 0 (default) +bool AddEPFDFixType(tNMEA0183AISMsg &NMEA0183AISMsg, tN2kGNSStype &GNSStype) { + // Translate tN2kGNSStype to AIS conventions + // 3 & 4 not defined in AIS -> we take 1 for GPS + uint8_t fixType = 0; + switch (GNSStype) { + case 0: // GPS + case 3: // GPS+SBAS/WAAS + case 4: // GPS+SBAS/WAAS+GLONASS + fixType = 1; break; + case 1: // GLONASS + fixType = 2; break; + case 2: // GPS+GLONASS + fixType = 3; break; + case 5: // Chayka + fixType = 5; break; + case 6: // integrated + fixType = 6; break; + case 7: // surveyed + fixType = 7; break; + case 8: // Galileo + fixType = 8; break; + default: + fixType = 0; + } + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(fixType, 4) ) return false; + return true; +} + +// ***************************************************************************** +// 8 bit Maxiumum present static draught +// In 1/10 m, 255 = draught 25.5 m or greater, 0 = not available = default; in accordance with IMO Resolution A.851 +bool AddStaticDraught(tNMEA0183AISMsg &NMEA0183AISMsg, double &Draught) { + uint8_t staticDraught; + if ( N2kIsNA(Draught) ) staticDraught = 0; + else if (Draught < 0.0) staticDraught = 0; + else if (Draught>25.5) staticDraught = 255; + else staticDraught = (int) ceil( 10.0 * Draught); + + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(staticDraught, 8) ) return false; + return true; +} + +// ***************************************************************************** +// 20bit Estimated time of arrival; MMDDHHMM UTC +// 4 Bits 19-16: month; 1-12; 0 = not available = default +// 5 Bits 15-11: day; 1-31; 0 = not available = default +// 5 Bits 10-6: hour; 0-23; 24 = not available = default +// 6 Bits 5-0: minute; 0-59; 60 = not available = default +// N2k Field #7: ETA Time - Seconds since midnight Bits: 32 Units: s +// Type: Time Resolution: 0.0001 Signed: false e.g. 36000.00 +// N2k Field #8: ETA Date - Days since January 1, 1970 Bits: 16 +// Units: days Type: Date Resolution: 1 Signed: false e.g. 18184 +bool AddETADateTime(tNMEA0183AISMsg &NMEA0183AISMsg, uint16_t &ETAdate, double &ETAtime) { + + uint8_t month = 0; + uint8_t day = 0; + uint8_t hour = 24; + uint8_t minute = 60; + + if (!N2kIsNA(ETAdate) && ETAdate > 0 ) { + tmElements_t tm; + #ifndef _Time_h + time_t t=NMEA0183AISMsg.daysToTime_t(ETAdate); + #else + time_t t=ETAdate*86400; + #endif + NMEA0183AISMsg.breakTime(t, tm); + month = (uint8_t) NMEA0183AISMsg.GetMonth(tm); + day = (uint8_t) NMEA0183AISMsg.GetDay(tm); + } + if ( !N2kIsNA(ETAtime) && ETAtime >= 0 ) { + double temp = ETAtime / 3600; + hour = (int) temp; + minute = (int) ((temp - hour) * 60); + } else { + hour = 24; + minute = 60; + } + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(month, 4) ) return false; + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(day, 5) ) return false; + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(hour, 5) ) return false; + if ( ! NMEA0183AISMsg.AddIntToPayloadBin(minute, 6) ) return false; + return true; +} + + diff --git a/lib/nmea2ktoais/NMEA0183AISMessages.h b/lib/nmea2ktoais/NMEA0183AISMessages.h new file mode 100644 index 0000000..d818a2b --- /dev/null +++ b/lib/nmea2ktoais/NMEA0183AISMessages.h @@ -0,0 +1,87 @@ +/* +NMEA0183AISMessages.h + +Copyright (c) 2019 Ronnie Zeiller, www.zeiller.eu + +Based on the works of Timo Lappalainen and Eric S. Raymond and Kurt Schwehr https://gpsd.gitlab.io/gpsd/AIVDM.html + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + +#ifndef _tNMEA0183AISMessages_H_ +#define _tNMEA0183AISMessages_H_ + + +#include +#include +#include +#include +#include "NMEA0183AISMsg.h" +#include +#include +#include + + +// Types 1, 2 and 3: Position Report Class A or B +bool SetAISClassABMessage1(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageType, uint8_t Repeat, + uint32_t UserID, double Latitude, double Longitude, bool Accuracy, bool RAIM, uint8_t Seconds, + double COG, double SOG, double Heading, double ROT, uint8_t NavStatus); + +//***************************************************************************** +// AIS Class A Static and Voyage Related Data Message Type 5 +bool SetAISClassAMessage5(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, + uint32_t UserID, uint32_t IMONumber, char *Callsign, char *Name, + uint8_t VesselType, double Length, double Beam, double PosRefStbd, + double PosRefBow, uint16_t ETAdate, double ETAtime, double Draught, + char *Destination, tN2kGNSStype GNSStype, uint8_t DTE, + tN2kAISVersion AISversion); + +//***************************************************************************** +// AIS position report (class B 129039) -> Standard Class B CS Position Report Message Type 18 Part B +bool SetAISClassBMessage18(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, uint32_t UserID, + double Latitude, double Longitude, bool Accuracy, bool RAIM, + uint8_t Seconds, double COG, double SOG, double Heading, tN2kAISUnit Unit, + bool Display, bool DSC, bool Band, bool Msg22, bool Mode, bool State); + +//***************************************************************************** +// Static Data Report Class B, Message Type 24 +// PGN 129809 Handle AIS Class B "CS" Static Data Report, Part A +bool SetAISClassBMessage24PartA(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, uint32_t UserID, char *Name); + +//***************************************************************************** +// Static Data Report Class B, Message Type 24 +bool SetAISClassBMessage24PartB(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, + uint32_t UserID, uint8_t VesselType, char *VendorID, char *Callsign, + double Length, double Beam, double PosRefStbd, double PosRefBow, uint32_t MothershipID ); + +//***************************************************************************** +// Aton class 21 +bool SetAISMessage21(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t Repeat, uint32_t UserID, + double Latitude, double Longitude, bool Accuracy, bool RAIM, + uint8_t Seconds, double Length, double Beam, double PositionReferenceStarboard, + double PositionReferenceTrueNord, tN2kAISAtoNType Type, bool OffPositionIndicator, + bool VirtualAtoNFlag, bool AssignedModeFlag, tN2kGNSStype GNSSType, uint8_t AtoNStatus, + char * atonName ); + +inline int32_t aRoundToInt(double x) { + return x >= 0 + ? (int32_t) floor(x + 0.5) + : (int32_t) ceil(x - 0.5); +} +#endif diff --git a/lib/nmea2ktoais/NMEA0183AISMsg.cpp b/lib/nmea2ktoais/NMEA0183AISMsg.cpp new file mode 100644 index 0000000..04118df --- /dev/null +++ b/lib/nmea2ktoais/NMEA0183AISMsg.cpp @@ -0,0 +1,201 @@ +/* +NMEA0183AISMsg.cpp + +Copyright (c) 2019 Ronnie Zeiller, www.zeiller.eu +Based on the works of Timo Lappalainen NMEA2000 and NMEA0183 Library + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + +#include "NMEA0183AISMsg.h" +#include +//#include +#include +#include +#include +#include +#include + +const char AsciiChar[] = "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_ !\"#$%&\'()*+,-./0123456789:;<=>?"; +const char *tNMEA0183AISMsg::EmptyAISField = "000000"; + +//***************************************************************************** +tNMEA0183AISMsg::tNMEA0183AISMsg() { + ClearAIS(); +} + +//***************************************************************************** +void tNMEA0183AISMsg::ClearAIS() { + + Payload[0]=0; + PayloadBin.reset(); + iAddPldBin=0; + iAddPld=0; +} + + +//***************************************************************************** +bool tNMEA0183AISMsg::AddIntToPayloadBin(int32_t ival, uint16_t countBits) { + + if ( (iAddPldBin + countBits ) >= AIS_BIN_MAX_LEN ) return false; // Is there room for any data + + bset = ival; + + uint16_t iAdd=iAddPldBin; + + for(int i = countBits-1; i >= 0 ; i--) { + PayloadBin[iAdd]=bset [i]; + iAdd++; + } + + iAddPldBin += countBits; + + return true; +} + +//**************************************************************************** +bool tNMEA0183AISMsg::AddBoolToPayloadBin(bool &bval) { + if ( (iAddPldBin + 1 ) >= AIS_BIN_MAX_LEN ) return false; + PayloadBin[iAddPldBin]=bval; + iAddPldBin++; + return true; +} + +// ***************************************************************************** +// converts sval into binary 6-bit AScii encoded string and appends it to PayloadBin +// filled up with "@" == "000000" to given bit-size +bool tNMEA0183AISMsg::AddEncodedCharToPayloadBin(char *sval, size_t countBits) { + + if ( (iAddPldBin + countBits ) >= AIS_BIN_MAX_LEN ) return false; // Is there room for any data + + const char * ptr; + size_t len = strlen(sval); // e.g.: should be 7 for Callsign + if ( len * 6 > countBits ) len = countBits / 6; + + for (size_t i = 0; i= 0){ + AddIntToPayloadBin(index, 6); + } + } else { + AddIntToPayloadBin(0, 6); + } + } + // fill up with "@", also covers empty sval + if ( len * 6 < countBits ) { + for (size_t i=0;i<(countBits/6-len);i++) { + AddIntToPayloadBin(0, 6); + } + } + return true; +} + +//***************************************************************************** +template +int tNMEA0183AISMsg::ConvertBinaryAISPayloadBinToAscii(std::bitset &src,uint16_t maxSize,uint16_t bitSize,uint16_t stoffset) { + Payload[0]='\0'; + uint16_t slen=maxSize; + if (stoffset >= slen) return 0; + slen-=stoffset; + uint16_t bitLen=bitSize > 0?bitSize:slen; + uint16_t len= bitLen / 6; + if ((len * 6) < bitLen) len+=1; + uint16_t padBits=0; + uint32_t offset; + std::bitset<6> s; + uint8_t dec; + int i; + for ( i=0; i= 0){ + if ( !AddUInt32Field(sequence) ) return false; + } + else{ + if ( !AddEmptyField() ) return false; + } + if ( !AddStrField(channel) ) return false; + return true; +} +bool tNMEA0183AISMsg::BuildMsg5Part1() { + if ( iAddPldBin != 424 ) return false; + InitAis(2,1,5); + int padBits=0; + AddStrField( GetPayload(padBits,0,336)); + AddUInt32Field(padBits); + return true; +} + +bool tNMEA0183AISMsg::BuildMsg5Part2() { + if ( iAddPldBin != 424 ) return false; + InitAis(2,2,5); + int padBits=0; + AddStrField( GetPayload(padBits,336,88) ); + AddUInt32Field(padBits); + return true; +} + + +//******************************* AIS PAYLOADS ********************************* +// get converted Payload for Message 1, 2, 3 & 18, always Length 168 +const char *tNMEA0183AISMsg::GetPayloadFix(int &padBits,uint16_t fixLen){ + uint16_t lenbin = iAddPldBin; + if ( lenbin != fixLen ) return nullptr; + return GetPayload(padBits,0,0); +} +const char *tNMEA0183AISMsg::GetPayload(int &padBits,uint16_t offset,uint16_t bitLen) { + padBits=ConvertBinaryAISPayloadBinToAscii(PayloadBin,iAddPldBin, bitLen,offset ); + return Payload; +} + diff --git a/lib/nmea2ktoais/NMEA0183AISMsg.h b/lib/nmea2ktoais/NMEA0183AISMsg.h new file mode 100644 index 0000000..ae9f656 --- /dev/null +++ b/lib/nmea2ktoais/NMEA0183AISMsg.h @@ -0,0 +1,97 @@ +/* +NMEA0183AISMsg.h + +Copyright (c) 2019 Ronnie Zeiller, www.zeiller.eu +Based on the works of Timo Lappalainen NMEA2000 and NMEA0183 Library + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + +#ifndef _tNMEA0183AISMsg_H_ +#define _tNMEA0183AISMsg_H_ + +#include +#include +#include +#include +#include +#include +#include + + +#ifndef AIS_MSG_MAX_LEN +#define AIS_MSG_MAX_LEN 100 // maximum length of AIS Payload +#endif + +#ifndef AIS_BIN_MAX_LEN +#define AIS_BIN_MAX_LEN 500 // maximum length of AIS Binary Payload (before encoding to Ascii) +#endif + +#define BITSET_LENGTH 120 + +class tNMEA0183AISMsg : public tNMEA0183Msg { + + protected: // AIS-NMEA + std::bitset bset; + static const char *EmptyAISField; // 6bits 0 not used yet..... + static const char *AsciChar; + + uint16_t iAddPldBin; + char Payload[AIS_MSG_MAX_LEN]; + uint8_t iAddPld; + char talker[4]="VDM"; + char channel[2]="A"; + std::bitset PayloadBin; + public: + // Clear message + void ClearAIS(); + + public: + tNMEA0183AISMsg(); + const char *GetPayloadFix(int &padBits,uint16_t fixLen=168); + const char *GetPayload(int &padBits,uint16_t offset=0,uint16_t bitLen=0); + + bool BuildMsg5Part1(); + bool BuildMsg5Part2(); + bool InitAis(int max=1,int number=1,int sequence=-1); + + // Generally Used + bool AddIntToPayloadBin(int32_t ival, uint16_t countBits); + bool AddBoolToPayloadBin(bool &bval); + bool AddEncodedCharToPayloadBin(char *sval, size_t Length); + /** + * @param channelA - if set A, otherwise B + * @param own - if set VDO, else VDM + */ + void SetChannelAndTalker(bool channelA,bool own=false); + /** + * convert the payload to ascii + * return the number of padding bits + * @param bitSize the number of bits to be used, 0 - use all bits + */ + template + int ConvertBinaryAISPayloadBinToAscii(std::bitset &src,uint16_t maxSize, uint16_t bitSize,uint16_t offset=0); + + // AIS Helper functions + protected: + inline int32_t aRoundToInt(double x) { + return (x >= 0) ? (int32_t) floor(x + 0.5) : (int32_t) ceil(x - 0.5); + } +}; +#endif diff --git a/lib/nmea2ktoais/README.md b/lib/nmea2ktoais/README.md new file mode 100644 index 0000000..1d55424 --- /dev/null +++ b/lib/nmea2ktoais/README.md @@ -0,0 +1,73 @@ +# NMEA2000 to NMEA0183 AIS Converter + + +NMEA0183 AIS library © Ronnie Zeiller, www.zeiller.eu + +Addendum for NMEA2000 and NMEA0183 Library from Timo Lappalainen https://github.com/ttlappalainen + +to get NMEA0183 AIS data from N2k-bus + +## Conversions: + +- NMEA2000 PGN 129038 => AIS CLASS A Position Report (Message Type 1) 1.) 2.) 3.) +- NMEA2000 PGN 129039 => AIS Class B Position Report, Message Type 18 +- NMEA2000 PGN 129794 => AIS Class A Ship Static and Voyage related data, Message Type 5 4.) +- NMEA2000 PGN 129809 => AIS Class B "CS" Static Data Report, making a list of UserID (MMSI) and Ship Names used for Message 24 Part A +- NMEA2000 PGN 129810 => AIS Class B "CS" Static Data Report, Message 24 Part A+B + +### Versions +1.0.6 2024-03-25 +- fixed to work with Timo´s NMEA2000 v4.21.3 + +1.0.5 2023-12-02 +- removed VDO remote print statements + +1.0.4 2023-12-02 +- merged @Isoltero master with fixed memory over run, added VDO remote print statements Thanks to Luis Soltero +- fixed example, thanks to @arduinomnomnom + +1.0.3 2022-05-01 +- Update Examples: AISTransceiverInformation in ParseN2kPGN129039 for changes in NMEA2000 library: https://github.com/ttlappalainen/NMEA2000 + + +1.0.2 2022-04-30 +- bugfix: malloc without free. Thanks to Luis Soltero (Issue https://github.com/ronzeiller/NMEA0183-AIS/issues/3) + +1.0.1 2022-03-15 +- bugfix: buffer overrun missing space for termination. Thanks to Luis Soltero (Issue https://github.com/ronzeiller/NMEA0183-AIS/issues/2) + +2020-12-25 +- corrected Navigational Status 0. Thanks to Li-Ren (Issue https://github.com/ronzeiller/NMEA0183-AIS/issues/1) + +1.0.0 2019-11-24 +- initial upload + +### Remarks +1. Message Type could be set to 1 or 3 (identical messages) on demand +2. Maneuver Indicator (not part of NMEA2000 PGN 129038) => will be set to 0 (default) +3. Radio Status (not part of NMEA2000 PGN 129038) => will be set to 0 +4. AIS Version (not part of NMEA2000 PGN 129794) => will be set to 1 + +## Dependencies + +To use this library you need also: + + - NMEA2000 library + + - NMEA0183 library + + - Related CAN libraries. + +## License + +MIT license + +Copyright (c) 2019-2022 Ronnie Zeiller, www.zeiller.eu + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. From 70ad5cc9032d741403f9232a0c09bc2c5e8fbcf4 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Sun, 28 Sep 2025 19:28:30 +0200 Subject: [PATCH 29/48] omit external nmea2ktoais --- platformio.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/platformio.ini b/platformio.ini index 53bdb61..0025382 100644 --- a/platformio.ini +++ b/platformio.ini @@ -28,7 +28,6 @@ lib_deps = ESPmDNS WiFi Update - nmea2kto183ais=https://github.com/wellenvogel/esp32n2kto183ais.git#20250926 [devdeps] lib_deps= @@ -42,7 +41,6 @@ lib_deps= ESPmDNS WiFi Update - nmea2kto183ais=symlink://../esp32n2kto183ais [env] platform = espressif32 @ 6.8.1 framework = arduino From 6266f85db6ab7c856340fa047dce4d8b35fa90ed Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Sun, 28 Sep 2025 19:58:54 +0200 Subject: [PATCH 30/48] #116: sda and scl are swapped when building with online service --- lib/iictask/GwIicTask.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/iictask/GwIicTask.cpp b/lib/iictask/GwIicTask.cpp index 221a420..53ba9bd 100644 --- a/lib/iictask/GwIicTask.cpp +++ b/lib/iictask/GwIicTask.cpp @@ -147,13 +147,13 @@ bool initWire(GwLog *logger, TwoWire &wire, int num){ #ifdef _GWI_IIC1 return initWireDo(logger,wire,num,_GWI_IIC1); #endif - return initWireDo(logger,wire,num,"",GWIIC_SDA,GWIIC_SCL); + return initWireDo(logger,wire,num,"",GWIIC_SCL,GWIIC_SDA); } if (num == 2){ #ifdef _GWI_IIC2 return initWireDo(logger,wire,num,_GWI_IIC2); #endif - return initWireDo(logger,wire,num,"",GWIIC_SDA2,GWIIC_SCL2); + return initWireDo(logger,wire,num,"",GWIIC_SCL2,GWIIC_SDA2); } return false; } From 8bf8ada30e7c224c13a25cf3a5072e445e0d2037 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Mon, 29 Sep 2025 09:35:14 +0200 Subject: [PATCH 31/48] #111: allow to add extra scripts with custom_script --- extra_script.py | 13 ++++++++++++ lib/exampletask/Readme.md | 38 ++++++++++++++++++++++++++++++++++ lib/exampletask/platformio.ini | 1 + lib/exampletask/script.py | 4 ++++ 4 files changed, 56 insertions(+) create mode 100644 lib/exampletask/script.py diff --git a/extra_script.py b/extra_script.py index 218782f..3f41463 100644 --- a/extra_script.py +++ b/extra_script.py @@ -547,3 +547,16 @@ env.Append( ) #script does not run on clean yet - maybe in the future env.AddPostAction("clean",cleangenerated) +extraScripts=getFileList(getOption(env,'custom_script',toArray=True)) +for script in extraScripts: + if os.path.isfile(script): + print(f"#extra {script}") + with open(script) as fh: + try: + code = compile(fh.read(), script, 'exec') + except SyntaxError as e: + print(f"#ERROR: script {script} does not compile: {e}") + continue + exec(code) + else: + print(f"#ERROR: script {script} not found") diff --git a/lib/exampletask/Readme.md b/lib/exampletask/Readme.md index 7637ecf..7c701fb 100644 --- a/lib/exampletask/Readme.md +++ b/lib/exampletask/Readme.md @@ -57,6 +57,44 @@ Files Starting from Version 20250305 you should normally not use this file name any more as those styles would be added for all build environments. Instead define a parameter _custom_css_ in your [platformio.ini](platformio.ini) for the environments you would like to add some styles for. This parameter accepts a list of file names (relative to the project root, separated by , or as multi line entry) + * [script.py](script.py)
+ Starting from version 202509xx you can define a parameter "custom_script" in your [platformio.ini](platformio.ini). + This parameter can contain a list of file names (relative to the project root) that will be added as a [platformio extra script](https://docs.platformio.org/en/latest/scripting/index.html#scripting). The scripts will be loaded at the end of the main [extra_script](../../extra_script.py). + You can add code there that is specific for your build. + Example: + ``` + # PlatformIO extra script for obp60task + epdtype = "unknown" + pcbvers = "unknown" + for x in env["BUILD_FLAGS"]: + if x.startswith("-D HARDWARE_"): + pcbvers = x.split('_')[1] + if x.startswith("-D DISPLAY_"): + epdtype = x.split('_')[1] + + propfilename = os.path.join(env["PROJECT_LIBDEPS_DIR"], env ["PIOENV"], "GxEPD2/library.properties") + properties = {} + with open(propfilename, 'r') as file: + for line in file: + match = re.match(r'^([^=]+)=(.*)$', line) + if match: + key = match.group(1).strip() + value = match.group(2).strip() + properties[key] = value + + gxepd2vers = "unknown" + try: + if properties["name"] == "GxEPD2": + gxepd2vers = properties["version"] + except: + pass + + env["CPPDEFINES"].extend([("BOARD", env["BOARD"]), ("EPDTYPE", epdtype), ("PCBVERS", pcbvers), ("GXEPD2VERS", gxepd2vers)]) + + print("added hardware info to CPPDEFINES") + print("friendly board name is '{}'".format(env.GetProjectOption ("board_name"))) + ``` + Interfaces ---------- diff --git a/lib/exampletask/platformio.ini b/lib/exampletask/platformio.ini index 348b36c..74363a9 100644 --- a/lib/exampletask/platformio.ini +++ b/lib/exampletask/platformio.ini @@ -14,5 +14,6 @@ custom_config= lib/exampletask/exampleConfig.json custom_js=lib/exampletask/example.js custom_css=lib/exampletask/example.css +custom_script=lib/exampletask/script.py upload_port = /dev/esp32 upload_protocol = esptool \ No newline at end of file diff --git a/lib/exampletask/script.py b/lib/exampletask/script.py new file mode 100644 index 0000000..fb53d6f --- /dev/null +++ b/lib/exampletask/script.py @@ -0,0 +1,4 @@ +Import("env") + +print("exampletask extra script running") +syntax error here \ No newline at end of file From 9831f8da853a85b23bb2718fe0cd049c15d07ae2 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Mon, 29 Sep 2025 12:43:21 +0200 Subject: [PATCH 32/48] add code for SHT4X, ENV4 --- lib/hardware/GwM5Grove.in | 37 +++++-- lib/iictask/GwIicTask.cpp | 2 +- lib/iictask/GwSHT3X.cpp | 138 -------------------------- lib/iictask/GwSHT3X.h | 20 ---- lib/iictask/SHT3X.cpp | 2 +- lib/iictask/SHT4X.cpp | 128 ++++++++++++++++++++++++ lib/iictask/SHT4X.h | 76 ++++++++++++++ lib/iictask/config.json | 197 +++++++++++++++++++++++++++++++++++++ lib/iictask/platformio.ini | 11 +++ lib/sensors/GwSensor.h | 1 + 10 files changed, 443 insertions(+), 169 deletions(-) delete mode 100644 lib/iictask/GwSHT3X.cpp delete mode 100644 lib/iictask/GwSHT3X.h create mode 100644 lib/iictask/SHT4X.cpp create mode 100644 lib/iictask/SHT4X.h diff --git a/lib/hardware/GwM5Grove.in b/lib/hardware/GwM5Grove.in index c98d4d1..a3c8c06 100644 --- a/lib/hardware/GwM5Grove.in +++ b/lib/hardware/GwM5Grove.in @@ -71,15 +71,15 @@ #endif #GROVE -//#ifdef M5_ENV4$GS$ -// #ifndef M5_GROOVEIIC$GS$ -// #define M5_GROOVEIIC$GS$ -// #endif -// GROOVE_IIC(SHT3X,$Z$,1) -// GROOVE_IIC(BMP280,$Z$,1) -// #define _GWSHT3X -// #define _GWBMP280 -//#endif +#ifdef M5_ENV4$GS$ + #ifndef M5_GROOVEIIC$GS$ + #define M5_GROOVEIIC$GS$ + #endif + GROOVE_IIC(SHT4X,$Z$,1) + GROOVE_IIC(BMP280,$Z$,1) + #define _GWSHT4X + #define _GWBMP280 +#endif #GROVE //example: -DSHT3XG1_A : defines STH3Xn1 on grove A - x depends on the other devices @@ -100,6 +100,25 @@ #define _GWSHT3X #endif +#GROVE +//example: -DSHT4XG1_A : defines STH4Xn1 on grove A - x depends on the other devices +#ifdef GWSHT4XG1$GS$ + #ifndef M5_GROOVEIIC$GS$ + #define M5_GROOVEIIC$GS$ + #endif + GROOVE_IIC(SHT4X,$Z$,1) + #define _GWSHT4X +#endif + +#GROVE +#ifdef GWSHT4XG2$GS$ + #ifndef M5_GROOVEIIC$GS$ + #define M5_GROOVEIIC$GS$ + #endif + GROOVE_IIC(SHT4X,$Z$,2) + #define _GWSHT4X +#endif + #GROVE #ifdef GWQMP6988G1$GS$ #ifndef M5_GROOVEIIC$GS$ diff --git a/lib/iictask/GwIicTask.cpp b/lib/iictask/GwIicTask.cpp index 53ba9bd..3de396b 100644 --- a/lib/iictask/GwIicTask.cpp +++ b/lib/iictask/GwIicTask.cpp @@ -23,7 +23,7 @@ static std::vector iicGroveList; #include "GwBME280.h" #include "GwBMP280.h" #include "GwQMP6988.h" -#include "GwSHT3X.h" +#include "GwSHTXX.h" #include #include "GwTimer.h" diff --git a/lib/iictask/GwSHT3X.cpp b/lib/iictask/GwSHT3X.cpp deleted file mode 100644 index c93486f..0000000 --- a/lib/iictask/GwSHT3X.cpp +++ /dev/null @@ -1,138 +0,0 @@ -#include "GwSHT3X.h" -#ifdef _GWSHT3X -class SHT3XConfig; -static GwSensorConfigInitializerList configs; -class SHT3XConfig : public IICSensorBase{ - public: - String tmNam; - String huNam; - bool tmAct=false; - bool huAct=false; - tN2kHumiditySource huSrc; - tN2kTempSource tmSrc; - SHT3X *device=nullptr; - using IICSensorBase::IICSensorBase; - virtual bool isActive(){ - return tmAct || huAct; - } - virtual bool initDevice(GwApi * api,TwoWire *wire){ - if (! isActive()) return false; - device=new SHT3X(); - device->init(addr,wire); - GwLog *logger=api->getLogger(); - LOG_DEBUG(GwLog::LOG,"initialized %s at address %d, intv %ld",prefix.c_str(),(int)addr,intv); - return true; - } - virtual bool preinit(GwApi * api){ - GwLog *logger=api->getLogger(); - LOG_DEBUG(GwLog::LOG,"%s configured",prefix.c_str()); - addHumidXdr(api,*this); - addTempXdr(api,*this); - return isActive(); - } - virtual void measure(GwApi * api,TwoWire *wire, int counterId) - { - if (!device) - return; - GwLog *logger=api->getLogger(); - int rt = 0; - if ((rt = device->get()) == 0) - { - double temp = device->cTemp; - temp = CToKelvin(temp); - double humid = device->humidity; - LOG_DEBUG(GwLog::DEBUG, "%s measure temp=%2.1f, humid=%2.0f",prefix.c_str(), (float)temp, (float)humid); - if (huAct) - { - sendN2kHumidity(api, *this, humid, counterId); - } - if (tmAct) - { - sendN2kTemperature(api, *this, temp, counterId); - } - } - else - { - LOG_DEBUG(GwLog::DEBUG, "unable to query %s: %d",prefix.c_str(), rt); - } - } - - virtual void readConfig(GwConfigHandler *cfg){ - if (ok) return; - configs.readConfig(this,cfg); - return; - } -}; -SensorBase::Creator creator=[](GwApi *api,const String &prfx)-> SensorBase*{ - if (! configs.knowsPrefix(prfx)) return nullptr; - return new SHT3XConfig(api,prfx); -}; -SensorBase::Creator registerSHT3X(GwApi *api){ - GwLog *logger=api->getLogger(); - #if defined(GWSHT3X) || defined (GWSHT3X11) - { - api->addSensor(creator(api,"SHT3X11")); - CHECK_IIC1(); - #pragma message "GWSHT3X11 defined" - } - #endif - #if defined(GWSHT3X12) - { - api->addSensor(creator(api,"SHT3X12")); - CHECK_IIC1(); - #pragma message "GWSHT3X12 defined" - } - #endif - #if defined(GWSHT3X21) - { - api->addSensor(creator(api,"SHT3X21")); - CHECK_IIC2(); - #pragma message "GWSHT3X21 defined" - } - #endif - #if defined(GWSHT3X22) - { - api->addSensor(creator(api,"SHT3X22")); - CHECK_IIC2(); - #pragma message "GWSHT3X22 defined" - } - #endif - return creator; -}; - -/** - * we do not dynamically compute the config names - * just to get compile time errors if something does not fit - * correctly - */ -#define CFGSHT3X(s, prefix, bus, baddr) \ - CFG_SGET(s, tmNam, prefix); \ - CFG_SGET(s, huNam, prefix); \ - CFG_SGET(s, iid, prefix); \ - CFG_SGET(s, tmAct, prefix); \ - CFG_SGET(s, huAct, prefix); \ - CFG_SGET(s, intv, prefix); \ - CFG_SGET(s, huSrc, prefix); \ - CFG_SGET(s, tmSrc, prefix); \ - s->busId = bus; \ - s->addr = baddr; \ - s->ok = true; \ - s->intv *= 1000; - -#define SCSHT3X(prefix, bus, addr) \ - GWSENSORDEF(configs, SHT3XConfig, CFGSHT3X, prefix, bus, addr) - -SCSHT3X(SHT3X11, 1, 0x44); -SCSHT3X(SHT3X12, 1, 0x45); -SCSHT3X(SHT3X21, 2, 0x44); -SCSHT3X(SHT3X22, 2, 0x45); - -#else -SensorBase::Creator registerSHT3X(GwApi *api){ - return SensorBase::Creator(); -} - -#endif - - - diff --git a/lib/iictask/GwSHT3X.h b/lib/iictask/GwSHT3X.h deleted file mode 100644 index 6a5dfcf..0000000 --- a/lib/iictask/GwSHT3X.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef _GWSHT3X_H -#define _GWSHT3X_H -#include "GwIicSensors.h" -#ifdef _GWIIC - #if defined(GWSHT3X) || defined(GWSHT3X11) || defined(GWSHT3X12) || defined(GWSHT3X21) || defined(GWSHT3X22) - #define _GWSHT3X - #endif -#else - #undef _GWSHT3X - #undef GWSHT3X - #undef GWSHT3X11 - #undef GWSHT3X12 - #undef GWSHT3X21 - #undef GWSHT3X22 -#endif -#ifdef _GWSHT3X - #include "SHT3X.h" -#endif -SensorBase::Creator registerSHT3X(GwApi *api); -#endif \ No newline at end of file diff --git a/lib/iictask/SHT3X.cpp b/lib/iictask/SHT3X.cpp index 7830cf5..c36d92e 100644 --- a/lib/iictask/SHT3X.cpp +++ b/lib/iictask/SHT3X.cpp @@ -1,4 +1,4 @@ -#include "GwSHT3X.h" +#include "GwSHTXX.h" #ifdef _GWSHT3X bool SHT3X::init(uint8_t slave_addr_in, TwoWire* wire_in) diff --git a/lib/iictask/SHT4X.cpp b/lib/iictask/SHT4X.cpp new file mode 100644 index 0000000..7dc9235 --- /dev/null +++ b/lib/iictask/SHT4X.cpp @@ -0,0 +1,128 @@ +#include "SHT4X.h" +uint8_t crc8(const uint8_t *data, int len) { + /* + * + * CRC-8 formula from page 14 of SHT spec pdf + * + * Test data 0xBE, 0xEF should yield 0x92 + * + * Initialization data 0xFF + * Polynomial 0x31 (x8 + x5 +x4 +1) + * Final XOR 0x00 + */ + + const uint8_t POLYNOMIAL(0x31); + uint8_t crc(0xFF); + + for (int j = len; j; --j) { + crc ^= *data++; + + for (int i = 8; i; --i) { + crc = (crc & 0x80) ? (crc << 1) ^ POLYNOMIAL : (crc << 1); + } + } + return crc; +} + +bool SHT4X::begin(TwoWire* wire, uint8_t addr) { + _addr = addr; + _wire = wire; + int error; + _wire->beginTransmission(addr); + error = _wire->endTransmission(); + if (error == 0) { + return true; + } + return false; +} + +bool SHT4X::update() { + uint8_t readbuffer[6]; + uint8_t cmd = SHT4x_NOHEAT_HIGHPRECISION; + uint16_t duration = 10; + + if (_heater == SHT4X_NO_HEATER) { + if (_precision == SHT4X_HIGH_PRECISION) { + cmd = SHT4x_NOHEAT_HIGHPRECISION; + duration = 10; + } + if (_precision == SHT4X_MED_PRECISION) { + cmd = SHT4x_NOHEAT_MEDPRECISION; + duration = 5; + } + if (_precision == SHT4X_LOW_PRECISION) { + cmd = SHT4x_NOHEAT_LOWPRECISION; + duration = 2; + } + } + + if (_heater == SHT4X_HIGH_HEATER_1S) { + cmd = SHT4x_HIGHHEAT_1S; + duration = 1100; + } + if (_heater == SHT4X_HIGH_HEATER_100MS) { + cmd = SHT4x_HIGHHEAT_100MS; + duration = 110; + } + + if (_heater == SHT4X_MED_HEATER_1S) { + cmd = SHT4x_MEDHEAT_1S; + duration = 1100; + } + if (_heater == SHT4X_MED_HEATER_100MS) { + cmd = SHT4x_MEDHEAT_100MS; + duration = 110; + } + + if (_heater == SHT4X_LOW_HEATER_1S) { + cmd = SHT4x_LOWHEAT_1S; + duration = 1100; + } + if (_heater == SHT4X_LOW_HEATER_100MS) { + cmd = SHT4x_LOWHEAT_100MS; + duration = 110; + } + // _i2c.writeByte(_addr, cmd, 1); + _wire->beginTransmission(_addr); + _wire->write(cmd); + _wire->write(1); + _wire->endTransmission(); + + + delay(duration); + + _wire->requestFrom(_addr, (uint8_t)6); + + for (uint16_t i = 0; i < 6; i++) { + readbuffer[i] = _wire->read(); + } + + if (readbuffer[2] != crc8(readbuffer, 2) || + readbuffer[5] != crc8(readbuffer + 3, 2)) { + return false; + } + + float t_ticks = (uint16_t)readbuffer[0] * 256 + (uint16_t)readbuffer[1]; + float rh_ticks = (uint16_t)readbuffer[3] * 256 + (uint16_t)readbuffer[4]; + + cTemp = -45 + 175 * t_ticks / 65535; + humidity = -6 + 125 * rh_ticks / 65535; + humidity = min(max(humidity, (float)0.0), (float)100.0); + return true; +} + +void SHT4X::setPrecision(sht4x_precision_t prec) { + _precision = prec; +} + +sht4x_precision_t SHT4X::getPrecision(void) { + return _precision; +} + +void SHT4X::setHeater(sht4x_heater_t heat) { + _heater = heat; +} + +sht4x_heater_t SHT4X::getHeater(void) { + return _heater; +} diff --git a/lib/iictask/SHT4X.h b/lib/iictask/SHT4X.h new file mode 100644 index 0000000..dbfbabf --- /dev/null +++ b/lib/iictask/SHT4X.h @@ -0,0 +1,76 @@ +#ifndef __SHT4X_H_ +#define __SHT4X_H_ + +#include "Arduino.h" +#include "Wire.h" + +#define SHT40_I2C_ADDR_44 0x44 +#define SHT40_I2C_ADDR_45 0x45 +#define SHT41_I2C_ADDR_44 0x44 +#define SHT41_I2C_ADDR_45 0x45 +#define SHT45_I2C_ADDR_44 0x44 +#define SHT45_I2C_ADDR_45 0x45 + +#define SHT4x_DEFAULT_ADDR 0x44 /**< SHT4x I2C Address */ +#define SHT4x_NOHEAT_HIGHPRECISION \ + 0xFD /**< High precision measurement, no heater */ +#define SHT4x_NOHEAT_MEDPRECISION \ + 0xF6 /**< Medium precision measurement, no heater */ +#define SHT4x_NOHEAT_LOWPRECISION \ + 0xE0 /**< Low precision measurement, no heater */ + +#define SHT4x_HIGHHEAT_1S \ + 0x39 /**< High precision measurement, high heat for 1 sec */ +#define SHT4x_HIGHHEAT_100MS \ + 0x32 /**< High precision measurement, high heat for 0.1 sec */ +#define SHT4x_MEDHEAT_1S \ + 0x2F /**< High precision measurement, med heat for 1 sec */ +#define SHT4x_MEDHEAT_100MS \ + 0x24 /**< High precision measurement, med heat for 0.1 sec */ +#define SHT4x_LOWHEAT_1S \ + 0x1E /**< High precision measurement, low heat for 1 sec */ +#define SHT4x_LOWHEAT_100MS \ + 0x15 /**< High precision measurement, low heat for 0.1 sec */ + +#define SHT4x_READSERIAL 0x89 /**< Read Out of Serial Register */ +#define SHT4x_SOFTRESET 0x94 /**< Soft Reset */ + +typedef enum { + SHT4X_HIGH_PRECISION, + SHT4X_MED_PRECISION, + SHT4X_LOW_PRECISION, +} sht4x_precision_t; + +/** Optional pre-heater configuration setting */ +typedef enum { + SHT4X_NO_HEATER, + SHT4X_HIGH_HEATER_1S, + SHT4X_HIGH_HEATER_100MS, + SHT4X_MED_HEATER_1S, + SHT4X_MED_HEATER_100MS, + SHT4X_LOW_HEATER_1S, + SHT4X_LOW_HEATER_100MS, +} sht4x_heater_t; + +class SHT4X { + public: + bool begin(TwoWire* wire = &Wire, uint8_t addr = SHT40_I2C_ADDR_44); + bool update(void); + + float cTemp = 0; + float humidity = 0; + + void setPrecision(sht4x_precision_t prec); + sht4x_precision_t getPrecision(void); + void setHeater(sht4x_heater_t heat); + sht4x_heater_t getHeater(void); + + private: + TwoWire* _wire; + uint8_t _addr; + + sht4x_precision_t _precision = SHT4X_HIGH_PRECISION; + sht4x_heater_t _heater = SHT4X_NO_HEATER; +}; + +#endif diff --git a/lib/iictask/config.json b/lib/iictask/config.json index 54da57e..73147c6 100644 --- a/lib/iictask/config.json +++ b/lib/iictask/config.json @@ -196,6 +196,203 @@ } ] }, + { + "type": "array", + "name": "SHT4X", + "replace": [ + { + "b": "1", + "i": "11", + "n": "119" + }, + { + "b": "1", + "i": "12", + "n": "118" + }, + { + "b": "2", + "i": "21", + "n": "129" + }, + { + "b": "2", + "i": "22", + "n": "128" + } + + + ], + "children": [ + { + "name": "SHT4X$itmAct", + "label": "SHT4X$i Temp", + "type": "boolean", + "default": "true", + "description": "Enable the $i. I2C SHT4X temp sensor (bus $b)", + "category": "iicsensors$b", + "capabilities": { + "SHT4X$i": "true" + } + }, + { + "name": "SHT4X$itmSrc", + "label": "SHT4X$i Temp Type", + "type": "list", + "default": "2", + "description": "the NMEA2000 source type for the temperature", + "list": [ + { + "l": "SeaTemperature", + "v": "0" + }, + { + "l": "OutsideTemperature", + "v": "1" + }, + { + "l": "InsideTemperature", + "v": "2" + }, + { + "l": "EngineRoomTemperature", + "v": "3" + }, + { + "l": "MainCabinTemperature", + "v": "4" + }, + { + "l": "LiveWellTemperature", + "v": "5" + }, + { + "l": "BaitWellTemperature", + "v": "6" + }, + { + "l": "RefridgerationTemperature", + "v": "7" + }, + { + "l": "HeatingSystemTemperature", + "v": "8" + }, + { + "l": "DewPointTemperature", + "v": "9" + }, + { + "l": "ApparentWindChillTemperature", + "v": "10" + }, + { + "l": "TheoreticalWindChillTemperature", + "v": "11" + }, + { + "l": "HeatIndexTemperature", + "v": "12" + }, + { + "l": "FreezerTemperature", + "v": "13" + }, + { + "l": "ExhaustGasTemperature", + "v": "14" + }, + { + "l": "ShaftSealTemperature", + "v": "15" + } + ], + "category": "iicsensors$b", + "capabilities": { + "SHT4X$i": "true" + } + }, + { + "name": "SHT4X$ihuAct", + "label": "SHT4X$i Humidity", + "type": "boolean", + "default": "true", + "description": "Enable the $i. I2C SHT4X humidity sensor (bus $b)", + "category": "iicsensors$b", + "capabilities": { + "SHT4X$i": "true" + } + }, + { + "name": "SHT4X$ihuSrc", + "label": "SHT4X$i Humid Type", + "list": [ + { + "l": "OutsideHumidity", + "v": "1" + }, + { + "l": "Undef", + "v": "0xff" + } + ], + "category": "iicsensors$b", + "capabilities": { + "SHT4X": "true" + } + }, + { + "name": "SHT4X$iiid", + "label": "SHT4X$i N2K iid", + "type": "number", + "default": "$n", + "description": "the N2K instance id for the $i. SHT4X Temperature and Humidity ", + "category": "iicsensors$b", + "min": 0, + "max": 253, + "check": "checkMinMax", + "capabilities": { + "SHT4X$i": "true" + } + }, + { + "name": "SHT4X$iintv", + "label": "SHT4X$i Interval", + "type": "number", + "default": 2, + "description": "Interval(s) to query SHT4X Temperature and Humidity (1...300)", + "category": "iicsensors$b", + "min": 1, + "max": 300, + "check": "checkMinMax", + "capabilities": { + "SHT4X$i": "true" + } + }, + { + "name": "SHT4X$itmNam", + "label": "SHT4X$i Temp XDR", + "type": "String", + "default": "Temp$i", + "description": "set the XDR transducer name for the $i. SHT4X Temperature, leave empty to disable NMEA0183 XDR ", + "category": "iicsensors$b", + "capabilities": { + "SHT4X$i": "true" + } + }, + { + "name": "SHT4X$ihuNam", + "label": "SHT4X$i Humid XDR", + "type": "String", + "default": "Humidity$i", + "description": "set the XDR transducer name for the $i. SHT4X Humidity, leave empty to disable NMEA0183 XDR", + "category": "iicsensors$b", + "capabilities": { + "SHT4X$i": "true" + } + } + ] + }, { "type": "array", "name": "QMP6988", diff --git a/lib/iictask/platformio.ini b/lib/iictask/platformio.ini index c0f10f7..31e17a3 100644 --- a/lib/iictask/platformio.ini +++ b/lib/iictask/platformio.ini @@ -11,6 +11,17 @@ build_flags= -D M5_CAN_KIT ${env.build_flags} +[env:m5stack-atom-env4] +extends = sensors +board = m5stack-atom +lib_deps = + ${env.lib_deps} + ${sensors.lib_deps} +build_flags= + -D M5_ENV4 + -D M5_CAN_KIT + ${env.build_flags} + [env:m5stack-atom-bme280] extends = sensors diff --git a/lib/sensors/GwSensor.h b/lib/sensors/GwSensor.h index 48eb5b2..6bc611f 100644 --- a/lib/sensors/GwSensor.h +++ b/lib/sensors/GwSensor.h @@ -93,6 +93,7 @@ class GwSensorConfig{ } bool readConfig(T* s,GwConfigHandler *cfg){ if (s == nullptr) return false; + if (prefix != s->prefix) return false; configReader(s,cfg); return s->ok; } From fddc3c742b726bc56fa0a0fcc2e8647ece9418d3 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Mon, 29 Sep 2025 17:05:48 +0200 Subject: [PATCH 33/48] allow to switch PGN 130311 for sensors on/off, simplify config.json for i2csensors --- lib/iictask/GwBME280.cpp | 2 + lib/iictask/GwBMP280.cpp | 2 + lib/iictask/GwIicSensors.h | 1 + lib/iictask/GwIicTask.cpp | 1 + lib/iictask/GwQMP6988.cpp | 2 + lib/iictask/config.json | 345 ++++++++++++------------------------- 6 files changed, 118 insertions(+), 235 deletions(-) diff --git a/lib/iictask/GwBME280.cpp b/lib/iictask/GwBME280.cpp index 1bf541f..177a775 100644 --- a/lib/iictask/GwBME280.cpp +++ b/lib/iictask/GwBME280.cpp @@ -23,6 +23,7 @@ class BME280Config : public IICSensorBase{ bool prAct=true; bool tmAct=true; bool huAct=true; + bool sEnv=true; tN2kTempSource tmSrc=tN2kTempSource::N2kts_InsideTemperature; tN2kHumiditySource huSrc=tN2kHumiditySource::N2khs_InsideHumidity; tN2kPressureSource prSrc=tN2kPressureSource::N2kps_Atmospheric; @@ -152,6 +153,7 @@ SensorBase::Creator registerBME280(GwApi *api){ CFG_SGET(s, prNam, prefix); \ CFG_SGET(s, tmOff, prefix); \ CFG_SGET(s, prOff, prefix); \ + CFG_SGET(s, sEnv, prefix); \ s->busId = bus; \ s->addr = baddr; \ s->ok = true; \ diff --git a/lib/iictask/GwBMP280.cpp b/lib/iictask/GwBMP280.cpp index e51bd4b..bee51e4 100644 --- a/lib/iictask/GwBMP280.cpp +++ b/lib/iictask/GwBMP280.cpp @@ -29,6 +29,7 @@ class BMP280Config : public IICSensorBase{ public: bool prAct=true; bool tmAct=true; + bool sEnv=true; tN2kTempSource tmSrc=tN2kTempSource::N2kts_InsideTemperature; tN2kPressureSource prSrc=tN2kPressureSource::N2kps_Atmospheric; tN2kHumiditySource huSrc=tN2kHumiditySource::N2khs_Undef; @@ -150,6 +151,7 @@ SensorBase::Creator registerBMP280(GwApi *api){ CFG_SGET(s, prNam, prefix); \ CFG_SGET(s, tmOff, prefix); \ CFG_SGET(s, prOff, prefix); \ + CFG_SGET(s, sEnv,prefix); \ s->busId = bus; \ s->addr = baddr; \ s->ok = true; \ diff --git a/lib/iictask/GwIicSensors.h b/lib/iictask/GwIicSensors.h index 4937daa..a9465d5 100644 --- a/lib/iictask/GwIicSensors.h +++ b/lib/iictask/GwIicSensors.h @@ -104,6 +104,7 @@ void sendN2kTemperature(GwApi *api,CFG &cfg,double value, int counterId){ template void sendN2kEnvironmentalParameters(GwApi *api,CFG &cfg,double tmValue, double huValue, double prValue, int counterId){ + if (! cfg.sEnv) return; tN2kMsg msg; SetN2kEnvironmentalParameters(msg,1,cfg.tmSrc,tmValue,cfg.huSrc,huValue,prValue); api->sendN2kMessage(msg); diff --git a/lib/iictask/GwIicTask.cpp b/lib/iictask/GwIicTask.cpp index 3de396b..998e441 100644 --- a/lib/iictask/GwIicTask.cpp +++ b/lib/iictask/GwIicTask.cpp @@ -91,6 +91,7 @@ void initIicTask(GwApi *api){ GwConfigHandler *config=api->getConfig(); std::vector creators; creators.push_back(registerSHT3X(api)); + creators.push_back(registerSHT4X(api)); creators.push_back(registerQMP6988(api)); creators.push_back(registerBME280(api)); creators.push_back(registerBMP280(api)); diff --git a/lib/iictask/GwQMP6988.cpp b/lib/iictask/GwQMP6988.cpp index 4f2b78c..12b648f 100644 --- a/lib/iictask/GwQMP6988.cpp +++ b/lib/iictask/GwQMP6988.cpp @@ -9,6 +9,7 @@ class QMP6988Config : public IICSensorBase{ public: String prNam="Pressure"; bool prAct=true; + bool sEnv=true; tN2kPressureSource prSrc=tN2kPressureSource::N2kps_Atmospheric; float prOff=0; QMP6988 *device=nullptr; @@ -90,6 +91,7 @@ SensorBase::Creator registerQMP6988(GwApi *api){ CFG_SGET(s,prAct,prefix); \ CFG_SGET(s,intv,prefix); \ CFG_SGET(s,prOff,prefix); \ + CFG_SGET(s,sEnv,prefix); \ s->busId = bus; \ s->addr = baddr; \ s->ok = true; \ diff --git a/lib/iictask/config.json b/lib/iictask/config.json index 73147c6..5cd50af 100644 --- a/lib/iictask/config.json +++ b/lib/iictask/config.json @@ -1,49 +1,77 @@ [ { "type": "array", - "name": "SHT3X", + "name": "SHTXX", "replace": [ { "b": "1", "i": "11", - "n": "99" + "n": "99", + "x": "3" }, { "b": "1", "i": "12", - "n": "98" + "n": "98", + "x": "3" }, { "b": "2", "i": "21", - "n": "109" + "n": "109", + "x": "3" }, { "b": "2", "i": "22", - "n": "108" + "n": "108", + "x": "3" + }, + { + "b": "1", + "i": "11", + "n": "119", + "x": "4" + }, + { + "b": "1", + "i": "12", + "n": "118", + "x": "4" + }, + { + "b": "2", + "i": "21", + "n": "129", + "x": "4" + }, + { + "b": "2", + "i": "22", + "n": "128", + "x": "4" } ], "children": [ { - "name": "SHT3X$itmAct", - "label": "SHT3X$i Temp", + "name": "SHT$xX$itmAct", + "label": "SHT$xX$i Temp", "type": "boolean", "default": "true", - "description": "Enable the $i. I2C SHT3x temp sensor (bus $b)", + "description": "Enable the $i. I2C SHT$xX temp sensor (bus $b)", "category": "iicsensors$b", "capabilities": { - "SHT3X$i": "true" + "SHT$xX$i": "true" } }, { - "name": "SHT3X$itmSrc", - "label": "SHT3X$i Temp Type", + "name": "SHT$xX$itmSrc", + "label": "SHT$xX$i Temp Type", "type": "list", "default": "2", - "description": "the NMEA2000 source type for the temperature", + "description": "the NMEA2000 source type for the temperature (PGN 130312,130311)", "list": [ { "l": "SeaTemperature", @@ -112,23 +140,23 @@ ], "category": "iicsensors$b", "capabilities": { - "SHT3X$i": "true" + "SHT$xX$i": "true" } }, { - "name": "SHT3X$ihuAct", - "label": "SHT3X$i Humidity", + "name": "SHT$xX$ihuAct", + "label": "SHT$xX$i Humidity", "type": "boolean", "default": "true", - "description": "Enable the $i. I2C SHT3x humidity sensor (bus $b)", + "description": "Enable the $i. I2C SHT$xX humidity sensor (bus $b)", "category": "iicsensors$b", "capabilities": { - "SHT3X$i": "true" + "SHT$xX$i": "true" } }, { - "name": "SHT3X$ihuSrc", - "label": "SHT3X$i Humid Type", + "name": "SHT$xX$ihuSrc", + "label": "SHT$xX$i Humid Type", "list": [ { "l": "OutsideHumidity", @@ -141,254 +169,68 @@ ], "category": "iicsensors$b", "capabilities": { - "SHT3X": "true" + "SHT$xX": "true" } }, { - "name": "SHT3X$iiid", - "label": "SHT3X$i N2K iid", + "name": "SHT$xX$iiid", + "label": "SHT$xX$i N2K iid", "type": "number", "default": "$n", - "description": "the N2K instance id for the $i. SHT3X Temperature and Humidity ", + "description": "the N2K instance id for the $i. SHT$xX Temperature and Humidity (PGN 130312,130311) ", "category": "iicsensors$b", "min": 0, "max": 253, "check": "checkMinMax", "capabilities": { - "SHT3X$i": "true" + "SHT$xX$i": "true" } }, { - "name": "SHT3X$iintv", - "label": "SHT3X$i Interval", + "name": "SHT$xX$isEnv", + "label": "SHT$xX$i send Env", + "type": "boolean", + "default": "true", + "description": "also send PGN 130311", + "category": "iicsensors$b", + "capabilities": { + "SHT$xX$i": "true" + } + }, + { + "name": "SHT$xX$iintv", + "label": "SHT$xX$i Interval", "type": "number", "default": 2, - "description": "Interval(s) to query SHT3X Temperature and Humidity (1...300)", + "description": "Interval(s) to query SHT$xX Temperature and Humidity (1...300)", "category": "iicsensors$b", "min": 1, "max": 300, "check": "checkMinMax", "capabilities": { - "SHT3X$i": "true" + "SHT$xX$i": "true" } }, { - "name": "SHT3X$itmNam", - "label": "SHT3X$i Temp XDR", + "name": "SHT$xX$itmNam", + "label": "SHT$xX$i Temp XDR", "type": "String", "default": "Temp$i", - "description": "set the XDR transducer name for the $i. SHT3X Temperature, leave empty to disable NMEA0183 XDR ", + "description": "set the XDR transducer name for the $i. SHT$xX Temperature, leave empty to disable NMEA0183 XDR ", "category": "iicsensors$b", "capabilities": { - "SHT3X$i": "true" + "SHT$xX$i": "true" } }, { - "name": "SHT3X$ihuNam", - "label": "SHT3X$i Humid XDR", + "name": "SHT$xX$ihuNam", + "label": "SHT$xX$i Humid XDR", "type": "String", "default": "Humidity$i", - "description": "set the XDR transducer name for the $i. SHT3X Humidity, leave empty to disable NMEA0183 XDR", + "description": "set the XDR transducer name for the $i. SHT$xX Humidity, leave empty to disable NMEA0183 XDR", "category": "iicsensors$b", "capabilities": { - "SHT3X$i": "true" - } - } - ] - }, - { - "type": "array", - "name": "SHT4X", - "replace": [ - { - "b": "1", - "i": "11", - "n": "119" - }, - { - "b": "1", - "i": "12", - "n": "118" - }, - { - "b": "2", - "i": "21", - "n": "129" - }, - { - "b": "2", - "i": "22", - "n": "128" - } - - - ], - "children": [ - { - "name": "SHT4X$itmAct", - "label": "SHT4X$i Temp", - "type": "boolean", - "default": "true", - "description": "Enable the $i. I2C SHT4X temp sensor (bus $b)", - "category": "iicsensors$b", - "capabilities": { - "SHT4X$i": "true" - } - }, - { - "name": "SHT4X$itmSrc", - "label": "SHT4X$i Temp Type", - "type": "list", - "default": "2", - "description": "the NMEA2000 source type for the temperature", - "list": [ - { - "l": "SeaTemperature", - "v": "0" - }, - { - "l": "OutsideTemperature", - "v": "1" - }, - { - "l": "InsideTemperature", - "v": "2" - }, - { - "l": "EngineRoomTemperature", - "v": "3" - }, - { - "l": "MainCabinTemperature", - "v": "4" - }, - { - "l": "LiveWellTemperature", - "v": "5" - }, - { - "l": "BaitWellTemperature", - "v": "6" - }, - { - "l": "RefridgerationTemperature", - "v": "7" - }, - { - "l": "HeatingSystemTemperature", - "v": "8" - }, - { - "l": "DewPointTemperature", - "v": "9" - }, - { - "l": "ApparentWindChillTemperature", - "v": "10" - }, - { - "l": "TheoreticalWindChillTemperature", - "v": "11" - }, - { - "l": "HeatIndexTemperature", - "v": "12" - }, - { - "l": "FreezerTemperature", - "v": "13" - }, - { - "l": "ExhaustGasTemperature", - "v": "14" - }, - { - "l": "ShaftSealTemperature", - "v": "15" - } - ], - "category": "iicsensors$b", - "capabilities": { - "SHT4X$i": "true" - } - }, - { - "name": "SHT4X$ihuAct", - "label": "SHT4X$i Humidity", - "type": "boolean", - "default": "true", - "description": "Enable the $i. I2C SHT4X humidity sensor (bus $b)", - "category": "iicsensors$b", - "capabilities": { - "SHT4X$i": "true" - } - }, - { - "name": "SHT4X$ihuSrc", - "label": "SHT4X$i Humid Type", - "list": [ - { - "l": "OutsideHumidity", - "v": "1" - }, - { - "l": "Undef", - "v": "0xff" - } - ], - "category": "iicsensors$b", - "capabilities": { - "SHT4X": "true" - } - }, - { - "name": "SHT4X$iiid", - "label": "SHT4X$i N2K iid", - "type": "number", - "default": "$n", - "description": "the N2K instance id for the $i. SHT4X Temperature and Humidity ", - "category": "iicsensors$b", - "min": 0, - "max": 253, - "check": "checkMinMax", - "capabilities": { - "SHT4X$i": "true" - } - }, - { - "name": "SHT4X$iintv", - "label": "SHT4X$i Interval", - "type": "number", - "default": 2, - "description": "Interval(s) to query SHT4X Temperature and Humidity (1...300)", - "category": "iicsensors$b", - "min": 1, - "max": 300, - "check": "checkMinMax", - "capabilities": { - "SHT4X$i": "true" - } - }, - { - "name": "SHT4X$itmNam", - "label": "SHT4X$i Temp XDR", - "type": "String", - "default": "Temp$i", - "description": "set the XDR transducer name for the $i. SHT4X Temperature, leave empty to disable NMEA0183 XDR ", - "category": "iicsensors$b", - "capabilities": { - "SHT4X$i": "true" - } - }, - { - "name": "SHT4X$ihuNam", - "label": "SHT4X$i Humid XDR", - "type": "String", - "default": "Humidity$i", - "description": "set the XDR transducer name for the $i. SHT4X Humidity, leave empty to disable NMEA0183 XDR", - "category": "iicsensors$b", - "capabilities": { - "SHT4X$i": "true" + "SHT$xX$i": "true" } } ] @@ -444,6 +286,17 @@ "QMP6988$i": "true" } }, + { + "name": "QMP6988$isEnv", + "label": "QMP6988$i send Env", + "type": "boolean", + "default": "true", + "description": "also send PGN 130311", + "category": "iicsensors$b", + "capabilities": { + "QMP6988$i": "true" + } + }, { "name": "QMP6988$iintv", "label": "QMP6988-$i Interval", @@ -670,7 +523,7 @@ "label": "BME280-$i N2K iid", "type": "number", "default": "$n", - "description": "the N2K instance id for the BME280 Temperature and Humidity ", + "description": "the N2K instance id for the BME280 Temperature, Humidity, Pressure (PGN 130312,130313, 130314) ", "category": "iicsensors$b", "min": 0, "max": 253, @@ -679,6 +532,17 @@ "BME280$i": "true" } }, + { + "name": "BME280$isEnv", + "label": "BME280$i send Env", + "type": "boolean", + "default": "true", + "description": "also send PGN 130311", + "category": "iicsensors$b", + "capabilities": { + "BME280$i": "true" + } + }, { "name": "BME280$iintv", "label": "BME280-$i Interval", @@ -880,7 +744,7 @@ "label": "BMP280-$i N2K iid", "type": "number", "default": "$n", - "description": "the N2K instance id for the BMP280 Temperature", + "description": "the N2K instance id for the BMP280 Temperature/Pressure (PGN 130312,130314)", "category": "iicsensors$b", "min": 0, "max": 253, @@ -889,6 +753,17 @@ "BMP280$i": "true" } }, + { + "name": "BMP280$isEnv", + "label": "BMP280$i send Env", + "type": "boolean", + "default": "true", + "description": "also send PGN 130311", + "category": "iicsensors$b", + "capabilities": { + "BMP280$i": "true" + } + }, { "name": "BMP280$iintv", "label": "BMP280-$i Interval", From c21592599f26c40ddb43b4e6c305dfd775d15308 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Mon, 29 Sep 2025 17:51:41 +0200 Subject: [PATCH 34/48] re-add GwSHTXX* --- lib/iictask/GwSHTXX.cpp | 254 ++++++++++++++++++++++++++++++++++++++++ lib/iictask/GwSHTXX.h | 33 ++++++ 2 files changed, 287 insertions(+) create mode 100644 lib/iictask/GwSHTXX.cpp create mode 100644 lib/iictask/GwSHTXX.h diff --git a/lib/iictask/GwSHTXX.cpp b/lib/iictask/GwSHTXX.cpp new file mode 100644 index 0000000..0cfcca7 --- /dev/null +++ b/lib/iictask/GwSHTXX.cpp @@ -0,0 +1,254 @@ +#include "GwSHTXX.h" +#if defined(_GWSHT3X) || defined(_GWSHT4X) +class SHTXXConfig : public IICSensorBase{ + public: + String tmNam; + String huNam; + bool tmAct=false; + bool huAct=false; + bool sEnv=true; + tN2kHumiditySource huSrc; + tN2kTempSource tmSrc; + using IICSensorBase::IICSensorBase; + virtual bool isActive(){ + return tmAct || huAct; + } + virtual bool preinit(GwApi * api){ + GwLog *logger=api->getLogger(); + LOG_DEBUG(GwLog::LOG,"%s configured",prefix.c_str()); + addHumidXdr(api,*this); + addTempXdr(api,*this); + return isActive(); + } + virtual bool doMeasure(GwApi * api,double &temp, double &humid){ + return false; + } + virtual void measure(GwApi * api,TwoWire *wire, int counterId) override + { + GwLog *logger=api->getLogger(); + double temp = N2kDoubleNA; + double humid = N2kDoubleNA; + if (doMeasure(api,temp,humid)){ + temp = CToKelvin(temp); + LOG_DEBUG(GwLog::DEBUG, "%s measure temp=%2.1f, humid=%2.0f",prefix.c_str(), (float)temp, (float)humid); + if (huAct) + { + sendN2kHumidity(api, *this, humid, counterId); + } + if (tmAct) + { + sendN2kTemperature(api, *this, temp, counterId); + } + if (huAct || tmAct){ + sendN2kEnvironmentalParameters(api,*this,temp,humid,N2kDoubleNA,counterId); + } + } + } + +}; +/** + * we do not dynamically compute the config names + * just to get compile time errors if something does not fit + * correctly + */ +#define INITSHTXX(type,prefix,bus,baddr) \ +[] (type *s ,GwConfigHandler *cfg) { \ + CFG_SGET(s, tmNam, prefix); \ + CFG_SGET(s, huNam, prefix); \ + CFG_SGET(s, iid, prefix); \ + CFG_SGET(s, tmAct, prefix); \ + CFG_SGET(s, huAct, prefix); \ + CFG_SGET(s, intv, prefix); \ + CFG_SGET(s, huSrc, prefix); \ + CFG_SGET(s, tmSrc, prefix); \ + CFG_SGET(s, sEnv,prefix); \ + s->busId = bus; \ + s->addr = baddr; \ + s->ok = true; \ + s->intv *= 1000; \ +} + +#if defined(_GWSHT3X) +class SHT3XConfig; +static GwSensorConfigInitializerList configs3; +class SHT3XConfig : public SHTXXConfig{ + SHT3X *device=nullptr; + public: + using SHTXXConfig::SHTXXConfig; + virtual bool initDevice(GwApi * api,TwoWire *wire)override{ + if (! isActive()) return false; + device=new SHT3X(); + device->init(addr,wire); + GwLog *logger=api->getLogger(); + LOG_DEBUG(GwLog::LOG,"initialized %s at address %d, intv %ld",prefix.c_str(),(int)addr,intv); + return true; + } + virtual bool doMeasure(GwApi *api,double &temp, double &humid) override{ + if (!device) + return false; + int rt=0; + GwLog *logger=api->getLogger(); + if ((rt = device->get()) == 0) + { + temp = device->cTemp; + humid = device->humidity; + return true; + } + else{ + LOG_DEBUG(GwLog::DEBUG, "unable to query %s: %d",prefix.c_str(), rt); + } + return false; + } + virtual void readConfig(GwConfigHandler *cfg) override{ + if (ok) return; + configs3.readConfig(this,cfg); + return; + } +}; + +SensorBase::Creator creator3=[](GwApi *api,const String &prfx)-> SensorBase*{ + if (! configs3.knowsPrefix(prfx)) return nullptr; + return new SHT3XConfig(api,prfx); + }; +SensorBase::Creator registerSHT3X(GwApi *api){ + GwLog *logger=api->getLogger(); + #if defined(GWSHT3X) || defined (GWSHT3X11) + { + api->addSensor(creator3(api,"SHT3X11")); + CHECK_IIC1(); + #pragma message "GWSHT3X11 defined" + } + #endif + #if defined(GWSHT3X12) + { + api->addSensor(creator3(api,"SHT3X12")); + CHECK_IIC1(); + #pragma message "GWSHT3X12 defined" + } + #endif + #if defined(GWSHT3X21) + { + api->addSensor(creator3(api,"SHT3X21")); + CHECK_IIC2(); + #pragma message "GWSHT3X21 defined" + } + #endif + #if defined(GWSHT3X22) + { + api->addSensor(creator3(api,"SHT3X22")); + CHECK_IIC2(); + #pragma message "GWSHT3X22 defined" + } + #endif + return creator3; +}; + + +#define SCSHT3X(prefix, bus, addr) \ + GwSensorConfigInitializer __initCFGSHT3X ## prefix \ + (configs3,GwSensorConfig(#prefix,INITSHTXX(SHT3XConfig,prefix,bus,addr))); + +SCSHT3X(SHT3X11, 1, 0x44); +SCSHT3X(SHT3X12, 1, 0x45); +SCSHT3X(SHT3X21, 2, 0x44); +SCSHT3X(SHT3X22, 2, 0x45); + +#endif +#if defined(_GWSHT4X) +class SHT4XConfig; +static GwSensorConfigInitializerList configs4; +class SHT4XConfig : public SHTXXConfig{ + SHT4X *device=nullptr; + public: + using SHTXXConfig::SHTXXConfig; + virtual bool initDevice(GwApi * api,TwoWire *wire)override{ + if (! isActive()) return false; + device=new SHT4X(); + device->begin(wire,addr); + GwLog *logger=api->getLogger(); + LOG_DEBUG(GwLog::LOG,"initialized %s at address %d, intv %ld",prefix.c_str(),(int)addr,intv); + return true; + } + virtual bool doMeasure(GwApi *api,double &temp, double &humid) override{ + if (!device) + return false; + GwLog *logger=api->getLogger(); + if (device->update()) + { + temp = device->cTemp; + humid = device->humidity; + return true; + } + else{ + LOG_DEBUG(GwLog::DEBUG, "unable to query %s",prefix.c_str()); + } + return false; + } + virtual void readConfig(GwConfigHandler *cfg) override{ + if (ok) return; + configs4.readConfig(this,cfg); + return; + } +}; + +SensorBase::Creator creator4=[](GwApi *api,const String &prfx)-> SensorBase*{ + if (! configs4.knowsPrefix(prfx)) return nullptr; + return new SHT4XConfig(api,prfx); + }; +SensorBase::Creator registerSHT4X(GwApi *api){ + GwLog *logger=api->getLogger(); + #if defined(GWSHT4X) || defined (GWSHT4X11) + { + api->addSensor(creator3(api,"SHT4X11")); + CHECK_IIC1(); + #pragma message "GWSHT4X11 defined" + } + #endif + #if defined(GWSHT4X12) + { + api->addSensor(creator3(api,"SHT4X12")); + CHECK_IIC1(); + #pragma message "GWSHT4X12 defined" + } + #endif + #if defined(GWSHT4X21) + { + api->addSensor(creator3(api,"SHT4X21")); + CHECK_IIC2(); + #pragma message "GWSHT4X21 defined" + } + #endif + #if defined(GWSHT4X22) + { + api->addSensor(creator3(api,"SHT4X22")); + CHECK_IIC2(); + #pragma message "GWSHT4X22 defined" + } + #endif + return creator4; +}; + + +#define SCSHT4X(prefix, bus, addr) \ + GwSensorConfigInitializer __initCFGSHT4X ## prefix \ + (configs4,GwSensorConfig(#prefix,INITSHTXX(SHT4XConfig,prefix,bus,addr))); + +SCSHT4X(SHT4X11, 1, 0x44); +SCSHT4X(SHT4X12, 1, 0x45); +SCSHT4X(SHT4X21, 2, 0x44); +SCSHT4X(SHT4X22, 2, 0x45); +#endif +#endif +#ifndef _GWSHT3X +SensorBase::Creator registerSHT3X(GwApi *api){ + return SensorBase::Creator(); +} +#endif +#ifndef _GWSHT4X +SensorBase::Creator registerSHT4X(GwApi *api){ + return SensorBase::Creator(); +} +#endif + + + diff --git a/lib/iictask/GwSHTXX.h b/lib/iictask/GwSHTXX.h new file mode 100644 index 0000000..52829b9 --- /dev/null +++ b/lib/iictask/GwSHTXX.h @@ -0,0 +1,33 @@ +#ifndef _GWSHTXX_H +#define _GWSHTXX_H +#include "GwIicSensors.h" +#ifdef _GWIIC + #if defined(GWSHT3X) || defined(GWSHT3X11) || defined(GWSHT3X12) || defined(GWSHT3X21) || defined(GWSHT3X22) + #define _GWSHT3X + #endif + #if defined(GWSHT4X) || defined(GWSHT4X11) || defined(GWSHT4X12) || defined(GWSHT4X21) || defined(GWSHT4X22) + #define _GWSHT4X + #endif +#else + #undef _GWSHT3X + #undef GWSHT3X + #undef GWSHT3X11 + #undef GWSHT3X12 + #undef GWSHT3X21 + #undef GWSHT3X22 + #undef _GWSHT4X + #undef GWSHT4X + #undef GWSHT4X11 + #undef GWSHT4X12 + #undef GWSHT4X21 + #undef GWSHT4X22 +#endif +#ifdef _GWSHT3X + #include "SHT3X.h" +#endif +#ifdef _GWSHT4X + #include "SHT4X.h" +#endif +SensorBase::Creator registerSHT3X(GwApi *api); +SensorBase::Creator registerSHT4X(GwApi *api); +#endif \ No newline at end of file From 32862b9e29cd9032b290df1ed359d9954403b7fd Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Mon, 29 Sep 2025 17:53:14 +0200 Subject: [PATCH 35/48] avoid creating unmapped XDR entries for unset N2K values --- lib/nmea0183ton2k/NMEA0183DataToN2K.cpp | 2 +- lib/nmea2kto0183/N2kDataToNMEA0183.cpp | 47 +++++++++++++------------ lib/xdrmappings/GwXDRMappings.cpp | 3 +- lib/xdrmappings/GwXDRMappings.h | 2 +- 4 files changed, 28 insertions(+), 26 deletions(-) diff --git a/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp b/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp index ef17866..6989570 100644 --- a/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp +++ b/lib/nmea0183ton2k/NMEA0183DataToN2K.cpp @@ -143,7 +143,7 @@ private: */ GwXDRFoundMapping getOtherFieldMapping(GwXDRFoundMapping &found, int field){ if (found.empty) return GwXDRFoundMapping(); - return xdrMappings->getMapping(found.definition->category, + return xdrMappings->getMapping(0,found.definition->category, found.definition->selector, field, found.instanceId); diff --git a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp index e2b61fd..0c0e68c 100644 --- a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp +++ b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp @@ -1147,12 +1147,12 @@ private: double Level=N2kDoubleNA; double Capacity=N2kDoubleNA; if (ParseN2kPGN127505(N2kMsg,Instance,FluidType,Level,Capacity)) { - GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRFLUID,FluidType,0,Instance); + GwXDRFoundMapping mapping=xdrMappings->getMapping(Level,XDRFLUID,FluidType,0,Instance); if (updateDouble(&mapping,Level)){ LOG_DEBUG(GwLog::DEBUG+1,"found fluidlevel mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(Level)); } - mapping=xdrMappings->getMapping(XDRFLUID,FluidType,1,Instance); + mapping=xdrMappings->getMapping(Capacity, XDRFLUID,FluidType,1,Instance); if (updateDouble(&mapping,Capacity)){ LOG_DEBUG(GwLog::DEBUG+1,"found fluid capacity mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(Capacity)); @@ -1170,19 +1170,19 @@ private: double BatteryTemperature=N2kDoubleNA; if (ParseN2kPGN127508(N2kMsg,BatteryInstance,BatteryVoltage,BatteryCurrent,BatteryTemperature,SID)) { int i=0; - GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRBAT,0,0,BatteryInstance); + GwXDRFoundMapping mapping=xdrMappings->getMapping(BatteryVoltage, XDRBAT,0,0,BatteryInstance); if (updateDouble(&mapping,BatteryVoltage)){ LOG_DEBUG(GwLog::DEBUG+1,"found BatteryVoltage mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(BatteryVoltage)); i++; } - mapping=xdrMappings->getMapping(XDRBAT,0,1,BatteryInstance); + mapping=xdrMappings->getMapping(BatteryCurrent,XDRBAT,0,1,BatteryInstance); if (updateDouble(&mapping,BatteryCurrent)){ LOG_DEBUG(GwLog::DEBUG+1,"found BatteryCurrent mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(BatteryCurrent)); i++; } - mapping=xdrMappings->getMapping(XDRBAT,0,2,BatteryInstance); + mapping=xdrMappings->getMapping(BatteryTemperature,XDRBAT,0,2,BatteryInstance); if (updateDouble(&mapping,BatteryTemperature)){ LOG_DEBUG(GwLog::DEBUG+1,"found BatteryTemperature mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(BatteryTemperature)); @@ -1214,13 +1214,13 @@ private: SendMessage(NMEA0183Msg); } int i=0; - GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRTEMP,N2kts_OutsideTemperature,0,0); + GwXDRFoundMapping mapping=xdrMappings->getMapping(OutsideAmbientAirTemperature, XDRTEMP,N2kts_OutsideTemperature,0,0); if (updateDouble(&mapping,OutsideAmbientAirTemperature)){ LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(OutsideAmbientAirTemperature)); i++; } - mapping=xdrMappings->getMapping(XDRPRESSURE,N2kps_Atmospheric,0,0); + mapping=xdrMappings->getMapping(AtmosphericPressure,XDRPRESSURE,N2kps_Atmospheric,0,0); if (updateDouble(&mapping,AtmosphericPressure)){ LOG_DEBUG(GwLog::DEBUG+1,"found pressure mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(AtmosphericPressure)); @@ -1255,19 +1255,19 @@ private: SendMessage(NMEA0183Msg); } - GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRTEMP,TempSource,0,0); + GwXDRFoundMapping mapping=xdrMappings->getMapping(Temperature, XDRTEMP,TempSource,0,0); if (updateDouble(&mapping,Temperature)){ LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(Temperature)); i++; } - mapping=xdrMappings->getMapping(XDRHUMIDITY,HumiditySource,0,0); + mapping=xdrMappings->getMapping(Humidity, XDRHUMIDITY,HumiditySource,0,0); if (updateDouble(&mapping,Humidity)){ LOG_DEBUG(GwLog::DEBUG+1,"found humidity mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(Humidity)); i++; } - mapping=xdrMappings->getMapping(XDRPRESSURE,N2kps_Atmospheric,0,0); + mapping=xdrMappings->getMapping(AtmosphericPressure, XDRPRESSURE,N2kps_Atmospheric,0,0); if (updateDouble(&mapping,AtmosphericPressure)){ LOG_DEBUG(GwLog::DEBUG+1,"found pressure mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(AtmosphericPressure)); @@ -1302,12 +1302,12 @@ private: SendMessage(NMEA0183Msg); } - GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRTEMP,(int)TemperatureSource,0,TemperatureInstance); + GwXDRFoundMapping mapping=xdrMappings->getMapping(Temperature, XDRTEMP,(int)TemperatureSource,0,TemperatureInstance); if (updateDouble(&mapping,Temperature)){ LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(Temperature)); } - mapping=xdrMappings->getMapping(XDRTEMP,(int)TemperatureSource,1,TemperatureInstance); + mapping=xdrMappings->getMapping(setTemperature, XDRTEMP,(int)TemperatureSource,1,TemperatureInstance); if (updateDouble(&mapping,setTemperature)){ LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(setTemperature)); @@ -1325,12 +1325,13 @@ private: LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN); return; } - GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRHUMIDITY,(int)HumiditySource,0,HumidityInstance); + GwXDRFoundMapping mapping; + mapping=xdrMappings->getMapping(ActualHumidity, XDRHUMIDITY,(int)HumiditySource,0,HumidityInstance); if (updateDouble(&mapping,ActualHumidity)){ LOG_DEBUG(GwLog::DEBUG+1,"found humidity mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(ActualHumidity)); } - mapping=xdrMappings->getMapping(XDRHUMIDITY,(int)HumiditySource,1,HumidityInstance); + mapping=xdrMappings->getMapping(SetHumidity, XDRHUMIDITY,(int)HumiditySource,1,HumidityInstance); if (updateDouble(&mapping,SetHumidity)){ LOG_DEBUG(GwLog::DEBUG+1,"found humidity mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(SetHumidity)); @@ -1348,7 +1349,7 @@ private: LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN); return; } - GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRPRESSURE,(int)PressureSource,0,PressureInstance); + GwXDRFoundMapping mapping=xdrMappings->getMapping(ActualPressure, XDRPRESSURE,(int)PressureSource,0,PressureInstance); if (! updateDouble(&mapping,ActualPressure)) return; LOG_DEBUG(GwLog::DEBUG+1,"found pressure mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(ActualPressure)); @@ -1366,12 +1367,12 @@ private: LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN); } for (int i=0;i<8;i++){ - GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRENGINE,0,i,instance); + GwXDRFoundMapping mapping=xdrMappings->getMapping(values[i], XDRENGINE,0,i,instance); if (! updateDouble(&mapping,values[i])) continue; addToXdr(mapping.buildXdrEntry(values[i])); } for (int i=0;i< 2;i++){ - GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRENGINE,0,i+8,instance); + GwXDRFoundMapping mapping=xdrMappings->getMapping(ivalues[i],XDRENGINE,0,i+8,instance); if (! updateDouble(&mapping,ivalues[i])) continue; addToXdr(mapping.buildXdrEntry((double)ivalues[i])); } @@ -1387,7 +1388,7 @@ private: LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN); } for (int i=0;i<3;i++){ - GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRATTITUDE,0,i,instance); + GwXDRFoundMapping mapping=xdrMappings->getMapping(values[i], XDRATTITUDE,0,i,instance); if (! updateDouble(&mapping,values[i])) continue; addToXdr(mapping.buildXdrEntry(values[i])); } @@ -1401,15 +1402,15 @@ private: speed,pressure,tilt)){ LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN); } - GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRENGINE,0,10,instance); + GwXDRFoundMapping mapping=xdrMappings->getMapping(speed, XDRENGINE,0,10,instance); if (updateDouble(&mapping,speed)){ addToXdr(mapping.buildXdrEntry(speed)); } - mapping=xdrMappings->getMapping(XDRENGINE,0,11,instance); + mapping=xdrMappings->getMapping(pressure, XDRENGINE,0,11,instance); if (updateDouble(&mapping,pressure)){ addToXdr(mapping.buildXdrEntry(pressure)); } - mapping=xdrMappings->getMapping(XDRENGINE,0,12,instance); + mapping=xdrMappings->getMapping(tilt, XDRENGINE,0,12,instance); if (updateDouble(&mapping,tilt)){ addToXdr(mapping.buildXdrEntry((double)tilt)); } @@ -1435,12 +1436,12 @@ private: LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN); return; } - GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRTEMP,(int)TemperatureSource,0,TemperatureInstance); + GwXDRFoundMapping mapping=xdrMappings->getMapping(Temperature, XDRTEMP,(int)TemperatureSource,0,TemperatureInstance); if (updateDouble(&mapping,Temperature)){ LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(Temperature)); } - mapping=xdrMappings->getMapping(XDRTEMP,(int)TemperatureSource,1,TemperatureInstance); + mapping=xdrMappings->getMapping(setTemperature, XDRTEMP,(int)TemperatureSource,1,TemperatureInstance); if (updateDouble(&mapping,setTemperature)){ LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str()); addToXdr(mapping.buildXdrEntry(setTemperature)); diff --git a/lib/xdrmappings/GwXDRMappings.cpp b/lib/xdrmappings/GwXDRMappings.cpp index 28f88c2..211d394 100644 --- a/lib/xdrmappings/GwXDRMappings.cpp +++ b/lib/xdrmappings/GwXDRMappings.cpp @@ -431,7 +431,8 @@ GwXDRFoundMapping GwXDRMappings::getMapping(String xName,String xType,String xUn } return selectMapping(&(it->second),instance,n183Key.c_str()); } -GwXDRFoundMapping GwXDRMappings::getMapping(GwXDRCategory category,int selector,int field,int instance){ +GwXDRFoundMapping GwXDRMappings::getMapping(double value,GwXDRCategory category,int selector,int field,int instance){ + if (value == N2kDoubleNA) return GwXDRFoundMapping(); //do not add to unknown mappings unsigned long n2kKey=GwXDRMappingDef::n2kKey(category,selector,field); auto it=n2kMap.find(n2kKey); if (it == n2kMap.end()){ diff --git a/lib/xdrmappings/GwXDRMappings.h b/lib/xdrmappings/GwXDRMappings.h index 198a729..21eb9c3 100644 --- a/lib/xdrmappings/GwXDRMappings.h +++ b/lib/xdrmappings/GwXDRMappings.h @@ -244,7 +244,7 @@ class GwXDRMappings{ //get the mappings //the returned mapping will exactly contain one mapping def GwXDRFoundMapping getMapping(String xName,String xType,String xUnit); - GwXDRFoundMapping getMapping(GwXDRCategory category,int selector,int field=0,int instance=-1); + GwXDRFoundMapping getMapping(double value,GwXDRCategory category,int selector,int field=0,int instance=-1); String getXdrEntry(String mapping, double value,int instance=0); const char * getUnMapped(); const GwXDRType * findType(const String &typeString, const String &unitString) const; From 432a10bfb10af137e34837b0415dee1f95c1d684 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Mon, 29 Sep 2025 17:54:16 +0200 Subject: [PATCH 36/48] correctly send 130311 for QMP6988 --- lib/iictask/GwIicSensors.h | 12 +++++++++--- lib/iictask/GwQMP6988.cpp | 3 +++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/iictask/GwIicSensors.h b/lib/iictask/GwIicSensors.h index a9465d5..49fe2fe 100644 --- a/lib/iictask/GwIicSensors.h +++ b/lib/iictask/GwIicSensors.h @@ -108,9 +108,15 @@ void sendN2kEnvironmentalParameters(GwApi *api,CFG &cfg,double tmValue, double h tN2kMsg msg; SetN2kEnvironmentalParameters(msg,1,cfg.tmSrc,tmValue,cfg.huSrc,huValue,prValue); api->sendN2kMessage(msg); - api->increment(counterId,cfg.prefix+String("hum")); - api->increment(counterId,cfg.prefix+String("press")); - api->increment(counterId,cfg.prefix+String("temp")); + if (huValue != N2kDoubleNA){ + api->increment(counterId,cfg.prefix+String("ehum")); + } + if (prValue != N2kDoubleNA){ + api->increment(counterId,cfg.prefix+String("epress")); + } + if (tmValue != N2kDoubleNA){ + api->increment(counterId,cfg.prefix+String("etemp")); + } } #ifndef _GWI_IIC1 diff --git a/lib/iictask/GwQMP6988.cpp b/lib/iictask/GwQMP6988.cpp index 12b648f..1638907 100644 --- a/lib/iictask/GwQMP6988.cpp +++ b/lib/iictask/GwQMP6988.cpp @@ -10,6 +10,8 @@ class QMP6988Config : public IICSensorBase{ String prNam="Pressure"; bool prAct=true; bool sEnv=true; + tN2kTempSource tmSrc=tN2kTempSource::N2kts_InsideTemperature; + tN2kHumiditySource huSrc=tN2kHumiditySource::N2khs_Undef; tN2kPressureSource prSrc=tN2kPressureSource::N2kps_Atmospheric; float prOff=0; QMP6988 *device=nullptr; @@ -40,6 +42,7 @@ class QMP6988Config : public IICSensorBase{ float computed=pressure+prOff; LOG_DEBUG(GwLog::DEBUG,"%s measure %2.0fPa, computed %2.0fPa",prefix.c_str(), pressure,computed); sendN2kPressure(api,*this,computed,counterId); + sendN2kEnvironmentalParameters(api,*this,N2kDoubleNA,N2kDoubleNA,computed,counterId); } From 566d84d3e64d8feac07ab5626a6c05a6b76005ba Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Mon, 29 Sep 2025 18:04:04 +0200 Subject: [PATCH 37/48] correctly handle ifdefs for SHT4X --- lib/iictask/SHT4X.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/iictask/SHT4X.cpp b/lib/iictask/SHT4X.cpp index 7dc9235..6d14473 100644 --- a/lib/iictask/SHT4X.cpp +++ b/lib/iictask/SHT4X.cpp @@ -1,4 +1,6 @@ -#include "SHT4X.h" +#include "GwSHTXX.h" +#ifdef _GWSHT4X + uint8_t crc8(const uint8_t *data, int len) { /* * @@ -126,3 +128,4 @@ void SHT4X::setHeater(sht4x_heater_t heat) { sht4x_heater_t SHT4X::getHeater(void) { return _heater; } +#endif \ No newline at end of file From b68341312904b8b8e2ab0c1ae5e743621769d81b Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Mon, 29 Sep 2025 19:13:02 +0200 Subject: [PATCH 38/48] #117: add handling for an output enable pin for serial channels --- lib/channel/GwChannelList.cpp | 54 +++++++++++++++++++++++++++++------ webinstall/build.yaml | 22 ++++++++++++++ 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/lib/channel/GwChannelList.cpp b/lib/channel/GwChannelList.cpp index 87755b4..d89f227 100644 --- a/lib/channel/GwChannelList.cpp +++ b/lib/channel/GwChannelList.cpp @@ -15,8 +15,10 @@ class SerInit{ int tx=-1; int mode=-1; int fixedBaud=-1; - SerInit(int s,int r,int t, int m, int b=-1): - serial(s),rx(r),tx(t),mode(m),fixedBaud(b){} + int ena=-1; + int elow=1; + SerInit(int s,int r,int t, int m, int b=-1,int en=-1,int el=-1): + serial(s),rx(r),tx(t),mode(m),fixedBaud(b),ena(en),elow(el){} }; std::vector serialInits; @@ -47,11 +49,20 @@ static int typeFromMode(const char *mode){ #ifndef GWSERIAL_RX #define GWSERIAL_RX -1 #endif +#ifndef GWSERIAL_ENA +#define GWSERIAL_ENA -1 +#endif +#ifndef GWSERIAL_ELO +#define GWSERIAL_ELO 1 +#endif +#ifndef GWSERIAL_BAUD +#define GWSERIAL_BAUD -1 +#endif #ifdef GWSERIAL_TYPE - CFG_SERIAL(SERIAL1_CHANNEL_ID, GWSERIAL_RX, GWSERIAL_TX, GWSERIAL_TYPE) + CFG_SERIAL(SERIAL1_CHANNEL_ID, GWSERIAL_RX, GWSERIAL_TX, GWSERIAL_TYPE,GWSERIAL_BAUD,GWSERIAL_ENA,GWSERIAL_ELO) #else #ifdef GWSERIAL_MODE -CFG_SERIAL(SERIAL1_CHANNEL_ID, GWSERIAL_RX, GWSERIAL_TX, typeFromMode(GWSERIAL_MODE)) +CFG_SERIAL(SERIAL1_CHANNEL_ID, GWSERIAL_RX, GWSERIAL_TX, typeFromMode(GWSERIAL_MODE),GWSERIAL_BAUD,GWSERIAL_ENA,GWSERIAL_ELO) #endif #endif // serial 2 @@ -61,11 +72,20 @@ CFG_SERIAL(SERIAL1_CHANNEL_ID, GWSERIAL_RX, GWSERIAL_TX, typeFromMode(GWSERIAL_M #ifndef GWSERIAL2_RX #define GWSERIAL2_RX -1 #endif +#ifndef GWSERIAL2_ENA +#define GWSERIAL2_ENA -1 +#endif +#ifndef GWSERIAL2_ELO +#define GWSERIAL2_ELO 1 +#endif +#ifndef GWSERIAL2_BAUD +#define GWSERIAL2_BAUD -1 +#endif #ifdef GWSERIAL2_TYPE - CFG_SERIAL(SERIAL2_CHANNEL_ID, GWSERIAL2_RX, GWSERIAL2_TX, GWSERIAL2_TYPE) + CFG_SERIAL(SERIAL2_CHANNEL_ID, GWSERIAL2_RX, GWSERIAL2_TX, GWSERIAL2_TYPE,GWSERIAL2_BAUD,GWSERIAL2_ENA,GWSERIAL2_ELO) #else #ifdef GWSERIAL2_MODE -CFG_SERIAL(SERIAL2_CHANNEL_ID, GWSERIAL2_RX, GWSERIAL2_TX, typeFromMode(GWSERIAL2_MODE)) +CFG_SERIAL(SERIAL2_CHANNEL_ID, GWSERIAL2_RX, GWSERIAL2_TX, typeFromMode(GWSERIAL2_MODE),GWSERIAL2_BAUD,GWSERIAL2_ENA,GWSERIAL2_ELO) #endif #endif class GwSerialLog : public GwLogWriter @@ -300,7 +320,7 @@ static ChannelParam * findChannelParam(int id){ return param; } -static GwSerial * createSerialImpl(GwConfigHandler *config,GwLog *logger, int idx,int type,int rx,int tx, bool setLog){ +static GwSerial * createSerialImpl(GwConfigHandler *config,GwLog *logger, int idx,int type,int rx,int tx, bool setLog,int ena=-1,int elow=1){ LOG_DEBUG(GwLog::DEBUG,"create serial: channel=%d, rx=%d,tx=%d", idx,rx,tx); ChannelParam *param=findChannelParam(idx); @@ -325,6 +345,24 @@ static GwSerial * createSerialImpl(GwConfigHandler *config,GwLog *logger, int id LOG_DEBUG(GwLog::ERROR,"invalid serial config with id %d",param->id); return nullptr; } + if (ena >= 0){ + if (type == GWSERIAL_TYPE_UNI){ + String cfgMode=config->getString(param->direction); + int value=0; + if (cfgMode == "send"){ + value=elow?0:1; + } + else{ + value=elow?1:0; + } + LOG_DEBUG(GwLog::LOG,"serial %d: setting output enable %d to %d",param->id,ena,value); + pinMode(ena,OUTPUT); + digitalWrite(ena,value); + } + else{ + LOG_DEBUG(GwLog::ERROR,"serial %d: output enable ignored for mode %d",param->id, type); + } + } serialStream->begin(config->getInt(param->baud,115200),SERIAL_8N1,rx,tx); if (setLog){ logger->setWriter(new GwSerialLog(serialStream,config->getBool(param->preventLog,false))); @@ -446,7 +484,7 @@ void GwChannelList::begin(bool fallbackSerial){ //new serial config handling for (auto &&init:serialInits){ 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.mode,init.rx,init.tx,false); + GwSerial *ser=createSerialImpl(config,logger,init.serial,init.mode,init.rx,init.tx,false,init.ena,init.elow); if (ser != nullptr){ channel=createChannel(logger,config,init.serial,ser); if (channel != nullptr){ diff --git a/webinstall/build.yaml b/webinstall/build.yaml index ab2ca3c..61ddf19 100644 --- a/webinstall/build.yaml +++ b/webinstall/build.yaml @@ -339,6 +339,26 @@ types: help: 'number of the GPIO pin for the transmit function' target: "define:#serial#TX" mandatory: true + - &serialEnablePin + <<: *gpiopin + key: ENA + label: "enable pin" + help: "GPIO pin for output enable" + target: "define:#serial#ENA" + mandatory: false + - &serialEnableLow + type: checkbox + key: ELOW + label: "enable low" + target: "define:#serial#ELO" + default: true + help: "set: low on enable pin for output, unset: high on enable pin for output" + values: + - key: true + value: 1 + - key: false + value: 0 + - &serialValues - key: true children: @@ -355,6 +375,8 @@ types: children: - *serialRX - *serialTX + - *serialEnablePin + - *serialEnableLow - key: bi value: 2 label: "BiDir" From 68239f6199d6c2934c85c1c721889221fd633490 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Mon, 29 Sep 2025 19:34:35 +0200 Subject: [PATCH 39/48] add fixed baud to cibuild, allow enable pin also for pure rx/tx serial --- lib/channel/GwChannelList.cpp | 25 +++++++++++++++++-------- webinstall/build.yaml | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/lib/channel/GwChannelList.cpp b/lib/channel/GwChannelList.cpp index d89f227..cc70ecc 100644 --- a/lib/channel/GwChannelList.cpp +++ b/lib/channel/GwChannelList.cpp @@ -346,15 +346,23 @@ static GwSerial * createSerialImpl(GwConfigHandler *config,GwLog *logger, int id return nullptr; } if (ena >= 0){ + int value=-1; if (type == GWSERIAL_TYPE_UNI){ - String cfgMode=config->getString(param->direction); - int value=0; - if (cfgMode == "send"){ + String cfgMode=config->getString(param->direction); + if (cfgMode == "send"){ value=elow?0:1; - } - else{ - value=elow?1:0; - } + } + else{ + value=elow?1:0; + } + } + if (type == GWSERIAL_TYPE_RX){ + value=elow?1:0; + } + if (type == GWSERIAL_TYPE_TX){ + value=elow?0:1; + } + if (value >= 0){ LOG_DEBUG(GwLog::LOG,"serial %d: setting output enable %d to %d",param->id,ena,value); pinMode(ena,OUTPUT); digitalWrite(ena,value); @@ -483,7 +491,8 @@ void GwChannelList::begin(bool fallbackSerial){ //new serial config handling for (auto &&init:serialInits){ - LOG_INFO("creating serial channel %d, rx=%d,tx=%d,type=%d",init.serial,init.rx,init.tx,init.mode); + LOG_INFO("creating serial channel %d, rx=%d,tx=%d,type=%d fixedBaud=%d ena=%d elow=%d", + init.serial,init.rx,init.tx,init.mode,init.fixedBaud,init.ena,init.elow); GwSerial *ser=createSerialImpl(config,logger,init.serial,init.mode,init.rx,init.tx,false,init.ena,init.elow); if (ser != nullptr){ channel=createChannel(logger,config,init.serial,ser); diff --git a/webinstall/build.yaml b/webinstall/build.yaml index 61ddf19..4085704 100644 --- a/webinstall/build.yaml +++ b/webinstall/build.yaml @@ -326,6 +326,24 @@ types: - PPIN23 - PPIN25 - PPIN33 + + - &baudselect + type: dropdown + help: 'Select the baud rate' + values: + - {label: unset,value:} + - 1200 + - 2400 + - 4800 + - 9600 + - 14400 + - 19200 + - 28800 + - 38400 + - 57600 + - 115200 + - 230400 + - 460800 - &serialRX <<: *gpioinput @@ -358,6 +376,13 @@ types: value: 1 - key: false value: 0 + + - &serialFixedBaud + <<: *baudselect + key: fixedBaud + label: "fixed baud" + help: "you can set a fixed baud rate here, this disables changing the baud rate in the UI" + target: "define:#serial#BAUD" - &serialValues - key: true @@ -377,6 +402,7 @@ types: - *serialTX - *serialEnablePin - *serialEnableLow + - *serialFixedBaud - key: bi value: 2 label: "BiDir" @@ -385,18 +411,25 @@ types: children: - *serialRX - *serialTX + - *serialFixedBaud - key: rx value: 3 label: "RX" description: "Input only" children: - *serialRX + - *serialEnablePin + - *serialEnableLow + - *serialFixedBaud - key: tx value: 1 label: "TX" description: "output only" children: - *serialTX + - *serialEnablePin + - *serialEnableLow + - *serialFixedBaud - &serial1 type: checkbox label: 'Serial 1' From 3cd508a2391a2952873edeff280ef5e779883d9c Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Tue, 30 Sep 2025 12:13:06 +0200 Subject: [PATCH 40/48] better description of pins for cibuild --- webinstall/build.yaml | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/webinstall/build.yaml b/webinstall/build.yaml index 4085704..3590c59 100644 --- a/webinstall/build.yaml +++ b/webinstall/build.yaml @@ -320,13 +320,22 @@ types: - &protogpio - {label: unset,value:} - - PPIN19 - - PPIN21 - - PPIN22 - - PPIN23 - - PPIN25 - - PPIN33 - + - {label: "PPIN19(left-2,gpio 19)", value: PPIN19} + - {label: "PPIN21(right-1,gpio 21)", value: PPIN21} + - {label: "PPIN22(left-1,gpio 22)", value: PPIN22} + - {label: "PPIN23(left-3,gpio 23)", value: PPIN23} + - {label: "PPIN25(right-2,gpio 25)", value: PPIN25} + - {label: "PPIN33(left-4,gpio 33)", value: PPIN33} + + - &protogpios3 + - {label: unset,value:} + - {label: "PPIN19(left-2,gpio 6)", value: PPIN19} + - {label: "PPIN21(right-1,gpio 39)", value: PPIN21} + - {label: "PPIN22(left-1,gpio 5)", value: PPIN22} + - {label: "PPIN23(left-3,gpio 7)", value: PPIN23} + - {label: "PPIN25(right-2,gpio 38)", value: PPIN25} + - {label: "PPIN33(left-4,gpio 8)", value: PPIN33} + - &baudselect type: dropdown help: 'Select the baud rate' @@ -369,7 +378,7 @@ types: key: ELOW label: "enable low" target: "define:#serial#ELO" - default: true + default: false help: "set: low on enable pin for output, unset: high on enable pin for output" values: - key: true @@ -774,8 +783,8 @@ types: url: "https://docs.m5stack.com/en/atom/atomhub" label: "Hub Proto" base: - gpioinputv: *protogpio - gpiopinv: *protogpio + gpioinputv: "#protogpio#" + gpiopinv: "#protogpio#" children: *m5protochildren - value: M5_PORTABC @@ -802,6 +811,7 @@ config: gpiopinv: *gpiopinv gpioinputv: *gpioinputv grv: "" + protogpio: *protogpio values: - value: m5stack-atom-generic label: m5stack-atom @@ -816,6 +826,8 @@ config: description: "M5 Stack AtomS3 light" url: "http://docs.m5stack.com/en/core/AtomS3%20Lite" resource: *esp32default + base: + protogpio: *protogpios3 children: - *m5base - *m5groove @@ -830,7 +842,7 @@ config: - value: nodemcu-generic label: nodemcu - description: "Node mcu esp32" + description: "Node mcu esp32,4MB flash, no PSRAM" url: "https://docs.platformio.org/en/stable/boards/espressif32/nodemcu-32s.html" resource: *esp32default children: @@ -864,6 +876,7 @@ config: base: gpiopinv: *gpiopinvs3 gpioinputv: *gpiopinvs3 + protogpio: *protogpios3 children: - *serial1 - *serial2 From 034a338a819a39c9cf939e0a7091d8a10c9ae187 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Tue, 30 Sep 2025 12:16:51 +0200 Subject: [PATCH 41/48] set default for serial enable low = 0 --- lib/channel/GwChannelList.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/channel/GwChannelList.cpp b/lib/channel/GwChannelList.cpp index cc70ecc..8a85863 100644 --- a/lib/channel/GwChannelList.cpp +++ b/lib/channel/GwChannelList.cpp @@ -53,7 +53,7 @@ static int typeFromMode(const char *mode){ #define GWSERIAL_ENA -1 #endif #ifndef GWSERIAL_ELO -#define GWSERIAL_ELO 1 +#define GWSERIAL_ELO 0 #endif #ifndef GWSERIAL_BAUD #define GWSERIAL_BAUD -1 @@ -76,7 +76,7 @@ CFG_SERIAL(SERIAL1_CHANNEL_ID, GWSERIAL_RX, GWSERIAL_TX, typeFromMode(GWSERIAL_M #define GWSERIAL2_ENA -1 #endif #ifndef GWSERIAL2_ELO -#define GWSERIAL2_ELO 1 +#define GWSERIAL2_ELO 0 #endif #ifndef GWSERIAL2_BAUD #define GWSERIAL2_BAUD -1 From 5493c9695cef9f2bb2db0ec0dc6ae828b907ba56 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Tue, 30 Sep 2025 15:27:50 +0200 Subject: [PATCH 42/48] add buildname to cibuild, use the firmware name for file names --- .github/workflows/release.yml | 2 +- lib/appinfo/GwAppInfo.h | 5 ++++- post.py | 8 ++++++-- src/main.cpp | 6 +++--- webinstall/build.yaml | 7 +++++++ webinstall/cibuild.js | 34 +++++++++++++++++++++++++++++++++- 6 files changed, 54 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2aeae17..ea089d5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,5 +62,5 @@ jobs: with: repo_token: ${{ secrets.GITHUB_TOKEN }} tag: ${{ steps.version.outputs.version}} - file: ./.pio/build/*/*-{all,update}.bin + file: ./.pio/build/*/*${{ steps.version.outputs.version }}*-{all,update}.bin file_glob: true diff --git a/lib/appinfo/GwAppInfo.h b/lib/appinfo/GwAppInfo.h index f7eb143..0ccf711 100644 --- a/lib/appinfo/GwAppInfo.h +++ b/lib/appinfo/GwAppInfo.h @@ -14,6 +14,9 @@ #define LOGLEVEL GwLog::DEBUG #endif #endif - +#ifdef GWBUILD_NAME +#define FIRMWARE_TYPE GWSTRINGIFY(GWBUILD_NAME) +#else #define FIRMWARE_TYPE GWSTRINGIFY(PIO_ENV_BUILD) +#endif #define IDF_VERSION GWSTRINGIFY(ESP_IDF_VERSION_MAJOR) "." GWSTRINGIFY(ESP_IDF_VERSION_MINOR) "." GWSTRINGIFY(ESP_IDF_VERSION_PATCH) \ No newline at end of file diff --git a/post.py b/post.py index 8fe2f27..a09635f 100644 --- a/post.py +++ b/post.py @@ -2,6 +2,7 @@ Import("env", "projenv") import os import glob import shutil +import re print("##post script running") HDROFFSET=288 @@ -39,6 +40,7 @@ def post(source,target,env): appoffset=env.subst("$ESP32_APP_OFFSET") firmware=env.subst("$BUILD_DIR/${PROGNAME}.bin") (fwname,version)=getFirmwareInfo(firmware) + fwname=re.sub(r"[^0-9A-Za-z_.-]*","",fwname) print("found fwname=%s, fwversion=%s"%(fwname,version)) python=env.subst("$PYTHONEXE") print("base=%s,esptool=%s,appoffset=%s,uploaderflags=%s"%(base,esptool,appoffset,uploaderflags)) @@ -70,10 +72,12 @@ def post(source,target,env): print("running %s"%" ".join(cmd)) env.Execute(" ".join(cmd),"#testpost") ofversion="-"+version - versionedFile=os.path.join(outdir,"%s%s-update.bin"%(base,ofversion)) + versionedFile=os.path.join(outdir,"%s%s-update.bin"%(fwname,ofversion)) shutil.copyfile(firmware,versionedFile) - versioneOutFile=os.path.join(outdir,"%s%s-all.bin"%(base,ofversion)) + print(f"wrote {versionedFile}") + versioneOutFile=os.path.join(outdir,"%s%s-all.bin"%(fwname,ofversion)) shutil.copyfile(outfile,versioneOutFile) + print(f"wrote {versioneOutFile}") env.AddPostAction( "$BUILD_DIR/${PROGNAME}.bin", post diff --git a/src/main.cpp b/src/main.cpp index de56930..44c715f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -72,9 +72,9 @@ const unsigned long HEAP_REPORT_TIME=2000; //set to 0 to disable heap reporting #define MAX_NMEA2000_MESSAGE_SEASMART_SIZE 500 #define MAX_NMEA0183_MESSAGE_SIZE MAX_NMEA2000_MESSAGE_SEASMART_SIZE //assert length of firmware name and version -CASSERT(strlen(FIRMWARE_TYPE) <= 32, "environment name (FIRMWARE_TYPE) must not exceed 32 chars"); -CASSERT(strlen(VERSION) <= 32, "VERSION must not exceed 32 chars"); -CASSERT(strlen(IDF_VERSION) <= 32,"IDF_VERSION must not exceed 32 chars"); +CASSERT(strlen(FIRMWARE_TYPE) <= 31, "environment name (FIRMWARE_TYPE) must not exceed 32 chars"); +CASSERT(strlen(VERSION) <= 31, "VERSION must not exceed 32 chars"); +CASSERT(strlen(IDF_VERSION) <= 31,"IDF_VERSION must not exceed 32 chars"); //https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/app_image_format.html //and removed the bugs in the doc... __attribute__((section(".rodata_custom_desc"))) esp_app_desc_t custom_app_desc = { diff --git a/webinstall/build.yaml b/webinstall/build.yaml index 3590c59..672c435 100644 --- a/webinstall/build.yaml +++ b/webinstall/build.yaml @@ -803,6 +803,13 @@ resources: config: children: + - type: string + label: 'Build Name' + key: buildname + target: "define:GWBUILD_NAME" + help: "Set a name to identify your build. Will also become the name for the generated files and the firmware type in the image. Max 31 characters." + max: 31 + allowed: "0-9A-Za-z_-" - type: select target: environment label: 'Board' diff --git a/webinstall/cibuild.js b/webinstall/cibuild.js index 690da1d..b64787c 100644 --- a/webinstall/cibuild.js +++ b/webinstall/cibuild.js @@ -234,7 +234,11 @@ class PipelineInfo{ } const downloadConfig=()=>{ let name=configName; - if (isModified) name=name.replace(/[0-9]*$/,'')+formatDate(undefined,true); + const buildname=config["root:buildname"] + if (buildname && name != buildname){ + name+="-"+buildname+"-"; + } + name=name.replace(/[0-9]*$/,'')+formatDate(undefined,true); name+=".json"; fileDownload(JSON.stringify(config),name); } @@ -521,6 +525,34 @@ class PipelineInfo{ addDescription(config,inputFrame); initialConfig=expandedValues[0]; } + if (config.type === 'string'){ + let ip=addEl('input','t'+config.type,inputFrame); + addDescription(config,inputFrame); + ip.value=current?current:""; + ip.addEventListener('change',(ev)=>{ + let value=ev.target.value; + let modified=false; + if (config.max){ + if (value && value.length > config.max){ + modified=true; + value=value.substring(0,config.max); + } + } + if (config.allowed){ + let check=new RegExp("[^"+config.allowed+"]","g"); + let nv=value.replace(check,""); + if (nv != value){ + modified=true; + value=nv; + } + } + if (modified){ + ev.target.value=value; + } + callback(Object.assign({},config,{key: value,value:value}),false); + + }); + } let childFrame=addEl('div','childFrame',frame); if (initialConfig !== undefined){ callback(initialConfig,true,childFrame); From 6da87e4455a75dadc1dc11eebe0e8ec34e3d4513 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Tue, 30 Sep 2025 20:03:45 +0200 Subject: [PATCH 43/48] add buildname to ci output file names, correctly set initial build name --- webinstall/cibuild.js | 53 +++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/webinstall/cibuild.js b/webinstall/cibuild.js index b64787c..285bacc 100644 --- a/webinstall/cibuild.js +++ b/webinstall/cibuild.js @@ -167,8 +167,17 @@ class PipelineInfo{ updateStatus(); if (gitSha !== undefined) param.tag=gitSha; param.config=JSON.stringify(config); + let buildname=config['root:buildname'] + if (buildname){ + param.suffix="-"+buildname + } if (buildVersion !== undefined){ - param.suffix="-"+buildVersion; + if (param.suffix){ + param.suffix+="-"+buildVersion; + } + else{ + param.suffix="-"+buildVersion; + } } fetchJson(API,Object.assign({ api:'start'},param)) @@ -528,28 +537,32 @@ class PipelineInfo{ if (config.type === 'string'){ let ip=addEl('input','t'+config.type,inputFrame); addDescription(config,inputFrame); - ip.value=current?current:""; + const buildChild=(value)=>{ + if (value) { + if (config.max) { + if (value && value.length > config.max) { + value = value.substring(0, config.max); + } + } + if (config.allowed) { + let check = new RegExp("[^" + config.allowed + "]", "g"); + let nv = value.replace(check, ""); + if (nv != value) { + value = nv; + } + } + } + return Object.assign({},config,{key: value,value:value}); + } + initialConfig=buildChild(current); + ip.value=initialConfig.value||""; ip.addEventListener('change',(ev)=>{ let value=ev.target.value; - let modified=false; - if (config.max){ - if (value && value.length > config.max){ - modified=true; - value=value.substring(0,config.max); - } + let cbv=buildChild(value); + if (cbv.value != value){ + ev.target.value=cbv.value; } - if (config.allowed){ - let check=new RegExp("[^"+config.allowed+"]","g"); - let nv=value.replace(check,""); - if (nv != value){ - modified=true; - value=nv; - } - } - if (modified){ - ev.target.value=value; - } - callback(Object.assign({},config,{key: value,value:value}),false); + callback(cbv,false); }); } From 9211b13dcd013d89595d568fc54573388396664d Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Tue, 30 Sep 2025 20:09:13 +0200 Subject: [PATCH 44/48] #113: add env4 to cibuild --- webinstall/build.yaml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/webinstall/build.yaml b/webinstall/build.yaml index 672c435..0234010 100644 --- a/webinstall/build.yaml +++ b/webinstall/build.yaml @@ -53,16 +53,16 @@ types: - value: M5_ENV3#grv# key: true resource: qmp69881#grv#1,sht3x#grv#1 -# - label: "M5 ENV4" -# type: checkbox -# key: m5env4#grv# -# target: define -# url: "https://docs.m5stack.com/en/unit/ENV%E2%85%A3%20Unit" -# description: "M5 sensor module temperature, humidity, pressure" -# values: -# - value: M5_ENV4#grv# -# key: true -# resource: bmp280#grv#1,sht3x#grv#1 + - label: "M5 ENV4" + type: checkbox + key: m5env4#grv# + target: define + url: "https://docs.m5stack.com/en/unit/ENV%E2%85%A3%20Unit" + description: "M5 sensor module temperature, humidity, pressure" + values: + - value: M5_ENV4#grv# + key: true + resource: bmp280#grv#1,sht4x#grv#1 - type: checkbox label: SHT3X-1 description: "SHT30 temperature and humidity sensor 0x44" From fb62e41bd98302141e2492c5d7da33d3eeb8f3da Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Tue, 7 Oct 2025 12:58:28 +0200 Subject: [PATCH 45/48] prepare relase 20251007 --- Readme.md | 13 +++++++++++++ doc/Conversions.odt | Bin 26253 -> 36415 bytes doc/Conversions.pdf | Bin 38386 -> 152289 bytes lib/exampletask/Readme.md | 2 +- 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index d3fae7f..b833857 100644 --- a/Readme.md +++ b/Readme.md @@ -174,6 +174,19 @@ For details refer to the [example description](lib/exampletask/Readme.md). Changelog --------- +[20251007](../../releases/tag/20251007) +********* +* add AIS Aton translations (PGN 129041 <-> Ais class 21) +* improved mapping of AIS transducer information (NMEA2000) to AIS channel and Talker on NMEA0183 +* [#114](../../issues/114) correctly translate AIS type 1/3 from PGN 129038 +* add support for a generic S3 build in the build UI +* [#117](../../issues/117) add support for a transmit enable pin for RS 485 conections (also in the build UI) +* [#116](../../issues/116) SDA and SCL are swapped in the build UI +* [#112](../../issues/112) clearify licenses +* [#110](../../issues/110) / [#115](../../pull/115) support for the M5 GPS unit v1.1 +* [#102](../../issues/102) optimize Wifi reconnect handling +* [#111](../../pull/111) allow for a custom python build script + [20250305](../../releases/tag/20250305) ********* * better handling for reconnect to a raspberry pi after reset [#102](../../issues/102) diff --git a/doc/Conversions.odt b/doc/Conversions.odt index a44672b0fb4ff6cb0e502ef7d9ca216e493636a9..1a181ac6c931a1ce8d8631ca872315ef45c5bae9 100644 GIT binary patch delta 35065 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 RA%T#N$q8xdY)3jlVgQ_O28RFu literal 26253 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 diff --git a/doc/Conversions.pdf b/doc/Conversions.pdf index 3ed26e12f99dc9cde4f31bbe9916a6eb3ae92375..0361c69f5f609bb3fcb2422aa77c955eb25d7368 100644 GIT binary patch literal 152289 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 z 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 delta 11753 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 diff --git a/lib/exampletask/Readme.md b/lib/exampletask/Readme.md index 7c701fb..3220242 100644 --- a/lib/exampletask/Readme.md +++ b/lib/exampletask/Readme.md @@ -58,7 +58,7 @@ Files Starting from Version 20250305 you should normally not use this file name any more as those styles would be added for all build environments. Instead define a parameter _custom_css_ in your [platformio.ini](platformio.ini) for the environments you would like to add some styles for. This parameter accepts a list of file names (relative to the project root, separated by , or as multi line entry) * [script.py](script.py)
- Starting from version 202509xx you can define a parameter "custom_script" in your [platformio.ini](platformio.ini). + Starting from version 20251007 you can define a parameter "custom_script" in your [platformio.ini](platformio.ini). This parameter can contain a list of file names (relative to the project root) that will be added as a [platformio extra script](https://docs.platformio.org/en/latest/scripting/index.html#scripting). The scripts will be loaded at the end of the main [extra_script](../../extra_script.py). You can add code there that is specific for your build. Example: From 18b46ae5a0f5f2c5f148ffbb536808d7a86e43e2 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Tue, 7 Oct 2025 13:00:28 +0200 Subject: [PATCH 46/48] prepare relase 20251007 --- Readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Readme.md b/Readme.md index b833857..874e4d7 100644 --- a/Readme.md +++ b/Readme.md @@ -186,6 +186,7 @@ Changelog * [#110](../../issues/110) / [#115](../../pull/115) support for the M5 GPS unit v1.1 * [#102](../../issues/102) optimize Wifi reconnect handling * [#111](../../pull/111) allow for a custom python build script +* [#113](../../issues/113) support for M5 stack Env4 [20250305](../../releases/tag/20250305) ********* From 32099487fa4f3c27f8c4d2c8edfb3281d04b9aed Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Tue, 7 Oct 2025 13:02:19 +0200 Subject: [PATCH 47/48] prepare relase 20251007 --- Readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Readme.md b/Readme.md index 874e4d7..25eb111 100644 --- a/Readme.md +++ b/Readme.md @@ -178,6 +178,7 @@ Changelog ********* * add AIS Aton translations (PGN 129041 <-> Ais class 21) * improved mapping of AIS transducer information (NMEA2000) to AIS channel and Talker on NMEA0183 +* use a forked version of the NMEA2000 library (as an intermediate workaround) * [#114](../../issues/114) correctly translate AIS type 1/3 from PGN 129038 * add support for a generic S3 build in the build UI * [#117](../../issues/117) add support for a transmit enable pin for RS 485 conections (also in the build UI) From dd3a4f5093810267052197fb2b6eecce56fe6743 Mon Sep 17 00:00:00 2001 From: wellenvogel Date: Wed, 26 Nov 2025 18:02:41 +0100 Subject: [PATCH 48/48] fix a bug in the actisense reader, version 20251126 --- Readme.md | 4 ++++ platformio.ini | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 25eb111..5d2a20d 100644 --- a/Readme.md +++ b/Readme.md @@ -174,6 +174,10 @@ For details refer to the [example description](lib/exampletask/Readme.md). Changelog --------- +[20251126](../../releases/tag/20251126) +* fix a bug in the Actisense reader that could lead to an endless loop (making the device completely non responsive) +* upgrade to 4.24.1 of the NMEA2000 library (2025/11/01) - refer to the [changes](https://github.com/ttlappalainen/NMEA2000/blob/master/Documents/src/changes.md) - Especially UTF8 support +********* [20251007](../../releases/tag/20251007) ********* * add AIS Aton translations (PGN 129041 <-> Ais class 21) diff --git a/platformio.ini b/platformio.ini index 0025382..341083c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -18,7 +18,7 @@ extra_configs= [basedeps] lib_deps = - ttlappalainen_NMEA2000=https://github.com/wellenvogel/NMEA2000.git#20250926 + ttlappalainen_NMEA2000=https://github.com/wellenvogel/NMEA2000.git#20251126 ttlappalainen/NMEA0183 @ 1.10.1 ArduinoJson @ 6.18.5 AsyncTCP-esphome @ 2.0.1