From 82f5e179875e9cfd4bfa1a5fd9f50c496638fe32 Mon Sep 17 00:00:00 2001 From: andreas Date: Fri, 8 Nov 2024 21:00:25 +0100 Subject: [PATCH] intermediate: udp reader --- lib/api/GwApi.h | 1 + lib/channel/GwChannelList.cpp | 26 +++++ lib/channel/GwChannelList.h | 1 + lib/queue/GwBuffer.cpp | 12 ++- lib/queue/GwBuffer.h | 5 +- lib/socketserver/GwSocketHelper.h | 3 + lib/socketserver/GwUdpReader.cpp | 158 ++++++++++++++++++++++++++++++ lib/socketserver/GwUdpReader.h | 45 +++++++++ web/config.json | 143 ++++++++++++++++++++++++--- web/index.js | 36 +++++-- 10 files changed, 406 insertions(+), 24 deletions(-) create mode 100644 lib/socketserver/GwUdpReader.cpp create mode 100644 lib/socketserver/GwUdpReader.h diff --git a/lib/api/GwApi.h b/lib/api/GwApi.h index 00dbc1f..5a24e92 100644 --- a/lib/api/GwApi.h +++ b/lib/api/GwApi.h @@ -96,6 +96,7 @@ class GwApi{ unsigned long tcpSerRx=0; unsigned long tcpSerTx=0; unsigned long udpwTx=0; + unsigned long udprRx=0; int tcpClients=0; unsigned long tcpClRx=0; unsigned long tcpClTx=0; diff --git a/lib/channel/GwChannelList.cpp b/lib/channel/GwChannelList.cpp index b08bbfc..4371cef 100644 --- a/lib/channel/GwChannelList.cpp +++ b/lib/channel/GwChannelList.cpp @@ -7,6 +7,7 @@ #include "GwSerial.h" #include "GwTcpClient.h" #include "GwUdpWriter.h" +#include "GwUdpReader.h" class SerInit{ public: int serial=-1; @@ -260,8 +261,27 @@ static ChannelParam channelParameters[]={ .maxId=-1, .rxstatus=0, .txstatus=offsetof(GwApi::Status,GwApi::Status::udpwTx) + }, + { + .id=UDPR_CHANNEL_ID, + .baud="", + .receive=GwConfigDefinitions::udprEnabled, + .send="", + .direction="", + .toN2K=GwConfigDefinitions::udprToN2k, + .readF=GwConfigDefinitions::udprReadFilter, + .writeF="", + .preventLog="", + .readAct="", + .writeAct="", + .sendSeasmart="", + .name="UDPReader", + .maxId=-1, + .rxstatus=offsetof(GwApi::Status,GwApi::Status::udprRx), + .txstatus=0 } + }; template @@ -451,6 +471,12 @@ void GwChannelList::begin(bool fallbackSerial){ writer->begin(); addChannel(createChannel(logger,config,UDPW_CHANNEL_ID,writer)); } + //udp reader + if (config->getBool(GwConfigDefinitions::udprEnabled)){ + GwUdpReader *reader=new GwUdpReader(config,logger,UDPR_CHANNEL_ID); + reader->begin(); + addChannel(createChannel(logger,config,UDPR_CHANNEL_ID,reader)); + } logger->flush(); } String GwChannelList::getMode(int id){ diff --git a/lib/channel/GwChannelList.h b/lib/channel/GwChannelList.h index 956b786..f724716 100644 --- a/lib/channel/GwChannelList.h +++ b/lib/channel/GwChannelList.h @@ -19,6 +19,7 @@ #define TCP_CLIENT_CHANNEL_ID 4 #define MIN_TCP_CHANNEL_ID 5 #define UDPW_CHANNEL_ID 20 +#define UDPR_CHANNEL_ID 21 #define MIN_USER_TASK 200 class GwSocketServer; diff --git a/lib/queue/GwBuffer.cpp b/lib/queue/GwBuffer.cpp index 8e12934..de0f070 100644 --- a/lib/queue/GwBuffer.cpp +++ b/lib/queue/GwBuffer.cpp @@ -21,7 +21,7 @@ GwBuffer::~GwBuffer(){ } void GwBuffer::reset(String reason) { - LOG_DEBUG(GwLog::LOG,"reseting buffer %s, reason %s",this->name.c_str(),reason.c_str()); + if (! reason.isEmpty())LOG_DEBUG(GwLog::LOG,"reseting buffer %s, reason %s",this->name.c_str(),reason.c_str()); writePointer = buffer; readPointer = buffer; lp("reset"); @@ -33,6 +33,16 @@ size_t GwBuffer::freeSpace() } return readPointer - writePointer - 1; } +size_t GwBuffer::continousSpace() const{ + if (readPointer <= writePointer){ + return bufferSize-offset(writePointer); + } + return readPointer-writePointer-1; +} +void GwBuffer::moveWp(size_t offset){ + if (offset > continousSpace()) return; + writePointer+=offset; +} size_t GwBuffer::usedSpace() { if (readPointer <= writePointer) diff --git a/lib/queue/GwBuffer.h b/lib/queue/GwBuffer.h index 17490ec..aa67e04 100644 --- a/lib/queue/GwBuffer.h +++ b/lib/queue/GwBuffer.h @@ -33,7 +33,7 @@ class GwBuffer{ uint8_t *buffer; uint8_t *writePointer; uint8_t *readPointer; - size_t offset(uint8_t* ptr){ + size_t offset(uint8_t* ptr) const{ return (size_t)(ptr-buffer); } GwLog *logger; @@ -54,6 +54,9 @@ class GwBuffer{ * find the first occurance of x in the buffer, -1 if not found */ int findChar(char x); + uint8_t *getWp(){return writePointer;} + size_t continousSpace() const; //free space from wp + void moveWp(size_t offset); //move the wp forward }; #endif \ No newline at end of file diff --git a/lib/socketserver/GwSocketHelper.h b/lib/socketserver/GwSocketHelper.h index 67ba3a6..f153d45 100644 --- a/lib/socketserver/GwSocketHelper.h +++ b/lib/socketserver/GwSocketHelper.h @@ -22,4 +22,7 @@ class GwSocketHelper{ if (inet_pton(AF_INET,addr.c_str(),&iaddr) != 1) return false; return IN_MULTICAST(ntohl(iaddr.s_addr)); } + static bool equals(const in_addr &left, const in_addr &right){ + return left.s_addr == right.s_addr; + } }; \ No newline at end of file diff --git a/lib/socketserver/GwUdpReader.cpp b/lib/socketserver/GwUdpReader.cpp new file mode 100644 index 0000000..e8b619e --- /dev/null +++ b/lib/socketserver/GwUdpReader.cpp @@ -0,0 +1,158 @@ +#include "GwUdpReader.h" +#include +#include +#include "GwBuffer.h" +#include "GwSocketConnection.h" +#include "GwSocketHelper.h" +#include "GWWifi.h" + + +GwUdpReader::GwUdpReader(const GwConfigHandler *config, GwLog *logger, int minId) +{ + this->config = config; + this->logger = logger; + this->minId = minId; + port=config->getInt(GwConfigDefinitions::udprPort); + buffer= new GwBuffer(logger,GwBuffer::RX_BUFFER_SIZE,"udprd"); +} + +void GwUdpReader::createAndBind(){ + if (fd >= 0){ + ::close(fd); + } + if (currentStationIp.isEmpty() && (type == T_STA || type == T_MCSTA)) return; + fd=socket(AF_INET,SOCK_DGRAM,IPPROTO_IP); + if (fd < 0){ + LOG_ERROR("UDPR: unable to create udp socket: %d",errno); + return; + } + int enable = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); + if (type == T_STA) + { + if (inet_pton(AF_INET, currentStationIp.c_str(), &listenA.sin_addr) != 1) + { + LOG_ERROR("UDPR: invalid station ip address %s", currentStationIp.c_str()); + close(fd); + fd = -1; + return; + } + } + if (bind(fd,(struct sockaddr *)&listenA,sizeof(listenA)) < 0){ + LOG_ERROR("UDPR: unable to bind: %d",errno); + close(fd); + fd=-1; + return; + } + LOG_INFO("UDPR: socket created and bound"); + if (type != T_MCALL && type != T_MCAP && type != T_MCSTA) { + return; + } + struct ip_mreq mc; + String mcAddr=config->getString(GwConfigDefinitions::udprMC); + if (inet_pton(AF_INET,mcAddr.c_str(),&mc.imr_multiaddr) != 1){ + LOG_ERROR("UDPR: invalid multicast addr %s",mcAddr.c_str()); + ::close(fd); + fd=-1; + return; + } + if (type == T_MCALL || type == T_MCAP){ + mc.imr_interface=apAddr; + int res=setsockopt(fd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mc,sizeof(mc)); + if (res != 0){ + LOG_ERROR("UDPR: unable to add MC membership for AP:%d",errno); + } + else{ + LOG_INFO("UDPR: membership for %s for AP",mcAddr.c_str()); + } + } + if (!currentStationIp.isEmpty() && (type == T_MCALL || type == T_MCSTA)) + { + mc.imr_interface = staAddr; + int res = setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mc, sizeof(mc)); + if (res != 0) + { + LOG_ERROR("UDPR: unable to add MC membership for STA:%d", errno); + } + else{ + LOG_INFO("UDPR: membership for %s for STA %s",mcAddr.c_str(),currentStationIp.c_str()); + } + } +} + +void GwUdpReader::begin() +{ + if (type != T_UNKNOWN) return; //already started + type=(UType)(config->getInt(GwConfigDefinitions::udprType)); + LOG_INFO("UDPR begin, mode=%d",(int)type); + port=config->getInt(GwConfigDefinitions::udprPort); + listenA.sin_family=AF_INET; + listenA.sin_port=htons(port); + if (type != T_STA){ + listenA.sin_addr.s_addr=htonl(INADDR_ANY); + } + String ap=WiFi.softAPIP().toString(); + if (inet_pton(AF_INET, ap.c_str(), &apAddr) != 1) + { + LOG_ERROR("UDPR: invalid ap ip address %s", ap.c_str()); + return; + } + String sta; + if (WiFi.isConnected()) sta=WiFi.localIP().toString(); + setStationAdd(sta); + createAndBind(); +} + +bool GwUdpReader::setStationAdd(const String &sta){ + if (sta == currentStationIp) return false; + currentStationIp=sta; + if (inet_pton(AF_INET, currentStationIp.c_str(), &staAddr) != 1){ + LOG_ERROR("UDPR: invalid station ip address %s", currentStationIp.c_str()); + return false; + } + LOG_INFO("UDPR: new station IP %s",currentStationIp.c_str()); + return true; +} +void GwUdpReader::loop(bool handleRead, bool handleWrite) +{ + if (handleRead){ + if (type == T_STA || type == T_MCALL || type == T_MCSTA){ + //only change anything if we considered the station IP + String nextStationIp; + if (WiFi.isConnected()){ + String nextStationIp=WiFi.localIP().toString(); + } + if (setStationAdd(nextStationIp)){ + LOG_INFO("UDPR: wifi client IP changed, restart"); + createAndBind(); + } + } + } + +} + +void GwUdpReader::readMessages(GwMessageFetcher *writer) +{ + if (fd < 0) return; + //we expect one NMEA message in one UDP packet + buffer->reset(); + struct sockaddr_in from; + socklen_t fromLen=sizeof(from); + ssize_t res=recvfrom(fd,buffer->getWp(),buffer->continousSpace(),MSG_DONTWAIT, + (struct sockaddr*)&from,&fromLen); + if (res <= 0) return; + if (GwSocketHelper::equals(from.sin_addr,apAddr)) return; + if (!currentStationIp.isEmpty() && (GwSocketHelper::equals(from.sin_addr,staAddr))) return; + buffer->moveWp(res); + LOG_DEBUG(GwLog::DEBUG,"UDPR: received %d bytes",res); + writer->handleBuffer(buffer); +} +size_t GwUdpReader::sendToClients(const char *buf, int source,bool partial) +{ + return 0; +} + + +GwUdpReader::~GwUdpReader() +{ +} \ No newline at end of file diff --git a/lib/socketserver/GwUdpReader.h b/lib/socketserver/GwUdpReader.h new file mode 100644 index 0000000..08c56bb --- /dev/null +++ b/lib/socketserver/GwUdpReader.h @@ -0,0 +1,45 @@ +#ifndef _GWUDPREADER_H +#define _GWUDPREADER_H +#include "GWConfig.h" +#include "GwLog.h" +#include "GwBuffer.h" +#include "GwChannelInterface.h" +#include +#include +#include + +class GwUdpReader: public GwChannelInterface{ + public: + using UType=enum{ + T_ALL=0, + T_AP=1, + T_STA=2, + T_MCALL=4, + T_MCAP=5, + T_MCSTA=6, + T_UNKNOWN=-1 + }; + private: + const GwConfigHandler *config; + GwLog *logger; + int minId; + int port; + int fd=-1; + struct sockaddr_in listenA; + String listenIp; + String currentStationIp; + struct in_addr apAddr; + struct in_addr staAddr; + UType type=T_UNKNOWN; + void createAndBind(); + bool setStationAdd(const String &sta); + GwBuffer *buffer=nullptr; + public: + GwUdpReader(const GwConfigHandler *config,GwLog *logger,int minId); + ~GwUdpReader(); + void begin(); + virtual void loop(bool handleRead=true,bool handleWrite=true); + virtual size_t sendToClients(const char *buf,int sourceId, bool partialWrite=false); + virtual void readMessages(GwMessageFetcher *writer); +}; +#endif \ No newline at end of file diff --git a/web/config.json b/web/config.json index 3e44a4b..0b75d27 100644 --- a/web/config.json +++ b/web/config.json @@ -691,6 +691,7 @@ "label": "TCP port", "type": "number", "default": "10110", + "check":"checkPort", "description": "the TCP port we listen on", "category": "TCP server" }, @@ -766,8 +767,12 @@ "label": "remote port", "type": "number", "default": "10110", + "check":"checkPort", "description": "the TCP port we connect to", - "category": "TCP client" + "category": "TCP client", + "condition":{ + "tclEnabled":"true" + } }, { "name": "remoteAddress", @@ -776,7 +781,10 @@ "default": "", "check": "checkIpAddress", "description": "the IP address we connect to in the form 192.168.1.2\nor an MDNS name like ESP32NMEA2K.local", - "category": "TCP client" + "category": "TCP client", + "condition":{ + "tclEnabled":"true" + } }, { "name": "sendTCL", @@ -784,7 +792,10 @@ "type": "boolean", "default": "true", "description": "send out NMEA data to remote TCP server", - "category": "TCP client" + "category": "TCP client", + "condition":{ + "tclEnabled":"true" + } }, { "name": "readTCL", @@ -792,7 +803,10 @@ "type": "boolean", "default": "true", "description": "receive NMEA data from remote TCP server", - "category": "TCP client" + "category": "TCP client", + "condition":{ + "tclEnabled":"true" + } }, { "name": "tclToN2k", @@ -800,7 +814,10 @@ "type": "boolean", "default": "true", "description": "convert NMEA0183 from remote TCP server to NMEA2000", - "category": "TCP client" + "category": "TCP client", + "condition":{ + "tclEnabled":"true" + } }, { "name": "tclReadFilter", @@ -808,7 +825,10 @@ "type": "filter", "default": "", "description": "filter for NMEA0183 data when reading from remote TCP server\nselect aison|aisoff, set a whitelist or a blacklist with NMEA sentences like RMC,RMB", - "category": "TCP client" + "category": "TCP client", + "condition":{ + "tclEnabled":"true" + } }, { "name": "tclWriteFilter", @@ -816,7 +836,10 @@ "type": "filter", "default": "", "description": "filter for NMEA0183 data when writing to remote TCP server\nselect aison|aisoff, set a whitelist or a blacklist with NMEA sentences like RMC,RMB", - "category": "TCP client" + "category": "TCP client", + "condition":{ + "tclEnabled":"true" + } }, { "name": "tclSeasmart", @@ -824,7 +847,10 @@ "type": "boolean", "default": "false", "description": "send NMEA2000 as seasmart to remote TCP server", - "category": "TCP client" + "category": "TCP client", + "condition":{ + "tclEnabled":"true" + } }, { "name": "udpwEnabled", @@ -840,7 +866,11 @@ "type": "number", "default": "10110", "description": "the UDP port we send to", - "category": "UDP writer" + "check":"checkPort", + "category": "UDP writer", + "condition":{ + "udpwEnabled":"true" + } }, { "name": "udpwType", @@ -857,7 +887,10 @@ {"l":"mc-ap","v":"5"}, {"l":"mc-cli","v":"6"} ], - "category": "UDP writer" + "category": "UDP writer", + "condition":{ + "udpwEnabled":"true" + } }, { "name": "udpwAddress", @@ -868,7 +901,8 @@ "description": "the IP address we connect to in the form 192.168.1.2", "category": "UDP writer", "condition":{ - "udpwType":["3"] + "udpwType":["3"], + "udpwEnabled":"true" } }, { @@ -880,7 +914,8 @@ "description": "the multicast address we send to 224.0.0.0...239.255.255.255", "category": "UDP writer", "condition":{ - "udpwType":["4","5","6"] + "udpwType":["4","5","6"], + "udpwEnabled":"true" } }, { @@ -889,7 +924,10 @@ "type": "filter", "default": "", "description": "filter for NMEA0183 data when writing to remote UDP server\nselect aison|aisoff, set a whitelist or a blacklist with NMEA sentences like RMC,RMB", - "category": "UDP writer" + "category": "UDP writer", + "condition":{ + "udpwEnabled":"true" + } }, { "name": "udpwSeasmart", @@ -897,7 +935,84 @@ "type": "boolean", "default": "false", "description": "send NMEA2000 as seasmart to remote UDP server", - "category": "UDP writer" + "category": "UDP writer", + "condition":{ + "udpwEnabled":"true" + } + }, + { + "name": "udprEnabled", + "label": "enable", + "type": "boolean", + "default": "false", + "description":"enable the UDP reader", + "category":"UDP reader" + }, + { + "name": "udprPort", + "label": "local port", + "type": "number", + "default": "10110", + "check":"checkPort", + "description": "the UDP port we listen on", + "category": "UDP reader", + "condition":{ + "udprEnabled":"true" + } + }, + { + "name": "udprType", + "label": "local address type", + "type": "list", + "default": "0", + "description": "to which networks/addresses do we listen\nall: listen on AP and wifi client network\nap: listen in access point network only\ncli: listen in wifi client network\nmc-all: receive multicast from AP and wifi client network\nmc-ap:receive multicast from AP network\nmc-cli: receive muticast wifi client network", + "list":[ + {"l":"all","v":"0"}, + {"l":"ap","v":"1"}, + {"l":"cli","v":"2"}, + {"l":"mc-all","v":"4"}, + {"l":"mc-ap","v":"5"}, + {"l":"mc-cli","v":"6"} + ], + "category": "UDP reader", + "condition":{ + "udprEnabled":"true" + } + }, + { + "name": "udprToN2k", + "label": "to NMEA2000", + "type": "boolean", + "default": "true", + "description": "convert NMEA0183 from UDP to NMEA2000", + "category": "UDP reader", + "condition":{ + "udprEnabled":"true" + } + }, + { + "name": "udprMC", + "label": "multicast address", + "type": "string", + "default": "224.0.0.1", + "check": "checkMCAddress", + "description": "the multicast address we listen on 224.0.0.0...239.255.255.255", + "category": "UDP reader", + "condition":{ + "udprType":["4","5","6"], + "udprEnabled":"true" + } + }, + { + "name": "udprReadFilter", + "label": "NMEA read Filter", + "type": "filter", + "default": "", + "description": "filter for NMEA0183 data when receiving\nselect aison|aisoff, set a whitelist or a blacklist with NMEA sentences like RMC,RMB", + "category": "UDP reader", + "condition":{ + "udprEnabled":"true" + } }, { "name": "wifiClient", diff --git a/web/index.js b/web/index.js index 337c21c..6962506 100644 --- a/web/index.js +++ b/web/index.js @@ -181,6 +181,12 @@ } } + checkers.checkPort=function(v,allValues,def){ + let parsed=parseInt(v); + if (isNaN(parsed)) return "must be a number"; + if (parsed <1 || parsed >= 65536) return "port must be in the range 1..65536"; + } + checkers.checkSystemName=function(v) { //2...32 characters for ssid let allowed = v.replace(/[^a-zA-Z0-9]*/g, ''); @@ -213,13 +219,20 @@ } checkers.checkIpAddress=function(v, allValues, def) { - if (allValues.tclEnabled != "true") return; if (!v) return "cannot be empty"; if (!v.match(/[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*/) && !v.match(/.*\.local/)) return "must be either in the form 192.168.1.1 or xxx.local"; } + checkers.checkMCAddress=function(v, allValues, def) { + if (!v) return "cannot be empty"; + if (!v.match(/[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*/)) + return "must be in the form 224.0.0.1"; + let parts=v.split("."); + let o1=parseInt(v[0]); + if (o1 < 224 || o1 > 239) return "mulicast address must be in the range 224.0.0.0 to 239.255.255.255" + } checkers.checkXDR=function(v, allValues) { if (!v) return; let parts = v.split(','); @@ -264,21 +277,22 @@ continue; } let check = v.getAttribute('data-check'); - if (check) { + if (check && conditionOk(name)) { + let cfgDef=getConfigDefition(name); let checkFunction=checkers[check]; if (typeof (checkFunction) === 'function') { if (! loggedChecks[check]){ loggedChecks[check]=true; //console.log("check:"+check); } - let res = checkFunction(v.value, allValues, getConfigDefition(name)); + let res = checkFunction(v.value, allValues, cfgDef); if (res) { let value = v.value; if (v.type === 'password') value = "******"; let label = v.getAttribute('data-label'); if (!label) label = v.getAttribute('name'); v.classList.add("error"); - alert("invalid config for " + label + "(" + value + "):\n" + res); + alert("invalid config for "+cfgDef.category+":" + label + "(" + value + "):\n" + res); return; } } @@ -472,10 +486,10 @@ if (!(condition instanceof Array)) condition = [condition]; return condition; } - function checkCondition(element) { - let name = element.getAttribute('name'); + + function conditionOk(name){ let condition = getConditions(name); - if (!condition) return; + if (!condition) return true; let visible = false; if (!condition instanceof Array) condition = [condition]; condition.forEach(function (cel) { @@ -493,7 +507,13 @@ } } if (lvis) visible = true; - }); + }); + return visible; + } + + function checkCondition(element) { + let name = element.getAttribute('name'); + let visible=conditionOk(name); let row = closestParent(element, 'row'); if (!row) return; if (visible) row.classList.remove('hidden');