Merge branch 'wellenvogel:master' into master

This commit is contained in:
norbert-walter 2022-01-06 12:01:22 +01:00 committed by GitHub
commit 09e4a03981
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1955 additions and 881 deletions

View File

@ -54,19 +54,22 @@ The flash command must be (example for m5stack-atom):
esptool.py --port XXXX --chip esp32 write_flash 0x1000 m5stack-atom-20211217-all.bin esptool.py --port XXXX --chip esp32 write_flash 0x1000 m5stack-atom-20211217-all.bin
``` ```
For the meaning of the board names have a look at [Hardware](doc/Hardware.md). For details refer to the code in [platformio.ini](platformio.ini) and look for the hardware definitions in [GwHardware.h](lib/hardware/GwHardware.h). For the meaning of the board names have a look at [Hardware](doc/Hardware.md). For details refer to the code in [platformio.ini](platformio.ini) and look for the hardware definitions in [GwHardware.h](lib/hardware/GwHardware.h).
Additionally there is a small GUI for the esptool included here at [tools/flashtool.py](tools/flashtool.py) Additionally there is a small GUI for the esptool included here at [tools/flashtool/flashtool.py](tools/flashtool/flashtool.py)
__linux users__<br> __linux users__<br>
You can typically install the esptool (once you have python 3 installed) with You can typically install the esptool (once you have python 3 installed) with
``` ```
sudo pip install esptool sudo pip install esptool
``` ```
To use the flashtool just copy flashtool.py and esptool.py from [tools](tools) to an empty directory. To use the flashtool just download [flashtool.pyz](../../raw/master/tools/flashtool.pyz).
``` ```
sudo pip install tkinter sudo pip install tkinter
sudo pip install pyserial sudo pip install pyserial
``` ```
Afterwards run flashtool.py (potentially making it executable before). Afterwards run flashtool.pyz with
```
python3 flashtool.pyz
```
__windows users__<br> __windows users__<br>
You can find a prebuild executable in tools: [esptool.exe](tools/esptool.exe). You can find a prebuild executable in tools: [esptool.exe](tools/esptool.exe).
@ -76,11 +79,10 @@ After installing the driver check with your device manager for the com port that
Open a command prompt and change into the directory you downloaded the esptool.exe and the firmware binary. Open a command prompt and change into the directory you downloaded the esptool.exe and the firmware binary.
Flash with the command Flash with the command
``` ```
esptool.exe --port COM3 0x1000 xxxxx-xxxx-all.bin esptool.exe --port COM3 write_flash 0x1000 xxxxx-xxxx-all.bin
``` ```
Replace COM3 with the port shown in the device manager and the xxx with the name of the downloaded binary. Replace COM3 with the port shown in the device manager and the xxx with the name of the downloaded binary.
If you do not want to use the command line you can download the precompiled [flashtool.exe](../../raw/master/tools/flashtool.exe). If you do not want to use the command line you first need to install python3 from the [download page](https://www.python.org/downloads/windows/) - use the Windows 64 Bit installer. Install using the default settings. Afterwards download [flashtool.pyz](../../raw/master/tools/flashtool.pyz) and run it with a double click.
Just start the downloaded exe. Unfortunately some virus scanners seem to consider the exe a virus or trojan. There is not much I can do against this - the exe is simply build from flashtool.py - see [tools readme](tools/readme-esptool-win.txt).
Update Update

View File

@ -31,6 +31,52 @@ class GwApi{
return format; return format;
} }
}; };
class Status{
public:
bool wifiApOn=false;
bool wifiClientOn=false;
bool wifiClientConnected=false;
String wifiApIp;
String systemName; //is also AP SSID
String wifiApPass;
String wifiClientIp;
String wifiClientSSID;
unsigned long usbRx=0;
unsigned long usbTx=0;
unsigned long serRx=0;
unsigned long serTx=0;
unsigned long tcpSerRx=0;
unsigned long tcpSerTx=0;
int tcpClients=0;
unsigned long tcpClRx=0;
unsigned long tcpClTx=0;
bool tcpClientConnected=false;
unsigned long n2kRx=0;
unsigned long n2kTx=0;
void empty(){
wifiApOn=false;
wifiClientOn=false;
wifiClientConnected=false;
wifiApIp=String();
systemName=String(); //is also AP SSID
wifiApPass=String();
wifiClientIp=String();
wifiClientSSID=String();
usbRx=0;
usbTx=0;
serRx=0;
serTx=0;
tcpSerRx=0;
tcpSerTx=0;
tcpClients=0;
tcpClRx=0;
tcpClTx=0;
tcpClientConnected=false;
n2kRx=0;
n2kTx=0;
}
};
/** /**
* thread safe methods - can directly be called from a user task * thread safe methods - can directly be called from a user task
*/ */
@ -58,6 +104,11 @@ class GwApi{
* just make sure to have the list being of appropriate size (numValues) * just make sure to have the list being of appropriate size (numValues)
*/ */
virtual void getBoatDataValues(int numValues,BoatValue **list)=0; virtual void getBoatDataValues(int numValues,BoatValue **list)=0;
/**
* fill the status information
*/
virtual void getStatus(Status &status);
/** /**
* not thread safe methods * not thread safe methods
* accessing boat data must only be executed from within the main thread * accessing boat data must only be executed from within the main thread

219
lib/channel/GwChannel.cpp Normal file
View File

@ -0,0 +1,219 @@
#include "GwChannel.h"
#include <ActisenseReader.h>
class GwChannelMessageReceiver : public GwMessageFetcher{
static const int bufferSize=GwBuffer::RX_BUFFER_SIZE+4;
uint8_t buffer[bufferSize];
uint8_t *writePointer=buffer;
GwLog *logger;
GwChannel *channel;
GwChannel::NMEA0183Handler handler;
public:
GwChannelMessageReceiver(GwLog *logger,GwChannel *channel){
this->logger=logger;
this->channel=channel;
}
void setHandler(GwChannel::NMEA0183Handler handler){
this->handler=handler;
}
virtual bool handleBuffer(GwBuffer *gwbuffer){
size_t len=fetchMessageToBuffer(gwbuffer,buffer,bufferSize-4,'\n');
writePointer=buffer+len;
if (writePointer == buffer) return false;
uint8_t *p;
for (p=writePointer-1;p>=buffer && *p <= 0x20;p--){
*p=0;
}
if (p > buffer){
p++;
*p=0x0d;
p++;
*p=0x0a;
p++;
*p=0;
}
for (p=buffer; *p != 0 && p < writePointer && *p <= 0x20;p++){}
//very simple NMEA check
if (*p != '!' && *p != '$'){
LOG_DEBUG(GwLog::DEBUG,"unknown line [%d] - ignore: %s",id,(const char *)p);
}
else{
LOG_DEBUG(GwLog::DEBUG,"NMEA[%d]: %s",id,(const char *)p);
if (channel->canReceive((const char *)p)){
handler((const char *)p,id);
}
}
writePointer=buffer;
return true;
}
};
GwChannel::GwChannel(GwLog *logger,
String name,
int sourceId,
int maxSourceId){
this->logger = logger;
this->name=name;
this->sourceId=sourceId;
this->maxSourceId=sourceId;
this->countIn=new GwCounter<String>(String("count")+name+String("in"));
this->countOut=new GwCounter<String>(String("count")+name+String("out"));
this->impl=NULL;
this->receiver=new GwChannelMessageReceiver(logger,this);
this->actisenseReader=NULL;
}
void GwChannel::begin(
bool enabled,
bool nmeaOut,
bool nmeaIn,
String readFilter,
String writeFilter,
bool seaSmartOut,
bool toN2k,
bool readActisense,
bool writeActisense)
{
this->enabled = enabled;
this->NMEAout = nmeaOut;
this->NMEAin = nmeaIn;
this->readFilter=readFilter.isEmpty()?
NULL:
new GwNmeaFilter(readFilter);
this->writeFilter=writeFilter.isEmpty()?
NULL:
new GwNmeaFilter(writeFilter);
this->seaSmartOut=seaSmartOut;
this->toN2k=toN2k;
this->readActisense=readActisense;
this->writeActisense=writeActisense;
if (impl && readActisense){
channelStream=impl->getStream(false);
if (! channelStream) {
this->readActisense=false;
this->writeActisense=false;
LOG_DEBUG(GwLog::ERROR,"unable to read actisnse on %s",name.c_str());
}
else{
this->actisenseReader= new tActisenseReader();
actisenseReader->SetReadStream(channelStream);
}
}
}
void GwChannel::setImpl(GwChannelInterface *impl){
this->impl=impl;
}
void GwChannel::updateCounter(const char *msg, bool out)
{
char key[6];
if (msg[0] == '$')
{
strncpy(key, &msg[3], 3);
key[3] = 0;
}
else if (msg[0] == '!')
{
strncpy(key, &msg[1], 5);
key[5] = 0;
}
else{
return;
}
if (out){
countOut->add(key);
}
else{
countIn->add(key);
}
}
bool GwChannel::canSendOut(const char *buffer){
if (! enabled || ! impl) return false;
if (! NMEAout || readActisense) return false;
if (writeFilter && ! writeFilter->canPass(buffer)) return false;
return true;
}
bool GwChannel::canReceive(const char *buffer){
if (! enabled) return false;
if (! NMEAin) return false;
if (readFilter && ! readFilter->canPass(buffer)) return false;
updateCounter(buffer,false);
return true;
}
int GwChannel::getJsonSize(){
int rt=2;
if (countIn) rt+=countIn->getJsonSize();
if (countOut) rt+=countOut->getJsonSize();
return rt;
}
void GwChannel::toJson(GwJsonDocument &doc){
if (countOut) countOut->toJson(doc);
if (countIn) countIn->toJson(doc);
}
String GwChannel::toString(){
String rt="CH:"+name;
rt+=enabled?"[ena]":"[dis]";
rt+=NMEAin?"in,":"";
rt+=NMEAout?"out,":"";
rt+=String("RF:") + (readFilter?readFilter->toString():"[]");
rt+=String("WF:") + (writeFilter?writeFilter->toString():"[]");
rt+=String(",")+ (toN2k?"n2k":"");
rt+=String(",")+ (seaSmartOut?"SM":"");
rt+=String(",")+(readActisense?"AR":"");
rt+=String(",")+(writeActisense?"AW":"");
return rt;
}
void GwChannel::loop(bool handleRead, bool handleWrite){
if (! enabled || ! impl) return;
impl->loop(handleRead,handleWrite);
}
void GwChannel::readMessages(GwChannel::NMEA0183Handler handler){
if (! enabled || ! impl) return;
if (readActisense || ! NMEAin) return;
receiver->id=sourceId;
receiver->setHandler(handler);
impl->readMessages(receiver);
}
void GwChannel::sendToClients(const char *buffer, int sourceId){
if (! impl) return;
if (canSendOut(buffer)){
if(impl->sendToClients(buffer,sourceId)){
updateCounter(buffer,true);
}
}
}
void GwChannel::parseActisense(N2kHandler handler){
if (!enabled || ! impl || ! readActisense || ! actisenseReader) return;
tN2kMsg N2kMsg;
while (actisenseReader->GetMessageFromStream(N2kMsg)) {
countIn->add(String(N2kMsg.PGN));
handler(N2kMsg,sourceId);
}
}
void GwChannel::sendActisense(const tN2kMsg &msg, int sourceId){
if (!enabled || ! impl || ! writeActisense || ! channelStream) return;
//currently actisense only for channels with a single source id
//so we can check it here
if (isOwnSource(sourceId)) return;
countOut->add(String(msg.PGN));
msg.SendInActisenseFormat(channelStream);
}
bool GwChannel::isOwnSource(int id){
if (maxSourceId < 0) return id == sourceId;
else return (id >= sourceId && id <= maxSourceId);
}
unsigned long GwChannel::countRx(){
if (! countIn) return 0UL;
return countIn->getGlobal();
}
unsigned long GwChannel::countTx(){
if (! countOut) return 0UL;
return countOut->getGlobal();
}

77
lib/channel/GwChannel.h Normal file
View File

@ -0,0 +1,77 @@
#pragma once
#include "GwChannelInterface.h"
#include "GwConfigItem.h"
#include "GwLog.h"
#include "GWConfig.h"
#include "GwCounter.h"
#include "GwJsonDocument.h"
#include <N2kMsg.h>
#include <functional>
class GwChannelMessageReceiver;
class tActisenseReader;
class GwChannel{
bool enabled=false;
bool NMEAout=false;
bool NMEAin=false;
GwNmeaFilter* readFilter=NULL;
GwNmeaFilter* writeFilter=NULL;
bool seaSmartOut=false;
bool toN2k=false;
bool readActisense=false;
bool writeActisense=false;
GwLog *logger;
String name;
GwCounter<String> *countIn=NULL;
GwCounter<String> *countOut=NULL;
GwChannelInterface *impl;
int sourceId=0;
int maxSourceId=-1;
GwChannelMessageReceiver *receiver=NULL;
tActisenseReader *actisenseReader=NULL;
Stream *channelStream=NULL;
void updateCounter(const char *msg, bool out);
public:
GwChannel(
GwLog *logger,
String name,
int sourceId,
int maxSourceId=-1);
void begin(
bool enabled,
bool nmeaOut,
bool nmeaIn,
String readFilter,
String writeFilter,
bool seaSmartOut,
bool toN2k,
bool readActisense=false,
bool writeActisense=false
);
void setImpl(GwChannelInterface *impl);
bool isOwnSource(int id);
void enable(bool enabled){
this->enabled=enabled;
}
bool isEnabled(){return enabled;}
bool shouldRead(){return enabled && NMEAin;}
bool canSendOut(const char *buffer);
bool canReceive(const char *buffer);
bool sendSeaSmart(){ return seaSmartOut;}
bool sendToN2K(){return toN2k;}
int getJsonSize();
void toJson(GwJsonDocument &doc);
String toString();
void loop(bool handleRead, bool handleWrite);
typedef std::function<void(const char *buffer, int sourceid)> NMEA0183Handler;
void readMessages(NMEA0183Handler handler);
void sendToClients(const char *buffer, int sourceId);
typedef std::function<void(const tN2kMsg &msg, int sourceId)> N2kHandler ;
void parseActisense(N2kHandler handler);
void sendActisense(const tN2kMsg &msg, int sourceId);
unsigned long countRx();
unsigned long countTx();
};

View File

@ -0,0 +1,9 @@
#pragma once
#include "GwBuffer.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;}
};

View File

@ -0,0 +1,220 @@
#include "GwChannelList.h"
#include "GwApi.h"
#include "GwHardware.h"
#include "GwSocketServer.h"
#include "GwSerial.h"
#include "GwTcpClient.h"
class GwSerialLog : public GwLogWriter{
static const size_t bufferSize=4096;
char *logBuffer=NULL;
int wp=0;
GwSerial *writer;
public:
GwSerialLog(GwSerial *writer){
this->writer=writer;
logBuffer=new char[bufferSize];
wp=0;
}
virtual ~GwSerialLog(){}
virtual void write(const char *data){
int len=strlen(data);
if ((wp+len) >= (bufferSize-1)) return;
strncpy(logBuffer+wp,data,len);
wp+=len;
logBuffer[wp]=0;
}
virtual void flush(){
size_t handled=0;
while (handled < wp){
writer->flush();
size_t rt=writer->sendToClients(logBuffer+handled,-1,true);
handled+=rt;
}
wp=0;
logBuffer[0]=0;
}
};
GwChannelList::GwChannelList(GwLog *logger, GwConfigHandler *config){
this->logger=logger;
this->config=config;
}
void GwChannelList::allChannels(ChannelAction action){
for (auto it=theChannels.begin();it != theChannels.end();it++){
action(*it);
}
}
void GwChannelList::begin(bool fallbackSerial){
LOG_DEBUG(GwLog::DEBUG,"GwChannelList::begin");
GwChannel *channel=NULL;
//usb
if (! fallbackSerial){
GwSerial *usb=new GwSerial(NULL,0,USB_CHANNEL_ID);
usb->setup(config->getInt(config->usbBaud),3,1);
logger->setWriter(new GwSerialLog(usb));
logger->prefix="GWSERIAL:";
channel=new GwChannel(logger,"USB",USB_CHANNEL_ID);
channel->setImpl(usb);
channel->begin(true,
config->getBool(config->sendUsb),
config->getBool(config->receiveUsb),
config->getString(config->usbReadFilter),
config->getString(config->usbWriteFilter),
false,
config->getBool(config->usbToN2k),
config->getBool(config->usbActisense),
config->getBool(config->usbActSend)
);
theChannels.push_back(channel);
LOG_DEBUG(GwLog::LOG,"%s",channel->toString().c_str());
}
//TCP server
sockets=new GwSocketServer(config,logger,MIN_TCP_CHANNEL_ID);
sockets->begin();
channel=new GwChannel(logger,"TCP",MIN_TCP_CHANNEL_ID,MIN_TCP_CHANNEL_ID+10);
channel->setImpl(sockets);
channel->begin(
true,
config->getBool(config->sendTCP),
config->getBool(config->readTCP),
config->getString(config->tcpReadFilter),
config->getString(config->tcpWriteFilter),
config->getBool(config->sendSeasmart),
config->getBool(config->tcpToN2k),
false,
false
);
LOG_DEBUG(GwLog::LOG,"%s",channel->toString().c_str());
theChannels.push_back(channel);
//serial 1
bool serCanRead=false;
bool serCanWrite=false;
int serialrx=-1;
int serialtx=-1;
#ifdef GWSERIAL_MODE
#ifdef GWSERIAL_TX
serialtx=GWSERIAL_TX;
#endif
#ifdef GWSERIAL_RX
serialrx=GWSERIAL_RX;
#endif
if (serialrx != -1 && serialtx != -1){
serialMode=GWSERIAL_MODE;
}
#endif
//the serial direction is from the config (only valid for mode UNI)
String serialDirection=config->getString(config->serialDirection);
//we only consider the direction if mode is UNI
if (serialMode != String("UNI")){
serialDirection=String("");
//if mode is UNI it depends on the selection
serCanRead=config->getBool(config->receiveSerial);
serCanWrite=config->getBool(config->sendSerial);
}
if (serialDirection == "receive" || serialDirection == "off" || serialMode == "RX") serCanWrite=false;
if (serialDirection == "send" || serialDirection == "off" || serialMode == "TX") serCanRead=false;
LOG_DEBUG(GwLog::DEBUG,"serial set up: mode=%s,direction=%s,rx=%d,tx=%d",
serialMode.c_str(),serialDirection.c_str(),serialrx,serialtx
);
if (serialtx != -1 || serialrx != -1 ){
LOG_DEBUG(GwLog::LOG,"creating serial interface rx=%d, tx=%d",serialrx,serialtx);
GwSerial *serial=new GwSerial(logger,1,SERIAL1_CHANNEL_ID,serCanRead);
int rt=serial->setup(config->getInt(config->serialBaud,115200),serialrx,serialtx);
LOG_DEBUG(GwLog::LOG,"starting serial returns %d",rt);
channel=new GwChannel(logger,"SER",SERIAL1_CHANNEL_ID);
channel->setImpl(serial);
channel->begin(
serCanRead || serCanWrite,
serCanWrite,
serCanRead,
config->getString(config->serialReadF),
config->getString(config->serialWriteF),
false,
config->getBool(config->serialToN2k),
false,
false
);
LOG_DEBUG(GwLog::LOG,"%s",channel->toString().c_str());
theChannels.push_back(channel);
}
//tcp client
bool tclEnabled=config->getBool(config->tclEnabled);
channel=new GwChannel(logger,"TCPClient",TCP_CLIENT_CHANNEL_ID);
if (tclEnabled){
client=new GwTcpClient(logger);
client->begin(TCP_CLIENT_CHANNEL_ID,
config->getString(config->remoteAddress),
config->getInt(config->remotePort),
config->getBool(config->readTCL)
);
channel->setImpl(client);
}
channel->begin(
tclEnabled,
config->getBool(config->sendTCL),
config->getBool(config->readTCL),
config->getString(config->tclReadFilter),
config->getString(config->tclReadFilter),
config->getBool(config->tclSeasmart),
config->getBool(config->tclToN2k),
false,
false
);
theChannels.push_back(channel);
LOG_DEBUG(GwLog::LOG,"%s",channel->toString().c_str());
logger->flush();
}
int GwChannelList::getJsonSize(){
int rt=0;
allChannels([&](GwChannel *c){
rt+=c->getJsonSize();
});
return rt+20;
}
void GwChannelList::toJson(GwJsonDocument &doc){
if (sockets) doc["numClients"]=sockets->numClients();
if (client){
doc["clientCon"]=client->isConnected();
doc["clientErr"]=client->getError();
}
else{
doc["clientCon"]=false;
doc["clientErr"]="disabled";
}
allChannels([&](GwChannel *c){
c->toJson(doc);
});
}
GwChannel *GwChannelList::getChannelById(int sourceId){
for (auto it=theChannels.begin();it != theChannels.end();it++){
if ((*it)->isOwnSource(sourceId)) return *it;
}
return NULL;
}
void GwChannelList::fillStatus(GwApi::Status &status){
GwChannel *channel=getChannelById(USB_CHANNEL_ID);
if (channel){
status.usbRx=channel->countRx();
status.usbTx=channel->countTx();
}
channel=getChannelById(SERIAL1_CHANNEL_ID);
if (channel){
status.serRx=channel->countRx();
status.serTx=channel->countTx();
}
channel=getChannelById(MIN_TCP_CHANNEL_ID);
if (channel){
status.tcpSerRx=channel->countRx();
status.tcpSerTx=channel->countTx();
}
channel=getChannelById(TCP_CLIENT_CHANNEL_ID);
if (channel){
status.tcpClRx=channel->countRx();
status.tcpClTx=channel->countTx();
}
}

View File

@ -0,0 +1,45 @@
#pragma once
#include <functional>
#include <vector>
#include <WString.h>
#include "GwChannel.h"
#include "GwLog.h"
#include "GWConfig.h"
#include "GwJsonDocument.h"
#include "GwApi.h"
//NMEA message channels
#define N2K_CHANNEL_ID 0
#define USB_CHANNEL_ID 1
#define SERIAL1_CHANNEL_ID 2
#define TCP_CLIENT_CHANNEL_ID 3
#define MIN_TCP_CHANNEL_ID 4
#define MIN_USER_TASK 200
class GwSocketServer;
class GwTcpClient;
class GwChannelList{
private:
GwLog *logger;
GwConfigHandler *config;
typedef std::vector<GwChannel *> ChannelList;
ChannelList theChannels;
GwSocketServer *sockets;
GwTcpClient *client;
String serialMode=F("NONE");
public:
GwChannelList(GwLog *logger, GwConfigHandler *config);
typedef std::function<void(GwChannel *)> ChannelAction;
void allChannels(ChannelAction action);
//initialize
void begin(bool fallbackSerial=false);
//status
int getJsonSize();
void toJson(GwJsonDocument &doc);
//single channel
GwChannel *getChannelById(int sourceId);
void fillStatus(GwApi::Status &status);
};

View File

@ -140,18 +140,21 @@ void GwNmeaFilter::parseFilter(){
// "0:1:RMB,RMC" // "0:1:RMB,RMC"
// 0: AIS off, 1:whitelist, list of sentences // 0: AIS off, 1:whitelist, list of sentences
if (isReady) return; if (isReady) return;
if (config.isEmpty()){
isReady=true;
return;
}
int found=0; int found=0;
int last=0; int last=0;
int index=0; int index=0;
String data=config->asString(); while ((found = config.indexOf(':',last)) >= 0){
while ((found = data.indexOf(':',last)) >= 0){ String tok=config.substring(last,found);
String tok=data.substring(last,found);
handleToken(tok,index); handleToken(tok,index);
last=found+1; last=found+1;
index++; index++;
} }
if (last < data.length()){ if (last < config.length()){
String tok=data.substring(last); String tok=config.substring(last);
handleToken(tok,index); handleToken(tok,index);
} }
isReady=true; isReady=true;

View File

@ -46,7 +46,7 @@ class GwConfigInterface{
class GwNmeaFilter{ class GwNmeaFilter{
private: private:
GwConfigInterface *config=NULL; String config;
bool isReady=false; bool isReady=false;
bool ais=true; bool ais=true;
bool blacklist=true; bool blacklist=true;
@ -54,7 +54,7 @@ class GwNmeaFilter{
void handleToken(String token, int index); void handleToken(String token, int index);
void parseFilter(); void parseFilter();
public: public:
GwNmeaFilter(GwConfigInterface *config){ GwNmeaFilter(String config){
this->config=config; this->config=config;
isReady=false; isReady=false;
} }

View File

@ -20,6 +20,7 @@ template<class T> class GwCounter{
globalFail=0; globalFail=0;
globalOk=0; globalOk=0;
} }
unsigned long getGlobal(){return globalOk;}
void add(T key){ void add(T key){
globalOk++; globalOk++;
auto it=okCounter.find(key); auto it=okCounter.find(key);

View File

@ -88,6 +88,7 @@ void exampleTask(GwApi *api){
GwApi::BoatValue *latitude=new GwApi::BoatValue(F("Latitude")); GwApi::BoatValue *latitude=new GwApi::BoatValue(F("Latitude"));
GwApi::BoatValue *testValue=new GwApi::BoatValue(boatItemName); GwApi::BoatValue *testValue=new GwApi::BoatValue(boatItemName);
GwApi::BoatValue *valueList[]={longitude,latitude,testValue}; GwApi::BoatValue *valueList[]={longitude,latitude,testValue};
GwApi::Status status;
while(true){ while(true){
delay(1000); delay(1000);
/* /*
@ -162,6 +163,29 @@ void exampleTask(GwApi *api){
LOG_DEBUG(GwLog::LOG,"%s now invalid",testValue->getName().c_str()); LOG_DEBUG(GwLog::LOG,"%s now invalid",testValue->getName().c_str());
} }
} }
api->getStatus(status);
#define B(v) (v?"true":"false")
LOG_DEBUG(GwLog::LOG,"ST1:ap=%s,wc=%s,cc=%s",
B(status.wifiApOn),
B(status.wifiClientOn),
B(status.wifiClientConnected));
LOG_DEBUG(GwLog::LOG,"ST2:sn=%s,ai=%s,ap=%s,cs=%s,ci=%s",
status.systemName.c_str(),
status.wifiApIp.c_str(),
status.wifiApPass.c_str(),
status.wifiClientSSID.c_str(),
status.wifiClientIp.c_str());
LOG_DEBUG(GwLog::LOG,"ST3:ur=%ld,ut=%ld,sr=%ld,st=%ld,tr=%ld,tt=%ld,cr=%ld,ct=%ld,2r=%ld,2t=%ld",
status.usbRx,
status.usbTx,
status.serRx,
status.serTx,
status.tcpSerRx,
status.tcpSerTx,
status.tcpClRx,
status.tcpClTx,
status.n2kRx,
status.n2kTx);
} }
vTaskDelete(NULL); vTaskDelete(NULL);

View File

@ -29,11 +29,13 @@ uint16_t DaysSince1970 = 0;
class MyAisDecoder : public AIS::AisDecoder class MyAisDecoder : public AIS::AisDecoder
{ {
public:
int sourceId=-1;
private: private:
NMEA0183DataToN2K::N2kSender sender; NMEA0183DataToN2K::N2kSender sender;
GwLog *logger; GwLog *logger;
void send(const tN2kMsg &msg){ void send(const tN2kMsg &msg){
(*sender)(msg); (*sender)(msg,sourceId);
} }
AIS::DefaultSentenceParser parser; AIS::DefaultSentenceParser parser;
public: public:

View File

@ -97,26 +97,26 @@ private:
waypointMap[wpName]=newWp; waypointMap[wpName]=newWp;
return newWp.id; return newWp.id;
} }
bool send(tN2kMsg &msg,String key,unsigned long minDiff){ bool send(tN2kMsg &msg,String key,unsigned long minDiff,int sourceId){
unsigned long now=millis(); unsigned long now=millis();
unsigned long pgn=msg.PGN; unsigned long pgn=msg.PGN;
if (key == "") key=String(msg.PGN); if (key == "") key=String(msg.PGN);
auto it=lastSends.find(key); auto it=lastSends.find(key);
if (it == lastSends.end()){ if (it == lastSends.end()){
lastSends[key]=now; lastSends[key]=now;
sender(msg); sender(msg,sourceId);
return true; return true;
} }
if ((it->second + minDiff) <= now){ if ((it->second + minDiff) <= now){
lastSends[key]=now; lastSends[key]=now;
sender(msg); sender(msg,sourceId);
return true; return true;
} }
LOG_DEBUG(GwLog::DEBUG+1,"skipped n2k message %d",msg.PGN); LOG_DEBUG(GwLog::DEBUG+1,"skipped n2k message %d",msg.PGN);
return false; return false;
} }
bool send(tN2kMsg &msg, String key=""){ bool send(tN2kMsg &msg, int sourceId,String key=""){
send(msg,key,minSendInterval); return send(msg,key,minSendInterval,sourceId);
} }
bool updateDouble(GwBoatItem<double> *target,double v, int sourceId){ bool updateDouble(GwBoatItem<double> *target,double v, int sourceId){
if (v != NMEA0183DoubleNA){ if (v != NMEA0183DoubleNA){
@ -255,7 +255,7 @@ private:
(tN2kFluidType)(current.selector()), (tN2kFluidType)(current.selector()),
fields[0], fields[0],
fields[1]); fields[1]);
send(n2kMsg, buildN2KKey(n2kMsg, current.mapping)); send(n2kMsg,msg.sourceId, buildN2KKey(n2kMsg, current.mapping));
} }
break; break;
case XDRBAT: case XDRBAT:
@ -263,7 +263,7 @@ private:
{ {
SetN2kPGN127508(n2kMsg, current.mapping.instanceId, SetN2kPGN127508(n2kMsg, current.mapping.instanceId,
fields[0], fields[1], fields[2]); fields[0], fields[1], fields[2]);
send(n2kMsg, buildN2KKey(n2kMsg, current.mapping)); send(n2kMsg,msg.sourceId, buildN2KKey(n2kMsg, current.mapping));
} }
break; break;
case XDRTEMP: case XDRTEMP:
@ -271,7 +271,7 @@ private:
SetN2kPGN130312(n2kMsg,1,current.mapping.instanceId, SetN2kPGN130312(n2kMsg,1,current.mapping.instanceId,
(tN2kTempSource)(current.selector()), (tN2kTempSource)(current.selector()),
fields[0],fields[1]); fields[0],fields[1]);
send(n2kMsg,buildN2KKey(n2kMsg,current.mapping)); send(n2kMsg,msg.sourceId,buildN2KKey(n2kMsg,current.mapping));
} }
break; break;
case XDRHUMIDITY: case XDRHUMIDITY:
@ -281,7 +281,7 @@ private:
fields[0], fields[0],
fields[1] fields[1]
); );
send(n2kMsg,buildN2KKey(n2kMsg,current.mapping)); send(n2kMsg,msg.sourceId,buildN2KKey(n2kMsg,current.mapping));
} }
break; break;
case XDRPRESSURE: case XDRPRESSURE:
@ -289,7 +289,7 @@ private:
SetN2kPGN130314(n2kMsg,1,current.mapping.instanceId, SetN2kPGN130314(n2kMsg,1,current.mapping.instanceId,
(tN2kPressureSource)(current.selector()), (tN2kPressureSource)(current.selector()),
fields[0]); fields[0]);
send(n2kMsg,buildN2KKey(n2kMsg,current.mapping)); send(n2kMsg,msg.sourceId,buildN2KKey(n2kMsg,current.mapping));
} }
break; break;
case XDRENGINE: case XDRENGINE:
@ -301,14 +301,14 @@ private:
fields[0], fields[1], fields[2], fields[3], fields[4], fields[0], fields[1], fields[2], fields[3], fields[4],
fields[5], fields[6], fields[7], fromDouble(fields[8]), fromDouble(fields[9]), fields[5], fields[6], fields[7], fromDouble(fields[8]), fromDouble(fields[9]),
tN2kEngineDiscreteStatus1(), tN2kEngineDiscreteStatus2()); tN2kEngineDiscreteStatus1(), tN2kEngineDiscreteStatus2());
send(n2kMsg, buildN2KKey(n2kMsg, current.mapping)); send(n2kMsg,msg.sourceId, buildN2KKey(n2kMsg, current.mapping));
} }
} }
else{ else{
if (fillFieldList(current, fields, 13,10)){ if (fillFieldList(current, fields, 13,10)){
SetN2kPGN127488(n2kMsg,current.mapping.instanceId, SetN2kPGN127488(n2kMsg,current.mapping.instanceId,
fields[10],fields[11],fromDouble(fields[12])); fields[10],fields[11],fromDouble(fields[12]));
send(n2kMsg, buildN2KKey(n2kMsg, current.mapping)); send(n2kMsg,msg.sourceId, buildN2KKey(n2kMsg, current.mapping));
} }
} }
break; break;
@ -334,7 +334,7 @@ private:
mode=xteMode(*modeChar); mode=xteMode(*modeChar);
} }
SetN2kXTE(n2kMsg,1,mode,false,rmb.xte); SetN2kXTE(n2kMsg,1,mode,false,rmb.xte);
send(n2kMsg); send(n2kMsg,msg.sourceId);
} }
uint8_t destinationId=getWaypointId(rmb.destID); uint8_t destinationId=getWaypointId(rmb.destID);
uint8_t sourceId=getWaypointId(rmb.originID); uint8_t sourceId=getWaypointId(rmb.originID);
@ -357,10 +357,10 @@ private:
rmb.longitude, rmb.longitude,
rmb.vmg rmb.vmg
); );
send(n2kMsg); send(n2kMsg,msg.sourceId);
SetN2kPGN129285(n2kMsg,sourceId,1,1,true,true,"default"); SetN2kPGN129285(n2kMsg,sourceId,1,1,true,true,"default");
AppendN2kPGN129285(n2kMsg,destinationId,rmb.destID,rmb.latitude,rmb.longitude); AppendN2kPGN129285(n2kMsg,destinationId,rmb.destID,rmb.latitude,rmb.longitude);
send(n2kMsg); send(n2kMsg,msg.sourceId);
} }
} }
void convertRMC(const SNMEA0183Msg &msg) void convertRMC(const SNMEA0183Msg &msg)
@ -382,25 +382,26 @@ private:
{ {
SetN2kSystemTime(n2kMsg, 1, GpsDate, GpsTime); SetN2kSystemTime(n2kMsg, 1, GpsDate, GpsTime);
send(n2kMsg); send(n2kMsg,msg.sourceId);
} }
if (UD(Latitude) && if (UD(Latitude) &&
UD(Longitude)){ UD(Longitude)){
SetN2kLatLonRapid(n2kMsg,Latitude,Longitude); SetN2kLatLonRapid(n2kMsg,Latitude,Longitude);
send(n2kMsg); send(n2kMsg,msg.sourceId);
} }
if (UD(COG) && UD(SOG)){ if (UD(COG) && UD(SOG)){
SetN2kCOGSOGRapid(n2kMsg,1,N2khr_true,COG,SOG); SetN2kCOGSOGRapid(n2kMsg,1,N2khr_true,COG,SOG);
send(n2kMsg); send(n2kMsg,msg.sourceId);
} }
if (UD(Variation)){ if (UD(Variation)){
SetN2kMagneticVariation(n2kMsg,1,N2kmagvar_Calc, SetN2kMagneticVariation(n2kMsg,1,N2kmagvar_Calc,
getUint32(boatData->GpsDate), Variation); getUint32(boatData->GpsDate), Variation);
send(n2kMsg); send(n2kMsg,msg.sourceId);
} }
} }
void convertAIVDX(const SNMEA0183Msg &msg){ void convertAIVDX(const SNMEA0183Msg &msg){
aisDecoder->sourceId=msg.sourceId;
aisDecoder->handleMessage(msg.line); aisDecoder->handleMessage(msg.line);
} }
void convertMWV(const SNMEA0183Msg &msg){ void convertMWV(const SNMEA0183Msg &msg){
@ -434,7 +435,7 @@ private:
} }
if (shouldSend){ if (shouldSend){
SetN2kWindSpeed(n2kMsg,1,WindSpeed,WindAngle,n2kRef); SetN2kWindSpeed(n2kMsg,1,WindSpeed,WindAngle,n2kRef);
send(n2kMsg,String(n2kMsg.PGN)+String((int)n2kRef)); send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((int)n2kRef));
} }
} }
void convertVWR(const SNMEA0183Msg &msg) void convertVWR(const SNMEA0183Msg &msg)
@ -475,7 +476,7 @@ private:
if (shouldSend) if (shouldSend)
{ {
SetN2kWindSpeed(n2kMsg, 1, WindSpeed, WindAngle, N2kWind_Apparent); SetN2kWindSpeed(n2kMsg, 1, WindSpeed, WindAngle, N2kWind_Apparent);
send(n2kMsg,String(n2kMsg.PGN)+String((int)N2kWind_Apparent)); send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((int)N2kWind_Apparent));
} }
} }
@ -519,11 +520,11 @@ private:
if (shouldSend) if (shouldSend)
{ {
SetN2kWindSpeed(n2kMsg, 1, WindSpeed, WindAngle, N2kWind_True_North); SetN2kWindSpeed(n2kMsg, 1, WindSpeed, WindAngle, N2kWind_True_North);
send(n2kMsg,String(n2kMsg.PGN)+String((int)N2kWind_True_North)); send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((int)N2kWind_True_North));
} }
if (WindAngleMagnetic != NMEA0183DoubleNA && shouldSend){ if (WindAngleMagnetic != NMEA0183DoubleNA && shouldSend){
SetN2kWindSpeed(n2kMsg, 1, WindSpeed, WindAngleMagnetic, N2kWind_Magnetic); SetN2kWindSpeed(n2kMsg, 1, WindSpeed, WindAngleMagnetic, N2kWind_Magnetic);
send(n2kMsg,String(n2kMsg.PGN)+String((int)N2kWind_Magnetic)); send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((int)N2kWind_Magnetic));
} }
} }
@ -540,7 +541,7 @@ private:
boatData->Variation->getDataWithDefault(N2kDoubleNA), boatData->Variation->getDataWithDefault(N2kDoubleNA),
boatData->Deviation->getDataWithDefault(N2kDoubleNA) boatData->Deviation->getDataWithDefault(N2kDoubleNA)
); );
send(n2kMsg); send(n2kMsg,msg.sourceId);
} }
void convertHDT(const SNMEA0183Msg &msg){ void convertHDT(const SNMEA0183Msg &msg){
@ -553,7 +554,7 @@ private:
if (! UD(Heading)) return; if (! UD(Heading)) return;
tN2kMsg n2kMsg; tN2kMsg n2kMsg;
SetN2kTrueHeading(n2kMsg,1,Heading); SetN2kTrueHeading(n2kMsg,1,Heading);
send(n2kMsg); send(n2kMsg,msg.sourceId);
} }
void convertHDG(const SNMEA0183Msg &msg){ void convertHDG(const SNMEA0183Msg &msg){
double MagneticHeading=NMEA0183DoubleNA; double MagneticHeading=NMEA0183DoubleNA;
@ -584,7 +585,7 @@ private:
UD(Deviation); UD(Deviation);
tN2kMsg n2kMsg; tN2kMsg n2kMsg;
SetN2kMagneticHeading(n2kMsg,1,MagneticHeading,Deviation,Variation); SetN2kMagneticHeading(n2kMsg,1,MagneticHeading,Deviation,Variation);
send(n2kMsg); send(n2kMsg,msg.sourceId);
} }
void convertDPT(const SNMEA0183Msg &msg){ void convertDPT(const SNMEA0183Msg &msg){
@ -612,7 +613,7 @@ private:
if (! boatData->DepthTransducer->update(DepthBelowTransducer)) return; if (! boatData->DepthTransducer->update(DepthBelowTransducer)) return;
tN2kMsg n2kMsg; tN2kMsg n2kMsg;
SetN2kWaterDepth(n2kMsg,1,DepthBelowTransducer,Offset); SetN2kWaterDepth(n2kMsg,1,DepthBelowTransducer,Offset);
send(n2kMsg,String(n2kMsg.PGN)+String((Offset != N2kDoubleNA)?1:0)); send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((Offset != N2kDoubleNA)?1:0));
} }
typedef enum { typedef enum {
DBS, DBS,
@ -647,7 +648,7 @@ private:
if (! boatData->DepthTransducer->update(Depth,msg.sourceId)) return; if (! boatData->DepthTransducer->update(Depth,msg.sourceId)) return;
tN2kMsg n2kMsg; tN2kMsg n2kMsg;
SetN2kWaterDepth(n2kMsg,1,Depth,N2kDoubleNA); SetN2kWaterDepth(n2kMsg,1,Depth,N2kDoubleNA);
send(n2kMsg,String(n2kMsg.PGN)+String(0)); send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String(0));
return; return;
} }
//we can only send if we have a valid depth beloww tranducer //we can only send if we have a valid depth beloww tranducer
@ -667,7 +668,7 @@ private:
} }
tN2kMsg n2kMsg; tN2kMsg n2kMsg;
SetN2kWaterDepth(n2kMsg,1,Depth,offset); SetN2kWaterDepth(n2kMsg,1,Depth,offset);
send(n2kMsg,String(n2kMsg.PGN)+String((offset != N2kDoubleNA)?1:0)); send(n2kMsg,msg.sourceId,(n2kMsg.PGN)+String((offset != N2kDoubleNA)?1:0));
} }
} }
} }
@ -694,7 +695,7 @@ private:
tN2kMsg n2kMsg; tN2kMsg n2kMsg;
if (! UD(RudderPosition)) return; if (! UD(RudderPosition)) return;
SetN2kRudder(n2kMsg,RudderPosition); SetN2kRudder(n2kMsg,RudderPosition);
send(n2kMsg); send(n2kMsg,msg.sourceId);
} }
} }
@ -711,7 +712,7 @@ private:
if (MagneticHeading == NMEA0183DoubleNA) MagneticHeading=N2kDoubleNA; if (MagneticHeading == NMEA0183DoubleNA) MagneticHeading=N2kDoubleNA;
tN2kMsg n2kMsg; tN2kMsg n2kMsg;
SetN2kBoatSpeed(n2kMsg,1,STW); SetN2kBoatSpeed(n2kMsg,1,STW);
send(n2kMsg); send(n2kMsg,msg.sourceId);
} }
void convertVTG(const SNMEA0183Msg &msg){ void convertVTG(const SNMEA0183Msg &msg){
@ -727,7 +728,7 @@ private:
tN2kMsg n2kMsg; tN2kMsg n2kMsg;
//TODO: maybe use MCOG if no COG? //TODO: maybe use MCOG if no COG?
SetN2kCOGSOGRapid(n2kMsg,1,N2khr_true,COG,SOG); SetN2kCOGSOGRapid(n2kMsg,1,N2khr_true,COG,SOG);
send(n2kMsg); send(n2kMsg,msg.sourceId);
} }
void convertZDA(const SNMEA0183Msg &msg){ void convertZDA(const SNMEA0183Msg &msg){
time_t DateTime; time_t DateTime;
@ -751,10 +752,10 @@ private:
tN2kMsg n2kMsg; tN2kMsg n2kMsg;
if (timezoneValid){ if (timezoneValid){
SetN2kLocalOffset(n2kMsg,DaysSince1970,GpsTime,Timezone); SetN2kLocalOffset(n2kMsg,DaysSince1970,GpsTime,Timezone);
send(n2kMsg); send(n2kMsg,msg.sourceId);
} }
SetN2kSystemTime(n2kMsg,1,DaysSince1970,GpsTime); SetN2kSystemTime(n2kMsg,1,DaysSince1970,GpsTime);
send(n2kMsg); send(n2kMsg,msg.sourceId);
} }
void convertGGA(const SNMEA0183Msg &msg){ void convertGGA(const SNMEA0183Msg &msg){
double GPSTime=NMEA0183DoubleNA; double GPSTime=NMEA0183DoubleNA;
@ -788,7 +789,7 @@ private:
SatelliteCount, HDOP, boatData->PDOP->getDataWithDefault(N2kDoubleNA), 0, SatelliteCount, HDOP, boatData->PDOP->getDataWithDefault(N2kDoubleNA), 0,
0, N2kGNSSt_GPS, DGPSReferenceStationID, 0, N2kGNSSt_GPS, DGPSReferenceStationID,
DGPSAge); DGPSAge);
send(n2kMsg); send(n2kMsg,msg.sourceId);
} }
void convertGSA(const SNMEA0183Msg &msg){ void convertGSA(const SNMEA0183Msg &msg){
if (msg.FieldCount() < 17) if (msg.FieldCount() < 17)
@ -819,7 +820,7 @@ private:
if (!updateDouble(boatData->VDOP,VDOP,msg.sourceId)) return; if (!updateDouble(boatData->VDOP,VDOP,msg.sourceId)) return;
} }
SetN2kGNSSDOPData(n2kMsg,1,rmode,mode,HDOP,VDOP,N2kDoubleNA); SetN2kGNSSDOPData(n2kMsg,1,rmode,mode,HDOP,VDOP,N2kDoubleNA);
send(n2kMsg); send(n2kMsg,msg.sourceId);
} }
void convertGSV(const SNMEA0183Msg &msg){ void convertGSV(const SNMEA0183Msg &msg){
if (msg.FieldCount() < 7){ if (msg.FieldCount() < 7){
@ -867,7 +868,7 @@ private:
} }
} }
if (hasInfos){ if (hasInfos){
send(n2kMsg); send(n2kMsg,msg.sourceId);
} }
} }
@ -885,7 +886,7 @@ private:
if (! updateDouble(boatData->GpsTime,GLL.GPSTime,msg.sourceId)) return; if (! updateDouble(boatData->GpsTime,GLL.GPSTime,msg.sourceId)) return;
tN2kMsg n2kMsg; tN2kMsg n2kMsg;
SetN2kLatLonRapid(n2kMsg,GLL.latitude,GLL.longitude); SetN2kLatLonRapid(n2kMsg,GLL.latitude,GLL.longitude);
send(n2kMsg); send(n2kMsg,msg.sourceId);
} }
void convertROT(const SNMEA0183Msg &msg){ void convertROT(const SNMEA0183Msg &msg){
@ -897,7 +898,7 @@ private:
if (! updateDouble(boatData->ROT,ROT,msg.sourceId)) return; if (! updateDouble(boatData->ROT,ROT,msg.sourceId)) return;
tN2kMsg n2kMsg; tN2kMsg n2kMsg;
SetN2kRateOfTurn(n2kMsg,1,ROT); SetN2kRateOfTurn(n2kMsg,1,ROT);
send(n2kMsg); send(n2kMsg,msg.sourceId);
} }
void convertXTE(const SNMEA0183Msg &msg){ void convertXTE(const SNMEA0183Msg &msg){
if (msg.FieldCount() < 6){ if (msg.FieldCount() < 6){
@ -916,7 +917,7 @@ private:
tN2kMsg n2kMsg; tN2kMsg n2kMsg;
tN2kXTEMode mode=xteMode(msg.Field(5)[0]); tN2kXTEMode mode=xteMode(msg.Field(5)[0]);
SetN2kXTE(n2kMsg,1,mode,false,xte); SetN2kXTE(n2kMsg,1,mode,false,xte);
send(n2kMsg); send(n2kMsg,msg.sourceId);
} }
//shortcut for lambda converters //shortcut for lambda converters

View File

@ -7,7 +7,7 @@
class NMEA0183DataToN2K{ class NMEA0183DataToN2K{
public: public:
typedef bool (*N2kSender)(const tN2kMsg &msg); typedef bool (*N2kSender)(const tN2kMsg &msg,int sourceId);
protected: protected:
GwLog * logger; GwLog * logger;
GwBoatData *boatData; GwBoatData *boatData;

View File

@ -34,12 +34,11 @@
N2kDataToNMEA0183::N2kDataToNMEA0183(GwLog * logger, GwBoatData *boatData, N2kDataToNMEA0183::N2kDataToNMEA0183(GwLog * logger, GwBoatData *boatData,
tSendNMEA0183MessageCallback callback, int id,String talkerId) SendNMEA0183MessageCallback callback, String talkerId)
{ {
this->SendNMEA0183MessageCallback=callback; this->sendNMEA0183MessageCallback=callback;
strncpy(this->talkerId,talkerId.c_str(),2); strncpy(this->talkerId,talkerId.c_str(),2);
this->talkerId[2]=0; this->talkerId[2]=0;
sourceId=id;
} }
@ -50,7 +49,7 @@ void N2kDataToNMEA0183::loop() {
//***************************************************************************** //*****************************************************************************
void N2kDataToNMEA0183::SendMessage(const tNMEA0183Msg &NMEA0183Msg) { void N2kDataToNMEA0183::SendMessage(const tNMEA0183Msg &NMEA0183Msg) {
if ( SendNMEA0183MessageCallback != 0 ) SendNMEA0183MessageCallback(NMEA0183Msg, sourceId); sendNMEA0183MessageCallback(NMEA0183Msg, sourceId);
} }
/** /**
@ -148,8 +147,9 @@ private:
virtual String handledKeys(){ virtual String handledKeys(){
return converters.handledKeys(); return converters.handledKeys();
} }
virtual void HandleMsg(const tN2kMsg &N2kMsg) virtual void HandleMsg(const tN2kMsg &N2kMsg, int sourceId)
{ {
this->sourceId=sourceId;
String key=String(N2kMsg.PGN); String key=String(N2kMsg.PGN);
bool rt=converters.handleMessage(key,N2kMsg,this); bool rt=converters.handleMessage(key,N2kMsg,this);
if (! rt){ if (! rt){
@ -1489,9 +1489,9 @@ private:
public: public:
N2kToNMEA0183Functions(GwLog *logger, GwBoatData *boatData, N2kToNMEA0183Functions(GwLog *logger, GwBoatData *boatData,
tSendNMEA0183MessageCallback callback, int sourceId, SendNMEA0183MessageCallback callback,
String talkerId, GwXDRMappings *xdrMappings, int minXdrInterval) String talkerId, GwXDRMappings *xdrMappings, int minXdrInterval)
: N2kDataToNMEA0183(logger, boatData, callback,sourceId,talkerId) : N2kDataToNMEA0183(logger, boatData, callback,talkerId)
{ {
LastPosSend = 0; LastPosSend = 0;
lastLoopTime = 0; lastLoopTime = 0;
@ -1516,9 +1516,9 @@ private:
N2kDataToNMEA0183* N2kDataToNMEA0183::create(GwLog *logger, GwBoatData *boatData, N2kDataToNMEA0183* N2kDataToNMEA0183::create(GwLog *logger, GwBoatData *boatData,
tSendNMEA0183MessageCallback callback, int sourceId,String talkerId, GwXDRMappings *xdrMappings, SendNMEA0183MessageCallback callback, String talkerId, GwXDRMappings *xdrMappings,
int minXdrInterval){ int minXdrInterval){
LOG_DEBUG(GwLog::LOG,"creating N2kToNMEA0183"); LOG_DEBUG(GwLog::LOG,"creating N2kToNMEA0183");
return new N2kToNMEA0183Functions(logger,boatData,callback, sourceId,talkerId,xdrMappings,minXdrInterval); return new N2kToNMEA0183Functions(logger,boatData,callback, talkerId,xdrMappings,minXdrInterval);
} }
//***************************************************************************** //*****************************************************************************

View File

@ -22,6 +22,7 @@ OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
#ifndef _N2KDATATONMEA0183_H #ifndef _N2KDATATONMEA0183_H
#define _N2KDATATONMEA0183_H #define _N2KDATATONMEA0183_H
#include <functional>
#include <NMEA0183.h> #include <NMEA0183.h>
#include <NMEA2000.h> #include <NMEA2000.h>
@ -34,22 +35,22 @@ class GwJsonDocument;
class N2kDataToNMEA0183 class N2kDataToNMEA0183
{ {
public: public:
using tSendNMEA0183MessageCallback = void (*)(const tNMEA0183Msg &NMEA0183Msg, int id); typedef std::function<void(const tNMEA0183Msg &NMEA0183Msg,int id)> SendNMEA0183MessageCallback;
protected: protected:
GwLog *logger; GwLog *logger;
GwBoatData *boatData; GwBoatData *boatData;
int sourceId; int sourceId=0;
char talkerId[3]; char talkerId[3];
tSendNMEA0183MessageCallback SendNMEA0183MessageCallback; SendNMEA0183MessageCallback sendNMEA0183MessageCallback;
void SendMessage(const tNMEA0183Msg &NMEA0183Msg); void SendMessage(const tNMEA0183Msg &NMEA0183Msg);
N2kDataToNMEA0183(GwLog *logger, GwBoatData *boatData, N2kDataToNMEA0183(GwLog *logger, GwBoatData *boatData,
tSendNMEA0183MessageCallback callback, int sourceId,String talkerId); SendNMEA0183MessageCallback callback, String talkerId);
public: public:
static N2kDataToNMEA0183* create(GwLog *logger, GwBoatData *boatData, tSendNMEA0183MessageCallback callback, static N2kDataToNMEA0183* create(GwLog *logger, GwBoatData *boatData, SendNMEA0183MessageCallback callback,
int sourceId,String talkerId, GwXDRMappings *xdrMappings,int minXdrInterval=100); String talkerId, GwXDRMappings *xdrMappings,int minXdrInterval=100);
virtual void HandleMsg(const tN2kMsg &N2kMsg) = 0; virtual void HandleMsg(const tN2kMsg &N2kMsg, int sourceId) = 0;
virtual void loop(); virtual void loop();
virtual ~N2kDataToNMEA0183(){} virtual ~N2kDataToNMEA0183(){}
virtual unsigned long* handledPgns()=0; virtual unsigned long* handledPgns()=0;

View File

@ -22,7 +22,7 @@ typedef size_t (*GwBufferHandleFunction)(uint8_t *buffer, size_t len, void *para
class GwBuffer{ class GwBuffer{
public: public:
static const size_t TX_BUFFER_SIZE=1620; // app. 20 NMEA messages static const size_t TX_BUFFER_SIZE=1620; // app. 20 NMEA messages
static const size_t RX_BUFFER_SIZE=400; // enough for 1 NMEA message or actisense message static const size_t RX_BUFFER_SIZE=600; // enough for 1 NMEA message or actisense message or seasmart message
typedef enum { typedef enum {
OK, OK,
ERROR, ERROR,

View File

@ -96,7 +96,7 @@ size_t GwSerial::sendToClients(const char *buf,int sourceId,bool partial){
} }
return enqueued; return enqueued;
} }
void GwSerial::loop(bool handleRead){ void GwSerial::loop(bool handleRead,bool handleWrite){
write(); write();
if (! isInitialized()) return; if (! isInitialized()) return;
if (! handleRead) return; if (! handleRead) return;
@ -116,10 +116,10 @@ void GwSerial::loop(bool handleRead){
serial->readBytes(buffer,available); serial->readBytes(buffer,available);
} }
} }
bool GwSerial::readMessages(GwMessageFetcher *writer){ void GwSerial::readMessages(GwMessageFetcher *writer){
if (! isInitialized()) return false; if (! isInitialized()) return;
if (! allowRead) return false; if (! allowRead) return;
return writer->handleBuffer(readBuffer); writer->handleBuffer(readBuffer);
} }
void GwSerial::flush(){ void GwSerial::flush(){

View File

@ -3,8 +3,9 @@
#include "HardwareSerial.h" #include "HardwareSerial.h"
#include "GwLog.h" #include "GwLog.h"
#include "GwBuffer.h" #include "GwBuffer.h"
#include "GwChannelInterface.h"
class GwSerialStream; class GwSerialStream;
class GwSerial{ class GwSerial : public GwChannelInterface{
private: private:
GwBuffer *buffer; GwBuffer *buffer;
GwBuffer *readBuffer=NULL; GwBuffer *readBuffer=NULL;
@ -23,11 +24,11 @@ class GwSerial{
~GwSerial(); ~GwSerial();
int setup(int baud,int rxpin,int txpin); int setup(int baud,int rxpin,int txpin);
bool isInitialized(); bool isInitialized();
size_t sendToClients(const char *buf,int sourceId,bool partial=false); virtual size_t sendToClients(const char *buf,int sourceId,bool partial=false);
void loop(bool handleRead=true); virtual void loop(bool handleRead=true,bool handleWrite=true);
bool readMessages(GwMessageFetcher *writer); virtual void readMessages(GwMessageFetcher *writer);
void flush(); void flush();
Stream *getStream(bool partialWrites); virtual Stream *getStream(bool partialWrites);
friend GwSerialStream; friend GwSerialStream;
}; };
#endif #endif

View File

@ -0,0 +1,214 @@
#include "GwSocketConnection.h"
IPAddress GwSocketConnection::remoteIP(int fd)
{
struct sockaddr_storage addr;
socklen_t len = sizeof addr;
getpeername(fd, (struct sockaddr *)&addr, &len);
struct sockaddr_in *s = (struct sockaddr_in *)&addr;
return IPAddress((uint32_t)(s->sin_addr.s_addr));
}
GwSocketConnection::GwSocketConnection(GwLog *logger, int id, bool allowRead)
{
this->logger = logger;
this->allowRead = allowRead;
String bufName = "Sock(";
bufName += String(id);
bufName += ")";
buffer = new GwBuffer(logger, GwBuffer::TX_BUFFER_SIZE, bufName + "wr");
if (allowRead)
{
readBuffer = new GwBuffer(logger, GwBuffer::RX_BUFFER_SIZE, bufName + "rd");
}
overflows = 0;
}
void GwSocketConnection::setClient(int fd)
{
this->fd = fd;
buffer->reset("new client");
if (readBuffer)
readBuffer->reset("new client");
overflows = 0;
pendingWrite = false;
writeError = false;
lastWrite = 0;
if (fd >= 0)
{
remoteIpAddress = remoteIP(fd).toString();
}
else
{
remoteIpAddress = String("---");
}
}
bool GwSocketConnection::hasClient()
{
return fd >= 0;
}
void GwSocketConnection::stop()
{
if (fd >= 0)
{
close(fd);
fd = -1;
}
}
GwSocketConnection::~GwSocketConnection()
{
delete buffer;
if (readBuffer)
delete readBuffer;
}
bool GwSocketConnection::connected()
{
if (fd >= 0)
{
uint8_t dummy;
int res = recv(fd, &dummy, 0, MSG_DONTWAIT);
// avoid unused var warning by gcc
(void)res;
// recv only sets errno if res is <= 0
if (res <= 0)
{
switch (errno)
{
case EWOULDBLOCK:
case ENOENT: //caused by vfs
return true;
break;
case ENOTCONN:
case EPIPE:
case ECONNRESET:
case ECONNREFUSED:
case ECONNABORTED:
return false;
break;
default:
return true;
}
}
else
{
return true;
}
}
return false;
}
bool GwSocketConnection::enqueue(uint8_t *data, size_t len)
{
if (len == 0)
return true;
size_t rt = buffer->addData(data, len);
if (rt < len)
{
LOG_DEBUG(GwLog::LOG, "overflow on %s", remoteIpAddress.c_str());
overflows++;
return false;
}
return true;
}
bool GwSocketConnection::hasData()
{
return buffer->usedSpace() > 0;
}
bool GwSocketConnection::handleError(int res, bool errorIf0)
{
if (res == 0 && errorIf0)
{
LOG_DEBUG(GwLog::LOG, "client shutdown (recv 0) on %s", remoteIpAddress.c_str());
stop();
return false;
}
if (res < 0)
{
if (errno != EAGAIN)
{
LOG_DEBUG(GwLog::LOG, "client read error %d on %s", errno, remoteIpAddress.c_str());
stop();
return false;
}
return false;
}
return true;
}
GwBuffer::WriteStatus GwSocketConnection::write()
{
if (!hasClient())
{
LOG_DEBUG(GwLog::LOG, "write called on empty client");
return GwBuffer::ERROR;
}
if (!buffer->usedSpace())
{
pendingWrite = false;
return GwBuffer::OK;
}
buffer->fetchData(
-1, [](uint8_t *buffer, size_t len, void *param) -> size_t
{
GwSocketConnection *c = (GwSocketConnection *)param;
int res = send(c->fd, (void *)buffer, len, MSG_DONTWAIT);
if (!c->handleError(res, false))
return 0;
if (res >= len)
{
c->pendingWrite = false;
}
else
{
if (!c->pendingWrite)
{
c->lastWrite = millis();
c->pendingWrite = true;
}
else
{
//we need to check if we have still not been able
//to write until timeout
if (millis() >= (c->lastWrite + c->writeTimeout))
{
c->logger->logDebug(GwLog::ERROR, "Write timeout on channel %s", c->remoteIpAddress.c_str());
c->writeError = true;
}
}
}
return res;
},
this);
if (writeError)
{
LOG_DEBUG(GwLog::DEBUG + 1, "write error on %s", remoteIpAddress.c_str());
return GwBuffer::ERROR;
}
return GwBuffer::OK;
}
bool GwSocketConnection::read()
{
if (!allowRead)
{
size_t maxLen = 100;
char buffer[maxLen];
int res = recv(fd, (void *)buffer, maxLen, MSG_DONTWAIT);
return handleError(res);
}
readBuffer->fillData(
-1, [](uint8_t *buffer, size_t len, void *param) -> size_t
{
GwSocketConnection *c = (GwSocketConnection *)param;
int res = recv(c->fd, (void *)buffer, len, MSG_DONTWAIT);
if (!c->handleError(res))
return 0;
return res;
},
this);
return true;
}
bool GwSocketConnection::messagesFromBuffer(GwMessageFetcher *writer)
{
if (!allowRead)
return false;
return writer->handleBuffer(readBuffer);
}

View File

@ -0,0 +1,36 @@
#pragma once
#include <Arduino.h>
#include <lwip/sockets.h>
#include "GwBuffer.h"
class GwSocketConnection
{
public:
int fd=-1;
int overflows;
String remoteIpAddress;
private:
unsigned long lastWrite = 0;
unsigned long writeTimeout = 10000;
bool pendingWrite = false;
bool writeError = false;
bool allowRead;
GwBuffer *buffer = NULL;
GwBuffer *readBuffer = NULL;
GwLog *logger;
public:
static IPAddress remoteIP(int fd);
GwSocketConnection(GwLog *logger, int id, bool allowRead = false);
void setClient(int fd);
bool hasClient();
void stop();
~GwSocketConnection();
bool connected();
bool enqueue(uint8_t *data, size_t len);
bool hasData();
bool handleError(int res, bool errorIf0 = true);
GwBuffer::WriteStatus write();
bool read();
bool messagesFromBuffer(GwMessageFetcher *writer);
};

View File

@ -2,189 +2,92 @@
#include <ESPmDNS.h> #include <ESPmDNS.h>
#include <lwip/sockets.h> #include <lwip/sockets.h>
#include "GwBuffer.h" #include "GwBuffer.h"
#include "GwSocketConnection.h"
class GwClient{ GwSocketServer::GwSocketServer(const GwConfigHandler *config, GwLog *logger, int minId)
public:
wiFiClientPtr client;
int overflows;
String remoteIp;
private:
unsigned long lastWrite=0;
unsigned long writeTimeout=10000;
bool pendingWrite=false;
bool writeError=false;
bool allowRead;
GwBuffer *buffer=NULL;
GwBuffer *readBuffer=NULL;
GwLog *logger;
public:
GwClient(wiFiClientPtr client,GwLog *logger,int id, bool allowRead=false){
this->client=client;
this->logger=logger;
this->allowRead=allowRead;
String bufName="Sock(";
bufName+=String(id);
bufName+=")";
buffer=new GwBuffer(logger,GwBuffer::TX_BUFFER_SIZE,bufName+"wr");
if (allowRead){
readBuffer=new GwBuffer(logger,GwBuffer::RX_BUFFER_SIZE,bufName+"rd");
}
overflows=0;
if (client != NULL){
remoteIp=client->remoteIP().toString();
}
}
void setClient(wiFiClientPtr client){
this->client=client;
buffer->reset("new client");
if (readBuffer) readBuffer->reset("new client");
overflows=0;
pendingWrite=false;
writeError=false;
lastWrite=0;
if (client){
remoteIp=client->remoteIP().toString();
}
else{
remoteIp=String("---");
}
}
bool hasClient(){
return client != NULL;
}
~GwClient(){
delete buffer;
if (readBuffer) delete readBuffer;
}
bool enqueue(uint8_t *data, size_t len){
if (len == 0) return true;
size_t rt=buffer->addData(data,len);
if (rt < len){
LOG_DEBUG(GwLog::LOG,"overflow on %s",remoteIp.c_str());
overflows++;
return false;
}
return true;
}
bool hasData(){
return buffer->usedSpace() > 0;
}
bool handleError(int res,bool errorIf0=true){
if (res == 0 && errorIf0){
LOG_DEBUG(GwLog::LOG,"client shutdown (recv 0) on %s",remoteIp.c_str());
client->stop();
return false;
}
if (res < 0){
if (errno != EAGAIN){
LOG_DEBUG(GwLog::LOG,"client read error %d on %s",errno,remoteIp.c_str());
client->stop();
return false;
}
return false;
}
return true;
}
GwBuffer::WriteStatus write(){
if (! hasClient()) {
LOG_DEBUG(GwLog::LOG,"write called on empty client");
return GwBuffer::ERROR;
}
if (! buffer->usedSpace()){
pendingWrite=false;
return GwBuffer::OK;
}
buffer->fetchData(-1,[](uint8_t *buffer, size_t len, void *param)->size_t{
GwClient *c=(GwClient*)param;
int res = send(c->client->fd(), (void*) buffer, len, MSG_DONTWAIT);
if (! c->handleError(res,false)) return 0;
if (res >= len){
c->pendingWrite=false;
}
else{
if (!c->pendingWrite){
c->lastWrite=millis();
c->pendingWrite=true;
}
else{
//we need to check if we have still not been able
//to write until timeout
if (millis() >= (c->lastWrite+c->writeTimeout)){
c->logger->logDebug(GwLog::ERROR,"Write timeout on channel %s",c->remoteIp.c_str());
c->writeError=true;
}
}
}
return res;
},this);
if (writeError){
LOG_DEBUG(GwLog::DEBUG+1,"write error on %s",remoteIp.c_str());
return GwBuffer::ERROR;
}
return GwBuffer::OK;
}
bool read(){
if (! allowRead){
size_t maxLen=100;
char buffer[maxLen];
int res = recv(client->fd(), (void*) buffer, maxLen, MSG_DONTWAIT);
return handleError(res);
}
readBuffer->fillData(-1,[](uint8_t *buffer, size_t len, void *param)->size_t{
GwClient *c=(GwClient*)param;
int res = recv(c->client->fd(), (void*) buffer, len, MSG_DONTWAIT);
if (! c->handleError(res)) return 0;
return res;
},this);
return true;
}
bool messagesFromBuffer(GwMessageFetcher *writer){
if (! allowRead) return false;
return writer->handleBuffer(readBuffer);
}
};
GwSocketServer::GwSocketServer(const GwConfigHandler *config,GwLog *logger,int minId){
this->config=config;
this->logger=logger;
this->minId=minId;
maxClients=1;
allowReceive=false;
}
void GwSocketServer::begin(){
maxClients=config->getInt(config->maxClients);
allowReceive=config->getBool(config->readTCP);
clients=new gwClientPtr[maxClients];
for (int i=0;i<maxClients;i++){
clients[i]=gwClientPtr(new GwClient(wiFiClientPtr(NULL),logger,i,allowReceive));
}
server=new WiFiServer(config->getInt(config->serverPort),maxClients+1);
server->begin();
LOG_DEBUG(GwLog::LOG,"Socket server created, port=%d",
config->getInt(config->serverPort));
MDNS.addService("_nmea-0183","_tcp",config->getInt(config->serverPort));
}
void GwSocketServer::loop(bool handleRead,bool handleWrite)
{ {
if (! clients) return; this->config = config;
WiFiClient client = server->available(); // listen for incoming clients this->logger = logger;
this->minId = minId;
if (client) maxClients = 1;
allowReceive = false;
}
bool GwSocketServer::createListener()
{
struct sockaddr_in server;
listener = socket(AF_INET, SOCK_STREAM, 0);
if (listener < 0)
{ {
LOG_DEBUG(GwLog::LOG,"new client connected from %s", return false;
client.remoteIP().toString().c_str()); }
fcntl(client.fd(), F_SETFL, O_NONBLOCK); int enable = 1;
setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
server.sin_family = AF_INET;
server.sin_addr.s_addr = INADDR_ANY;
server.sin_port = htons(listenerPort);
if (bind(listener, (struct sockaddr *)&server, sizeof(server)) < 0)
return false;
if (listen(listener, maxClients) < 0)
return false;
fcntl(listener, F_SETFL, O_NONBLOCK);
return true;
}
void GwSocketServer::begin()
{
maxClients = config->getInt(config->maxClients);
allowReceive = config->getBool(config->readTCP);
listenerPort=config->getInt(config->serverPort);
clients = new GwSocketConnection*[maxClients];
for (int i = 0; i < maxClients; i++)
{
clients[i] = new GwSocketConnection(logger, i, allowReceive);
}
if (! createListener()){
listener=-1;
LOG_DEBUG(GwLog::ERROR,"Unable to create listener");
return;
}
LOG_DEBUG(GwLog::LOG, "Socket server created, port=%d",
config->getInt(config->serverPort));
MDNS.addService("_nmea-0183", "_tcp", config->getInt(config->serverPort));
}
int GwSocketServer::available()
{
if (listener < 0)
return -1;
int client_sock;
struct sockaddr_in _client;
int cs = sizeof(struct sockaddr_in);
client_sock = lwip_accept_r(listener, (struct sockaddr *)&_client, (socklen_t *)&cs);
if (client_sock >= 0)
{
int val = 1;
if (setsockopt(client_sock, SOL_SOCKET, SO_KEEPALIVE, (char *)&val, sizeof(int)) == ESP_OK)
{
if (setsockopt(client_sock, IPPROTO_TCP, TCP_NODELAY, (char *)&val, sizeof(int)) == ESP_OK)
fcntl(client_sock, F_SETFL, O_NONBLOCK);
return client_sock;
}
close(client_sock);
}
return -1;
}
void GwSocketServer::loop(bool handleRead, bool handleWrite)
{
if (!clients)
return;
int client = available(); // listen for incoming clients
if (client >= 0)
{
LOG_DEBUG(GwLog::LOG, "new client connected from %s",
GwSocketConnection::remoteIP(client).toString().c_str());
bool canHandle = false; bool canHandle = false;
for (int i = 0; i < maxClients; i++) for (int i = 0; i < maxClients; i++)
{ {
if (!clients[i]->hasClient()) if (!clients[i]->hasClient())
{ {
clients[i]->setClient(wiFiClientPtr(new WiFiClient(client))); clients[i]->setClient(client);
LOG_DEBUG(GwLog::LOG,"set client as number %d", i); LOG_DEBUG(GwLog::LOG, "set client as number %d", i);
canHandle = true; canHandle = true;
break; break;
} }
@ -192,7 +95,7 @@ void GwSocketServer::loop(bool handleRead,bool handleWrite)
if (!canHandle) if (!canHandle)
{ {
logger->logDebug(GwLog::ERROR, "no space to store client, disconnect"); logger->logDebug(GwLog::ERROR, "no space to store client, disconnect");
client.stop(); close(client);
} }
} }
if (handleWrite) if (handleWrite)
@ -200,69 +103,83 @@ void GwSocketServer::loop(bool handleRead,bool handleWrite)
//sending //sending
for (int i = 0; i < maxClients; i++) for (int i = 0; i < maxClients; i++)
{ {
gwClientPtr client = clients[i]; GwSocketConnection *client = clients[i];
if (!client->hasClient()) if (!client->hasClient())
continue; continue;
GwBuffer::WriteStatus rt = client->write(); GwBuffer::WriteStatus rt = client->write();
if (rt == GwBuffer::ERROR) if (rt == GwBuffer::ERROR)
{ {
LOG_DEBUG(GwLog::ERROR, "write error on %s, closing", client->remoteIp.c_str()); LOG_DEBUG(GwLog::ERROR, "write error on %s, closing", client->remoteIpAddress.c_str());
client->client->stop(); client->stop();
} }
} }
} }
for (int i = 0; i < maxClients; i++) for (int i = 0; i < maxClients; i++)
{ {
gwClientPtr client = clients[i]; GwSocketConnection *client = clients[i];
if (!client->hasClient()) if (!client->hasClient())
continue; continue;
if (!client->client->connected()) if (!client->connected())
{ {
LOG_DEBUG(GwLog::LOG,"client %d disconnect %s", i, client->remoteIp.c_str()); LOG_DEBUG(GwLog::LOG, "client %d disconnect %s", i, client->remoteIpAddress.c_str());
client->client->stop(); client->stop();
client->setClient(NULL);
} }
else else
{ {
if (handleRead) client->read(); if (handleRead)
client->read();
} }
} }
} }
bool GwSocketServer::readMessages(GwMessageFetcher *writer){ void GwSocketServer::readMessages(GwMessageFetcher *writer)
if (! allowReceive || ! clients) return false; {
bool hasMessages=false; if (!allowReceive || !clients)
for (int i = 0; i < maxClients; i++){ return;
writer->id=minId+i;
if (!clients[i]->hasClient()) continue;
if (clients[i]->messagesFromBuffer(writer)) hasMessages=true;
}
return hasMessages;
}
void GwSocketServer::sendToClients(const char *buf,int source){
if (! clients) return;
int len=strlen(buf);
int sourceIndex=source-minId;
for (int i = 0; i < maxClients; i++) for (int i = 0; i < maxClients; i++)
{ {
if (i == sourceIndex)continue; //never send out to the source we received from writer->id = minId + i;
gwClientPtr client = clients[i]; if (!clients[i]->hasClient())
if (! client->hasClient()) continue; continue;
if ( client->client->connected() ) { clients[i]->messagesFromBuffer(writer);
client->enqueue((uint8_t*)buf,len); }
return;
}
size_t GwSocketServer::sendToClients(const char *buf, int source,bool partial)
{
if (!clients)
return 0;
bool hasSend=false;
int len = strlen(buf);
int sourceIndex = source - minId;
for (int i = 0; i < maxClients; i++)
{
if (i == sourceIndex)
continue; //never send out to the source we received from
GwSocketConnection *client = clients[i];
if (!client->hasClient())
continue;
if (client->connected())
{
if(client->enqueue((uint8_t *)buf, len)) hasSend=true;
} }
} }
return hasSend?len:0;
} }
int GwSocketServer::numClients(){ int GwSocketServer::numClients()
if (! clients) return 0; {
int num=0; if (!clients)
for (int i = 0; i < maxClients; i++){ return 0;
if (clients[i]->hasClient()) num++; int num = 0;
for (int i = 0; i < maxClients; i++)
{
if (clients[i]->hasClient())
num++;
} }
return num; return num;
} }
GwSocketServer::~GwSocketServer(){ GwSocketServer::~GwSocketServer()
{
} }

View File

@ -3,28 +3,29 @@
#include "GWConfig.h" #include "GWConfig.h"
#include "GwLog.h" #include "GwLog.h"
#include "GwBuffer.h" #include "GwBuffer.h"
#include "GwChannelInterface.h"
#include <memory> #include <memory>
#include <WiFi.h>
using wiFiClientPtr = std::shared_ptr<WiFiClient>; class GwSocketConnection;
class GwClient; class GwSocketServer: public GwChannelInterface{
using gwClientPtr = std::shared_ptr<GwClient>;
class GwSocketServer{
private: private:
const GwConfigHandler *config; const GwConfigHandler *config;
GwLog *logger; GwLog *logger;
gwClientPtr *clients=NULL; GwSocketConnection **clients=NULL;
WiFiServer *server=NULL; int listener=-1;
int listenerPort=-1;
bool allowReceive; bool allowReceive;
int maxClients; int maxClients;
int minId; int minId;
bool createListener();
int available();
public: public:
GwSocketServer(const GwConfigHandler *config,GwLog *logger,int minId); GwSocketServer(const GwConfigHandler *config,GwLog *logger,int minId);
~GwSocketServer(); ~GwSocketServer();
void begin(); void begin();
void loop(bool handleRead=true,bool handleWrite=true); virtual void loop(bool handleRead=true,bool handleWrite=true);
void sendToClients(const char *buf,int sourceId); virtual size_t sendToClients(const char *buf,int sourceId, bool partialWrite=false);
int numClients(); int numClients();
bool readMessages(GwMessageFetcher *writer); virtual void readMessages(GwMessageFetcher *writer);
}; };
#endif #endif

View File

@ -0,0 +1,287 @@
#include "GwTcpClient.h"
#include <functional>
#include <ESPmDNS.h>
class ResolveArgs{
public:
String host;
uint32_t timeout;
GwTcpClient *client;
};
bool GwTcpClient::hasConfig(){
return configured;
}
bool GwTcpClient::isConnected(){
return state == C_CONNECTED;
}
void GwTcpClient::stop()
{
if (connection && connection->hasClient())
{
LOG_DEBUG(GwLog::DEBUG, "stopping tcp client");
connection->stop();
}
state = C_DISABLED;
}
void GwTcpClient::startResolving(){
LOG_DEBUG(GwLog::DEBUG,"TcpClient::resolveHost to %s:%d",
remoteAddress.c_str(),port);
state = C_INITIALIZED;
IPAddress addr;
if (! addr.fromString(remoteAddress)){
if (remoteAddress.endsWith(".local")){
//try to resolve
resolveHost(remoteAddress.substring(0,remoteAddress.length()-6));
}
else{
error="invalid ip "+remoteAddress;
LOG_DEBUG(GwLog::ERROR,"%s",error.c_str());
return;
}
}
else{
setResolved(addr,true);
startConnection();
}
}
void GwTcpClient::startConnection()
{
LOG_DEBUG(GwLog::DEBUG,"TcpClient::startConnection to %s:%d",
remoteAddress.c_str(),port);
ResolvedAddress addr=getResolved();
state = C_INITIALIZED;
connectStart=millis();
if (! addr.resolved){
error="unable to resolve "+remoteAddress;
LOG_DEBUG(GwLog::ERROR,"%s",error.c_str());
return;
}
else{
if (error.isEmpty()) error="connecting...";
}
uint32_t ip_addr = addr.address;
struct sockaddr_in serveraddr;
memset((char *) &serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
memcpy((void *)&serveraddr.sin_addr.s_addr, (const void *)(&ip_addr), 4);
serveraddr.sin_port = htons(port);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
error="unable to create socket";
LOG_DEBUG(GwLog::ERROR,"unable to create socket: %d", errno);
return;
}
fcntl( sockfd, F_SETFL, fcntl( sockfd, F_GETFL, 0 ) | O_NONBLOCK );
int res = lwip_connect_r(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
if (res < 0 ) {
if (errno != EINPROGRESS){
error=String("connect error ")+String(strerror(errno));
LOG_DEBUG(GwLog::ERROR,"connect on fd %d, errno: %d, \"%s\"", sockfd, errno, strerror(errno));
close(sockfd);
return;
}
state=C_CONNECTING;
connection->setClient(sockfd);
LOG_DEBUG(GwLog::DEBUG,"TcpClient connecting...");
}
else{
state=C_CONNECTED;
connection->setClient(sockfd);
LOG_DEBUG(GwLog::DEBUG,"TcpClient connected");
}
}
void GwTcpClient::checkConnection()
{
unsigned long now=millis();
LOG_DEBUG(GwLog::DEBUG+3,"TcpClient::checkConnection state=%d, start=%ul, now=%ul",
(int)state,connectStart,now);
if (state == C_RESOLVING){
//TODO: timeout???
return;
}
if (state == C_RESOLVED){
startConnection();
return;
}
if (! connection->hasClient()){
state = hasConfig()?C_INITIALIZED:C_DISABLED;
}
if (state == C_INITIALIZED){
if ((now - connectStart) > CON_TIMEOUT){
LOG_DEBUG(GwLog::LOG,"retry connect to %s",remoteAddress.c_str());
startResolving();
}
return;
}
if (state != C_CONNECTING){
return;
}
fd_set fdset;
struct timeval tv;
FD_ZERO(&fdset);
int sockfd=connection->fd;
FD_SET(connection->fd, &fdset);
tv.tv_sec = 0;
tv.tv_usec = 0;
int res = select(sockfd + 1, nullptr, &fdset, nullptr, &tv);
if (res < 0) {
error=String("select error ")+String(strerror(errno));
LOG_DEBUG(GwLog::ERROR,"select on fd %d, errno: %d, \"%s\"", sockfd, errno, strerror(errno));
connection->stop();
return;
} else if (res == 0) {
//still connecting
if ((now - connectStart) >= CON_TIMEOUT){
error="connect timeout";
LOG_DEBUG(GwLog::ERROR,"connect timeout to %s, retry",remoteAddress.c_str());
connection->stop();
return;
}
return;
} else {
int sockerr;
socklen_t len = (socklen_t)sizeof(int);
res = getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &sockerr, &len);
if (res < 0) {
error="getsockopt failed";
LOG_DEBUG(GwLog::ERROR,"getsockopt on fd %d, errno: %d, \"%s\"", sockfd, errno, strerror(errno));
connection->stop();
return;
}
if (sockerr != 0) {
error=String("socket error ")+String(strerror(sockerr));
LOG_DEBUG(GwLog::ERROR,"socket error on fd %d, errno: %d, \"%s\"", sockfd, sockerr, strerror(sockerr));
connection->stop();
return;
}
}
if (connection->connected()){
error="";
LOG_DEBUG(GwLog::LOG,"connected to %s",remoteAddress.c_str());
state=C_CONNECTED;
}
else{
error=String("connect error ")+String(strerror(errno));
LOG_DEBUG(GwLog::ERROR,"%s",error.c_str());
state=C_INITIALIZED;
}
}
GwTcpClient::GwTcpClient(GwLog *logger)
{
this->logger = logger;
this->connection=NULL;
locker=xSemaphoreCreateMutex();
}
GwTcpClient::~GwTcpClient(){
if (connection)
delete connection;
vSemaphoreDelete(locker);
}
void GwTcpClient::begin(int sourceId,String address, uint16_t port,bool allowRead)
{
stop();
this->sourceId=sourceId;
this->remoteAddress = address;
this->port = port;
configured=true;
state = C_INITIALIZED;
this->connection = new GwSocketConnection(logger,0,allowRead);
startResolving();
}
void GwTcpClient::loop(bool handleRead,bool handleWrite)
{
checkConnection();
if (state != C_CONNECTED){
return;
}
if (handleRead){
if (connection->hasClient()){
if (! connection->connected()){
LOG_DEBUG(GwLog::ERROR,"tcp client connection closed on %s",connection->remoteIpAddress.c_str());
connection->stop();
}
else{
connection->read();
}
}
}
if (handleWrite){
if (connection->hasClient()){
GwBuffer::WriteStatus rt = connection->write();
if (rt == GwBuffer::ERROR)
{
LOG_DEBUG(GwLog::ERROR, "write error on %s, closing", connection->remoteIpAddress.c_str());
connection->stop();
}
}
}
}
size_t GwTcpClient::sendToClients(const char *buf,int sourceId, bool partialWrite){
if (sourceId == this->sourceId) return 0;
if (state != C_CONNECTED) return 0;
if (! connection->hasClient()) return 0;
size_t len=strlen(buf);
if (connection->enqueue((uint8_t*)buf,len)){
return len;
}
return 0;
}
void GwTcpClient::readMessages(GwMessageFetcher *writer){
if (state != C_CONNECTED) return;
if (! connection->hasClient()) return;
connection->messagesFromBuffer(writer);
}
void GwTcpClient::resolveHost(String host)
{
LOG_DEBUG(GwLog::LOG,"start resolving %s",host.c_str());
{
GWSYNCHRONIZED(&locker);
resolvedAddress.resolved = false;
}
state = C_RESOLVING;
error=String("resolving ")+host;
ResolveArgs *args=new ResolveArgs();
args->host = host;
args->timeout = 10000;
args->client = this;
if (xTaskCreate([](void *p)
{
ResolveArgs *args = (ResolveArgs *)p;
struct ip4_addr addr;
addr.addr = 0;
esp_err_t err = mdns_query_a(args->host.c_str(), args->timeout, &addr);
if (err)
{
args->client->setResolved(IPAddress(), false);
}
else{
args->client->setResolved(IPAddress(addr.addr), true);
}
args->client->logger->logDebug(GwLog::DEBUG,"resolve task end");
delete args;
vTaskDelete(NULL);
},
"resolve", 4000, args, 0, NULL) != pdPASS)
{
LOG_DEBUG(GwLog::ERROR,"unable to start resolve task");
error = "unable to start resolve task";
delete args;
setResolved(IPAddress(), false);
}
}
void GwTcpClient::setResolved(IPAddress addr, bool valid){
LOG_DEBUG(GwLog::LOG,"setResolved %s, valid=%s",
addr.toString().c_str(),(valid?"true":"false"));
GWSYNCHRONIZED(&locker);
resolvedAddress.address=addr;
resolvedAddress.resolved=valid;
state=C_RESOLVED;
}
GwTcpClient::ResolvedAddress GwTcpClient::getResolved(){
GWSYNCHRONIZED(&locker);
return resolvedAddress;
}

View File

@ -0,0 +1,56 @@
#pragma once
#include "GwSocketConnection.h"
#include "GwChannelInterface.h"
#include "GwSynchronized.h"
class GwTcpClient : public GwChannelInterface
{
class ResolvedAddress{
public:
IPAddress address;
bool resolved=false;
};
static const unsigned long CON_TIMEOUT=10000;
GwSocketConnection *connection = NULL;
String remoteAddress;
ResolvedAddress resolvedAddress;
uint16_t port = 0;
unsigned long connectStart=0;
GwLog *logger;
int sourceId;
bool configured=false;
String error;
SemaphoreHandle_t locker;
public:
typedef enum
{
C_DISABLED = 0,
C_INITIALIZED = 1,
C_RESOLVING = 2,
C_RESOLVED = 3,
C_CONNECTING = 4,
C_CONNECTED = 5
} State;
private:
State state = C_DISABLED;
void stop();
void startResolving();
void startConnection();
void checkConnection();
bool hasConfig();
void resolveHost(String host);
void setResolved(IPAddress addr, bool valid);
ResolvedAddress getResolved();
public:
GwTcpClient(GwLog *logger);
~GwTcpClient();
void begin(int sourceId,String address, uint16_t port,bool allowRead);
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);
bool isConnected();
String getError(){return error;}
};

View File

@ -99,6 +99,10 @@ public:
GWSYNCHRONIZED(mainLock); GWSYNCHRONIZED(mainLock);
api->getBoatDataValues(num,list); api->getBoatDataValues(num,list);
} }
virtual void getStatus(Status &status){
GWSYNCHRONIZED(mainLock);
api->getStatus(status);
}
virtual ~TaskApi(){}; virtual ~TaskApi(){};
}; };

View File

@ -23,5 +23,7 @@ class GwWifi{
bool clientConnected(); bool clientConnected();
bool connectClient(); bool connectClient();
String apIP(); String apIP();
bool isApActive(){return apActive;}
bool isClientActive(){return wifiClient->asBoolean();}
}; };
#endif #endif

View File

@ -35,6 +35,7 @@ const unsigned long HEAP_REPORT_TIME=2000; //set to 0 to disable heap reporting
#include <ESPAsyncWebServer.h> #include <ESPAsyncWebServer.h>
#include <Preferences.h> #include <Preferences.h>
#include <ESPmDNS.h> #include <ESPmDNS.h>
#include <memory>
#include <map> #include <map>
#include <vector> #include <vector>
#include "esp_heap_caps.h" #include "esp_heap_caps.h"
@ -61,18 +62,14 @@ const unsigned long HEAP_REPORT_TIME=2000; //set to 0 to disable heap reporting
#include "GwUserCode.h" #include "GwUserCode.h"
#include "GwStatistics.h" #include "GwStatistics.h"
#include "GwUpdate.h" #include "GwUpdate.h"
#include "GwTcpClient.h"
#include "GwChannel.h"
#include "GwChannelList.h"
//NMEA message channels
#define N2K_CHANNEL_ID 0
#define USB_CHANNEL_ID 1
#define SERIAL1_CHANNEL_ID 2
#define MIN_TCP_CHANNEL_ID 3
#define MIN_USER_TASK 200
#define MAX_NMEA2000_MESSAGE_SEASMART_SIZE 500 #define MAX_NMEA2000_MESSAGE_SEASMART_SIZE 500
#define MAX_NMEA0183_MESSAGE_SIZE 150 // For AIS #define MAX_NMEA0183_MESSAGE_SIZE MAX_NMEA2000_MESSAGE_SEASMART_SIZE
//https://curiouser.cheshireeng.com/2014/08/19/c-compile-time-assert/ //https://curiouser.cheshireeng.com/2014/08/19/c-compile-time-assert/
#define CASSERT(predicate, text) _impl_CASSERT_LINE(predicate,__LINE__) #define CASSERT(predicate, text) _impl_CASSERT_LINE(predicate,__LINE__)
#define _impl_PASTE(a,b) a##b #define _impl_PASTE(a,b) a##b
@ -109,7 +106,7 @@ bool fixedApPass=false;
bool fixedApPass=true; bool fixedApPass=true;
#endif #endif
GwWifi gwWifi(&config,&logger,fixedApPass); GwWifi gwWifi(&config,&logger,fixedApPass);
GwSocketServer socketServer(&config,&logger,MIN_TCP_CHANNEL_ID); GwChannelList channels(&logger,&config);
GwBoatData boatData(&logger); GwBoatData boatData(&logger);
GwXDRMappings xdrMappings(&logger,&config); GwXDRMappings xdrMappings(&logger,&config);
@ -119,8 +116,6 @@ int NodeAddress; // To store last Node Address
Preferences preferences; // Nonvolatile storage on ESP32 - To store LastDeviceAddress Preferences preferences; // Nonvolatile storage on ESP32 - To store LastDeviceAddress
N2kDataToNMEA0183 *nmea0183Converter=NULL; N2kDataToNMEA0183 *nmea0183Converter=NULL;
NMEA0183DataToN2K *toN2KConverter=NULL; NMEA0183DataToN2K *toN2KConverter=NULL;
tActisenseReader *actisenseReader=NULL;
Stream *usbStream=NULL;
SemaphoreHandle_t mainLock; SemaphoreHandle_t mainLock;
@ -130,13 +125,6 @@ GwWebServer webserver(&logger,&mainQueue,80);
GwCounter<unsigned long> countNMEA2KIn("count2Kin"); GwCounter<unsigned long> countNMEA2KIn("count2Kin");
GwCounter<unsigned long> countNMEA2KOut("count2Kout"); GwCounter<unsigned long> countNMEA2KOut("count2Kout");
GwCounter<String> countUSBIn("countUSBin");
GwCounter<String> countUSBOut("countUSBout");
GwCounter<String> countTCPIn("countTCPin");
GwCounter<String> countTCPOut("countTCPout");
GwCounter<String> countSerialIn("countSerialIn");
GwCounter<String> countSerialOut("countSerialOut");
unsigned long saltBase=esp_random(); unsigned long saltBase=esp_random();
char hv(uint8_t nibble){ char hv(uint8_t nibble){
@ -179,136 +167,50 @@ bool checkPass(String hash){
} }
GwUpdate updater(&logger,&webserver,&checkPass); GwUpdate updater(&logger,&webserver,&checkPass);
void updateNMEACounter(int id,const char *msg,bool incoming,bool fail=false){
//we rely on the msg being long enough
char key[6];
if (msg[0] == '$') {
strncpy(key,&msg[3],3);
key[3]=0;
}
else if(msg[0] == '!'){
strncpy(key,&msg[1],5);
key[5]=0;
}
else return;
GwCounter<String> *counter=NULL;
if (id == USB_CHANNEL_ID) counter=incoming?&countUSBIn:&countUSBOut;
if (id == SERIAL1_CHANNEL_ID) counter=incoming?&countSerialIn:&countSerialOut;
if (id >= MIN_TCP_CHANNEL_ID) counter=incoming?&countTCPIn:&countTCPOut;
if (! counter) return;
if (fail){
counter->addFail(key);
}
else{
counter->add(key);
}
}
//configs that we need in main
GwConfigInterface *sendUsb=config.getConfigItem(config.sendUsb,true);
GwConfigInterface *readUsb=config.getConfigItem(config.receiveUsb,true);
GwConfigInterface *usbActisense=config.getConfigItem(config.usbActisense,true);
GwConfigInterface *usbSendActisens=config.getConfigItem(config.usbActSend,true);
GwConfigInterface *sendTCP=config.getConfigItem(config.sendTCP,true);
GwConfigInterface *readTCP=config.getConfigItem(config.readTCP,true);
GwConfigInterface *sendSeasmart=config.getConfigItem(config.sendSeasmart,true);
GwConfigInterface *systemName=config.getConfigItem(config.systemName,true); GwConfigInterface *systemName=config.getConfigItem(config.systemName,true);
GwConfigInterface *n2kFromTCP=config.getConfigItem(config.tcpToN2k,true);
GwConfigInterface *n2kFromUSB=config.getConfigItem(config.usbToN2k,true);
GwConfigInterface *receiveSerial=config.getConfigItem(config.receiveSerial,true);
GwConfigInterface *sendSerial=config.getConfigItem(config.sendSerial,true);
GwConfigInterface *n2kFromSerial=config.getConfigItem(config.serialToN2k,true);
GwNmeaFilter usbReadFilter(config.getConfigItem(config.usbReadFilter,true));
GwNmeaFilter usbWriteFilter(config.getConfigItem(config.usbWriteFilter,true));
GwNmeaFilter serialReadFilter(config.getConfigItem(config.serialReadF,true));
GwNmeaFilter serialWriteFilter(config.getConfigItem(config.serialWriteF,true));
GwNmeaFilter tcpReadFilter(config.getConfigItem(config.tcpReadFilter,true));
GwNmeaFilter tcpWriteFilter(config.getConfigItem(config.tcpWriteFilter,true));
bool checkFilter(const char *buffer,int channelId,bool read){
GwNmeaFilter *filter=NULL;
if (channelId == USB_CHANNEL_ID) filter=read?&usbReadFilter:&usbWriteFilter;
else if (channelId == SERIAL1_CHANNEL_ID) filter=read?&serialReadFilter:&serialWriteFilter;
else if (channelId >= MIN_TCP_CHANNEL_ID) filter=read?&tcpReadFilter:&tcpWriteFilter;
if (!filter) return true;
if (filter->canPass(buffer)) return true;
logger.logDebug(GwLog::DEBUG,"%s filter for channel %d dropped %s",(read?"read":"write"),channelId,buffer);
return false;
}
bool serCanWrite=true; void handleN2kMessage(const tN2kMsg &n2kMsg,int sourceId, bool isConverted=false)
bool serCanRead=true;
GwSerial *usbSerial = new GwSerial(NULL, 0, USB_CHANNEL_ID);
GwSerial *serial1=NULL;
void sendBufferToChannels(const char * buffer, int sourceId){
if (sendTCP->asBoolean() && checkFilter(buffer,MIN_TCP_CHANNEL_ID,false)){
socketServer.sendToClients(buffer,sourceId);
updateNMEACounter(MIN_TCP_CHANNEL_ID,buffer,false);
}
if (! actisenseReader && sendUsb->asBoolean() && checkFilter(buffer,USB_CHANNEL_ID,false)){
usbSerial->sendToClients(buffer,sourceId);
updateNMEACounter(USB_CHANNEL_ID,buffer,false);
}
if (serial1 && serCanWrite && checkFilter(buffer,SERIAL1_CHANNEL_ID,false)){
serial1->sendToClients(buffer,sourceId);
updateNMEACounter(SERIAL1_CHANNEL_ID,buffer,false);
}
}
typedef enum {
N2KT_MSGIN, //from CAN
N2KT_MSGINT, //from internal source
N2KT_MSGOUT, //from converter
N2KT_MSGACT //from actisense
} N2K_MsgDirection;
void handleN2kMessage(const tN2kMsg &n2kMsg,N2K_MsgDirection direction)
{ {
logger.logDebug(GwLog::DEBUG + 1, "N2K: pgn %d, dir %d", logger.logDebug(GwLog::DEBUG + 1, "N2K: pgn %d, dir %d",
n2kMsg.PGN,(int)direction); n2kMsg.PGN,sourceId);
if (direction == N2KT_MSGIN){ if (sourceId == N2K_CHANNEL_ID){
countNMEA2KIn.add(n2kMsg.PGN); countNMEA2KIn.add(n2kMsg.PGN);
} }
if (sendSeasmart->asBoolean()) char *buf=new char[MAX_NMEA2000_MESSAGE_SEASMART_SIZE];
{ std::unique_ptr<char> bufDel(buf);
char buf[MAX_NMEA2000_MESSAGE_SEASMART_SIZE]; bool messageCreated=false;
if (N2kToSeasmart(n2kMsg, millis(), buf, MAX_NMEA2000_MESSAGE_SEASMART_SIZE) == 0) channels.allChannels([&](GwChannel *c){
return; if (c->sendSeaSmart()){
socketServer.sendToClients(buf, N2K_CHANNEL_ID); if (! messageCreated){
if (N2kToSeasmart(n2kMsg, millis(), buf, MAX_NMEA2000_MESSAGE_SEASMART_SIZE) != 0) {
messageCreated=true;
}
}
if (messageCreated){
c->sendToClients(buf,sourceId);
}
}
});
channels.allChannels([&](GwChannel *c){
c->sendActisense(n2kMsg,sourceId);
});
if (! isConverted){
nmea0183Converter->HandleMsg(n2kMsg,sourceId);
} }
if (actisenseReader && direction != N2KT_MSGACT && usbStream && usbSendActisens->asBoolean()) if (sourceId != N2K_CHANNEL_ID){
{
countUSBOut.add(String(n2kMsg.PGN));
n2kMsg.SendInActisenseFormat(usbStream);
}
if (direction != N2KT_MSGOUT){
nmea0183Converter->HandleMsg(n2kMsg);
}
if (direction != N2KT_MSGIN){
countNMEA2KOut.add(n2kMsg.PGN); countNMEA2KOut.add(n2kMsg.PGN);
NMEA2000.SendMsg(n2kMsg); NMEA2000.SendMsg(n2kMsg);
} }
}; };
void handleReceivedNmeaMessage(const char *buf, int sourceId){
if (! checkFilter(buf,sourceId,true)) return;
updateNMEACounter(sourceId,buf,true);
if ( (sourceId >= MIN_USER_TASK) ||
(sourceId == USB_CHANNEL_ID && n2kFromUSB->asBoolean())||
(sourceId >= MIN_TCP_CHANNEL_ID && n2kFromTCP->asBoolean())||
(sourceId == SERIAL1_CHANNEL_ID && n2kFromSerial->asBoolean())
)
toN2KConverter->parseAndSend(buf,sourceId);
sendBufferToChannels(buf,sourceId);
}
//***************************************************************************** //*****************************************************************************
void SendNMEA0183Message(const tNMEA0183Msg &NMEA0183Msg, int sourceId,bool convert=false) { void SendNMEA0183Message(const tNMEA0183Msg &NMEA0183Msg, int sourceId,bool convert=false) {
logger.logDebug(GwLog::DEBUG+2,"SendNMEA0183(1)"); logger.logDebug(GwLog::DEBUG+2,"SendNMEA0183(1)");
char buf[MAX_NMEA0183_MESSAGE_SIZE+3]; char *buf=new char[MAX_NMEA0183_MESSAGE_SIZE+3];
std::unique_ptr<char> bufDel(buf);
if ( !NMEA0183Msg.GetMessage(buf, MAX_NMEA0183_MESSAGE_SIZE) ) return; if ( !NMEA0183Msg.GetMessage(buf, MAX_NMEA0183_MESSAGE_SIZE) ) return;
logger.logDebug(GwLog::DEBUG+2,"SendNMEA0183: %s",buf); logger.logDebug(GwLog::DEBUG+2,"SendNMEA0183: %s",buf);
if (convert){ if (convert){
@ -318,42 +220,11 @@ void SendNMEA0183Message(const tNMEA0183Msg &NMEA0183Msg, int sourceId,bool conv
buf[len]=0x0d; buf[len]=0x0d;
buf[len+1]=0x0a; buf[len+1]=0x0a;
buf[len+2]=0; buf[len+2]=0;
sendBufferToChannels(buf,sourceId); channels.allChannels([&](GwChannel *c){
c->sendToClients(buf,sourceId);
});
} }
class GwSerialLog : public GwLogWriter{
static const size_t bufferSize=4096;
char *logBuffer=NULL;
int wp=0;
public:
GwSerialLog(){
logBuffer=new char[bufferSize];
wp=0;
}
virtual ~GwSerialLog(){}
virtual void write(const char *data){
int len=strlen(data);
if ((wp+len) >= (bufferSize-1)) return;
strncpy(logBuffer+wp,data,len);
wp+=len;
logBuffer[wp]=0;
}
virtual void flush(){
size_t handled=0;
while (handled < wp){
usbSerial->flush();
size_t rt=usbSerial->sendToClients(logBuffer+handled,-1,true);
handled+=rt;
}
wp=0;
logBuffer[0]=0;
}
};
GwSerialLog logWriter;
class ApiImpl : public GwApi class ApiImpl : public GwApi
{ {
private: private:
@ -370,7 +241,7 @@ public:
} }
virtual void sendN2kMessage(const tN2kMsg &msg,bool convert) virtual void sendN2kMessage(const tN2kMsg &msg,bool convert)
{ {
handleN2kMessage(msg,convert?N2KT_MSGINT:N2KT_MSGOUT); handleN2kMessage(msg,sourceId,!convert);
} }
virtual void sendNMEA0183Message(const tNMEA0183Msg &msg, int sourceId,bool convert) virtual void sendNMEA0183Message(const tNMEA0183Msg &msg, int sourceId,bool convert)
@ -413,6 +284,20 @@ public:
} }
} }
} }
virtual void getStatus(Status &status){
status.empty();
status.wifiApOn=gwWifi.isApActive();
status.wifiClientOn=gwWifi.isClientActive();
status.wifiClientConnected=gwWifi.clientConnected();
status.wifiApIp=gwWifi.apIP();
status.systemName=systemName->asString();
status.wifiApPass=config.getString(config.apPassword);
status.wifiClientIp=WiFi.localIP().toString();
status.wifiClientSSID=config.getString(config.wifiSSID);
status.n2kRx=countNMEA2KIn.getGlobal();
status.n2kTx=countNMEA2KOut.getGlobal();
channels.fillStatus(status);
}
virtual GwBoatData *getBoatData(){ virtual GwBoatData *getBoatData(){
return &boatData; return &boatData;
} }
@ -474,17 +359,11 @@ protected:
GwJsonDocument status(256 + GwJsonDocument status(256 +
countNMEA2KIn.getJsonSize()+ countNMEA2KIn.getJsonSize()+
countNMEA2KOut.getJsonSize() + countNMEA2KOut.getJsonSize() +
countUSBIn.getJsonSize()+ channels.getJsonSize()
countUSBOut.getJsonSize()+
countSerialIn.getJsonSize()+
countSerialOut.getJsonSize()+
countTCPIn.getJsonSize()+
countTCPOut.getJsonSize()
); );
status["version"] = VERSION; status["version"] = VERSION;
status["wifiConnected"] = gwWifi.clientConnected(); status["wifiConnected"] = gwWifi.clientConnected();
status["clientIP"] = WiFi.localIP().toString(); status["clientIP"] = WiFi.localIP().toString();
status["numClients"] = socketServer.numClients();
status["apIp"] = gwWifi.apIP(); status["apIp"] = gwWifi.apIP();
size_t bsize=2*sizeof(unsigned long)+1; size_t bsize=2*sizeof(unsigned long)+1;
unsigned long base=saltBase + ( millis()/1000UL & ~0x7UL); unsigned long base=saltBase + ( millis()/1000UL & ~0x7UL);
@ -495,12 +374,7 @@ protected:
//nmea0183Converter->toJson(status); //nmea0183Converter->toJson(status);
countNMEA2KIn.toJson(status); countNMEA2KIn.toJson(status);
countNMEA2KOut.toJson(status); countNMEA2KOut.toJson(status);
countUSBIn.toJson(status); channels.toJson(status);
countUSBOut.toJson(status);
countSerialIn.toJson(status);
countSerialOut.toJson(status);
countTCPIn.toJson(status);
countTCPOut.toJson(status);
serializeJson(status, result); serializeJson(status, result);
} }
}; };
@ -683,7 +557,8 @@ protected:
tNMEA0183Msg msg; tNMEA0183Msg msg;
msg.Init("XDR",config.getString(config.talkerId,String("GP")).c_str()); msg.Init("XDR",config.getString(config.talkerId,String("GP")).c_str());
msg.AddStrField(val.c_str()); msg.AddStrField(val.c_str());
char buf[MAX_NMEA0183_MSG_BUF_LEN]; char *buf=new char[MAX_NMEA0183_MSG_BUF_LEN+2];
std::unique_ptr<char> bufDel(buf);
msg.GetMessage(buf,MAX_NMEA0183_MSG_BUF_LEN); msg.GetMessage(buf,MAX_NMEA0183_MSG_BUF_LEN);
result=buf; result=buf;
} }
@ -707,75 +582,19 @@ void setup() {
uint8_t chipid[6]; uint8_t chipid[6];
uint32_t id = 0; uint32_t id = 0;
config.loadConfig(); config.loadConfig();
// Init USB serial port bool fallbackSerial=false;
GwConfigInterface *usbBaud=config.getConfigItem(config.usbBaud,false);
int baud=115200;
if (usbBaud){
baud=usbBaud->asInt();
}
#ifdef FALLBACK_SERIAL #ifdef FALLBACK_SERIAL
int st=-1; fallbackSerial=true;
#else
int st=usbSerial->setup(baud,3,1); //TODO: PIN defines
#endif
if (st < 0){
//falling back to old style serial for logging //falling back to old style serial for logging
Serial.begin(baud); Serial.begin(baud);
Serial.printf("fallback serial enabled, error was %d\n",st); Serial.printf("fallback serial enabled, error was %d\n",st);
logger.prefix="FALLBACK:"; logger.prefix="FALLBACK:";
} #endif
else{
logger.prefix="GWSERIAL:";
logger.setWriter(&logWriter);
logger.logDebug(GwLog::LOG,"created GwSerial for USB port");
}
logger.logDebug(GwLog::LOG,"config: %s", config.toString().c_str());
userCodeHandler.startInitTasks(MIN_USER_TASK); userCodeHandler.startInitTasks(MIN_USER_TASK);
#ifdef GWSERIAL_MODE
int serialrx=-1;
int serialtx=-1;
#ifdef GWSERIAL_TX
serialtx=GWSERIAL_TX;
#endif
#ifdef GWSERIAL_RX
serialrx=GWSERIAL_RX;
#endif
//the mode is a compile time preselection from hardware.h
String serialMode(F(GWSERIAL_MODE));
//the serial direction is from the config (only valid for mode UNI)
String serialDirection=config.getString(config.serialDirection);
//we only consider the direction if mode is UNI
if (serialMode != String("UNI")){
serialDirection=String("");
//if mode is UNI it depends on the selection
serCanRead=receiveSerial->asBoolean();
serCanWrite=sendSerial->asBoolean();
}
if (serialDirection == "receive" || serialDirection == "off" || serialMode == "RX") serCanWrite=false;
if (serialDirection == "send" || serialDirection == "off" || serialMode == "TX") serCanRead=false;
logger.logDebug(GwLog::DEBUG,"serial set up: mode=%s,direction=%s,rx=%d,tx=%d",
serialMode.c_str(),serialDirection.c_str(),serialrx,serialtx
);
if (serialtx != -1 || serialrx != -1){
logger.logDebug(GwLog::LOG,"creating serial interface rx=%d, tx=%d",serialrx,serialtx);
serial1=new GwSerial(&logger,1,SERIAL1_CHANNEL_ID,serCanRead);
}
if (serial1){
int rt=serial1->setup(config.getInt(config.serialBaud,115200),serialrx,serialtx);
logger.logDebug(GwLog::LOG,"starting serial returns %d",rt);
}
#endif
MDNS.begin(config.getConfigItem(config.systemName)->asCString());
gwWifi.setup(); gwWifi.setup();
MDNS.begin(config.getConfigItem(config.systemName)->asCString());
// Start TCP server channels.begin(fallbackSerial);
socketServer.begin();
logger.flush(); logger.flush();
logger.logDebug(GwLog::LOG,"usbRead: %s", usbReadFilter.toString().c_str());
logger.flush();
webserver.registerMainHandler("/api/reset", [](AsyncWebServerRequest *request)->GwRequestMessage *{ webserver.registerMainHandler("/api/reset", [](AsyncWebServerRequest *request)->GwRequestMessage *{
return new ResetRequest(request->arg("_hash")); return new ResetRequest(request->arg("_hash"));
}); });
@ -829,15 +648,15 @@ void setup() {
[](const tNMEA0183Msg &msg, int sourceId){ [](const tNMEA0183Msg &msg, int sourceId){
SendNMEA0183Message(msg,sourceId,false); SendNMEA0183Message(msg,sourceId,false);
} }
, N2K_CHANNEL_ID, ,
config.getString(config.talkerId,String("GP")), config.getString(config.talkerId,String("GP")),
&xdrMappings, &xdrMappings,
config.getInt(config.minXdrInterval,100) config.getInt(config.minXdrInterval,100)
); );
toN2KConverter= NMEA0183DataToN2K::create(&logger,&boatData,[](const tN2kMsg &msg)->bool{ toN2KConverter= NMEA0183DataToN2K::create(&logger,&boatData,[](const tN2kMsg &msg, int sourceId)->bool{
logger.logDebug(GwLog::DEBUG+2,"send N2K %ld",msg.PGN); logger.logDebug(GwLog::DEBUG+2,"send N2K %ld",msg.PGN);
handleN2kMessage(msg,N2KT_MSGOUT); handleN2kMessage(msg,sourceId,true);
return true; return true;
}, },
&xdrMappings, &xdrMappings,
@ -892,17 +711,8 @@ void setup() {
} }
NMEA2000.ExtendTransmitMessages(pgns); NMEA2000.ExtendTransmitMessages(pgns);
NMEA2000.ExtendReceiveMessages(nmea0183Converter->handledPgns()); NMEA2000.ExtendReceiveMessages(nmea0183Converter->handledPgns());
if (usbActisense->asBoolean()){
actisenseReader=new tActisenseReader();
usbStream=usbSerial->getStream(false);
actisenseReader->SetReadStream(usbStream);
actisenseReader->SetMsgHandler([](const tN2kMsg &msg){
countUSBIn.add(String(msg.PGN));
handleN2kMessage(msg,N2KT_MSGACT);
});
}
NMEA2000.SetMsgHandler([](const tN2kMsg &n2kMsg){ NMEA2000.SetMsgHandler([](const tN2kMsg &n2kMsg){
handleN2kMessage(n2kMsg,N2KT_MSGIN); handleN2kMessage(n2kMsg,N2K_CHANNEL_ID);
}); });
NMEA2000.Open(); NMEA2000.Open();
logger.logDebug(GwLog::LOG,"starting addon tasks"); logger.logDebug(GwLog::LOG,"starting addon tasks");
@ -920,49 +730,12 @@ void setup() {
} }
//***************************************************************************** //*****************************************************************************
void handleSendAndRead(bool handleRead){ void handleSendAndRead(bool handleRead){
socketServer.loop(handleRead); channels.allChannels([&](GwChannel *c){
usbSerial->loop(handleRead); c->loop(handleRead,true);
if (serial1) serial1->loop(handleRead); });
} }
class NMEAMessageReceiver : public GwMessageFetcher{
static const int bufferSize=GwBuffer::RX_BUFFER_SIZE+4;
uint8_t buffer[bufferSize];
uint8_t *writePointer=buffer;
public:
virtual bool handleBuffer(GwBuffer *gwbuffer){
size_t len=fetchMessageToBuffer(gwbuffer,buffer,bufferSize-4,'\n');
writePointer=buffer+len;
if (writePointer == buffer) return false;
uint8_t *p;
for (p=writePointer-1;p>=buffer && *p <= 0x20;p--){
*p=0;
}
if (p > buffer){
p++;
*p=0x0d;
p++;
*p=0x0a;
p++;
*p=0;
}
for (p=buffer; *p != 0 && p < writePointer && *p <= 0x20;p++){}
//very simple NMEA check
if (*p != '!' && *p != '$'){
logger.logDebug(GwLog::DEBUG,"unknown line [%d] - ignore: %s",id,(const char *)p);
}
else{
logger.logDebug(GwLog::DEBUG,"NMEA[%d]: %s",id,(const char *)p);
handleReceivedNmeaMessage((const char *)p,id);
//trigger sending to empty buffers
handleSendAndRead(false);
}
writePointer=buffer;
return true;
}
};
TimeMonitor monitor(20,0.2); TimeMonitor monitor(20,0.2);
NMEAMessageReceiver receiver;
unsigned long lastHeapReport=0; unsigned long lastHeapReport=0;
void loop() { void loop() {
monitor.reset(); monitor.reset();
@ -983,20 +756,18 @@ void loop() {
} }
} }
monitor.setTime(3); monitor.setTime(3);
//read sockets channels.allChannels([](GwChannel *c){
socketServer.loop(true,false); c->loop(true,false);
});
//reads
monitor.setTime(4); monitor.setTime(4);
//write sockets channels.allChannels([](GwChannel *c){
socketServer.loop(false,true); c->loop(false,true);
});
//writes
monitor.setTime(5); monitor.setTime(5);
usbSerial->loop(true);
monitor.setTime(6);
if (serial1) serial1->loop(true);
monitor.setTime(7);
handleSendAndRead(true);
monitor.setTime(8);
NMEA2000.ParseMessages(); NMEA2000.ParseMessages();
monitor.setTime(9); monitor.setTime(6);
int SourceAddress = NMEA2000.GetN2kSource(); int SourceAddress = NMEA2000.GetN2kSource();
if (SourceAddress != NodeAddress) { // Save potentially changed Source Address to NVS memory if (SourceAddress != NodeAddress) { // Save potentially changed Source Address to NVS memory
@ -1007,21 +778,36 @@ void loop() {
logger.logDebug(GwLog::LOG,"Address Change: New Address=%d\n", SourceAddress); logger.logDebug(GwLog::LOG,"Address Change: New Address=%d\n", SourceAddress);
} }
nmea0183Converter->loop(); nmea0183Converter->loop();
monitor.setTime(10); monitor.setTime(7);
//read channels //read channels
if (readTCP->asBoolean()) socketServer.readMessages(&receiver); channels.allChannels([](GwChannel *c){
monitor.setTime(11); c->readMessages([&](const char * buffer, int sourceId){
receiver.id=USB_CHANNEL_ID; channels.allChannels([&](GwChannel *oc){
if (! actisenseReader && readUsb->asBoolean()) usbSerial->readMessages(&receiver); oc->sendToClients(buffer,sourceId);
monitor.setTime(12); oc->loop(false,true);
receiver.id=SERIAL1_CHANNEL_ID; });
if (serial1 && serCanRead ) serial1->readMessages(&receiver); if (c->sendToN2K()){
monitor.setTime(13); if (strlen(buffer) > 6 && strncmp(buffer,"$PCDIN",6) == 0){
if (actisenseReader){ tN2kMsg n2kMsg;
actisenseReader->ParseMessages(); uint32_t timestamp;
} if (SeasmartToN2k(buffer,timestamp,n2kMsg)){
monitor.setTime(14); handleN2kMessage(n2kMsg,sourceId);
}
}
else{
toN2KConverter->parseAndSend(buffer, sourceId);
}
}
});
});
monitor.setTime(8);
channels.allChannels([](GwChannel *c){
c->parseActisense([](const tN2kMsg &msg,int source){
handleN2kMessage(msg,source);
});
});
monitor.setTime(9);
//handle message requests //handle message requests
GwMessage *msg=mainQueue.fetchMessage(0); GwMessage *msg=mainQueue.fetchMessage(0);
@ -1029,5 +815,5 @@ void loop() {
msg->process(); msg->process();
msg->unref(); msg->unref();
} }
monitor.setTime(15); monitor.setTime(10);
} }

6
tools/buildFlashtool.sh Executable file
View File

@ -0,0 +1,6 @@
#! /bin/sh
pdir=`dirname $0`
cd $pdir || exit 1
rm -rf flashtool/.idea
python3 -m zipapp flashtool -m flashtool:main

View File

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

BIN
tools/flashtool.pyz Normal file

Binary file not shown.

253
tools/flashtool/flashtool.py Executable file
View File

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

View File

@ -14,7 +14,7 @@ How to build the bundled flashtool.exe (on windows)
(2) pip install pyinstaller (2) pip install pyinstaller
(3) pip install pyserial (3) pip install pyserial
(4) in this directory: (4) in this directory:
pyinstaller -F flashtool.py pyinstaller --clean -F flashtool.py
will create flashtool.exe in dist will create flashtool.exe in dist

View File

@ -376,7 +376,7 @@
"type": "number", "type": "number",
"default": "10110", "default": "10110",
"description": "the TCP port we listen on", "description": "the TCP port we listen on",
"category": "TCP port" "category": "TCP server"
}, },
{ {
"name": "maxClients", "name": "maxClients",
@ -387,55 +387,128 @@
"min": 0, "min": 0,
"max": 6, "max": 6,
"description": "the number of clients we allow to connect to us", "description": "the number of clients we allow to connect to us",
"category": "TCP port" "category": "TCP server"
}, },
{ {
"name": "sendTCP", "name": "sendTCP",
"label": "NMEA to TCP", "label": "NMEA0183 out",
"type": "boolean", "type": "boolean",
"default": "true", "default": "true",
"description": "send out NMEA data to connected TCP clients", "description": "send out NMEA data to connected TCP clients",
"category": "TCP port" "category": "TCP server"
}, },
{ {
"name": "readTCP", "name": "readTCP",
"label": "NMEA from TCP", "label": "NMEA0183 in",
"type": "boolean", "type": "boolean",
"default": "true", "default": "true",
"description": "receive NMEA data from connected TCP clients", "description": "receive NMEA data from connected TCP clients",
"category": "TCP port" "category": "TCP server"
}, },
{ {
"name": "tcpToN2k", "name": "tcpToN2k",
"label": "TCP to NMEA2000", "label": "to NMEA2000",
"type": "boolean", "type": "boolean",
"default": "true", "default": "true",
"description": "convert NMEA0183 from TCP clients to NMEA2000", "description": "convert NMEA0183 from TCP clients to NMEA2000",
"category": "TCP port" "category": "TCP server"
}, },
{ {
"name": "tcpReadFilter", "name": "tcpReadFilter",
"label": "TCP read Filter", "label": "NMEA read Filter",
"type": "filter", "type": "filter",
"default": "", "default": "",
"description": "filter for NMEA0183 data when reading from TCP\nselect aison|aisoff, set a whitelist or a blacklist with NMEA sentences like RMC,RMB", "description": "filter for NMEA0183 data when reading from TCP\nselect aison|aisoff, set a whitelist or a blacklist with NMEA sentences like RMC,RMB",
"category": "TCP port" "category": "TCP server"
}, },
{ {
"name": "tcpWriteFilter", "name": "tcpWriteFilter",
"label": "TCP write Filter", "label": "NMEA write Filter",
"type": "filter", "type": "filter",
"default": "", "default": "",
"description": "filter for NMEA0183 data when writing to TCP\nselect aison|aisoff, set a whitelist or a blacklist with NMEA sentences like RMC,RMB", "description": "filter for NMEA0183 data when writing to TCP\nselect aison|aisoff, set a whitelist or a blacklist with NMEA sentences like RMC,RMB",
"category": "TCP port" "category": "TCP server"
}, },
{ {
"name": "sendSeasmart", "name": "sendSeasmart",
"label": "Seasmart to TCP", "label": "Seasmart out",
"type": "boolean", "type": "boolean",
"default": "false", "default": "false",
"description": "send NMEA2000 as seasmart to connected TCP clients", "description": "send NMEA2000 as seasmart to connected TCP clients",
"category": "TCP port" "category": "TCP server"
},
{
"name": "tclEnabled",
"label": "enable",
"type": "boolean",
"default": "false",
"description":"enable the TCP client",
"category":"TCP client"
},
{
"name": "remotePort",
"label": "remote port",
"type": "number",
"default": "10110",
"description": "the TCP port we connect to",
"category": "TCP client"
},
{
"name": "remoteAddress",
"label": "remote address",
"type": "string",
"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"
},
{
"name": "sendTCL",
"label": "NMEA0183 out",
"type": "boolean",
"default": "true",
"description": "send out NMEA data to remote TCP server",
"category": "TCP client"
},
{
"name": "readTCL",
"label": "NMEA0183 in",
"type": "boolean",
"default": "true",
"description": "receive NMEA data from remote TCP server",
"category": "TCP client"
},
{
"name": "tclToN2k",
"label": "to NMEA2000",
"type": "boolean",
"default": "true",
"description": "convert NMEA0183 from remote TCP server to NMEA2000",
"category": "TCP client"
},
{
"name": "tclReadFilter",
"label": "NMEA read Filter",
"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"
},
{
"name": "tclWriteFilter",
"label": "NMEA write Filter",
"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"
},
{
"name": "tclSeasmart",
"label": "Seasmart out",
"type": "boolean",
"default": "false",
"description": "send NMEA2000 as seasmart to remote TCP server",
"category": "TCP client"
}, },
{ {
"name": "wifiClient", "name": "wifiClient",

View File

@ -43,9 +43,17 @@
<span class="value" id="clientIP">---</span> <span class="value" id="clientIP">---</span>
</div> </div>
<div class="row"> <div class="row">
<span class="label"># TCP clients</span> <span class="label"># clients</span>
<span class="value" id="numClients">---</span> <span class="value" id="numClients">---</span>
</div> </div>
<div class="row">
<span class="label">TCP client connected</span>
<span class="value" id="clientCon">---</span>
</div>
<div class="row">
<span class="label">TCP client error</span>
<span class="value" id="clientErr">---</span>
</div>
</div> </div>
<button id="reset">Reset</button> <button id="reset">Reset</button>
</div> </div>

View File

@ -162,6 +162,14 @@ function checkAdminPass(v){
return checkApPass(v); return checkApPass(v);
} }
function checkIpAddress(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";
}
function checkXDR(v,allValues){ function checkXDR(v,allValues){
if (! v) return; if (! v) return;
let parts=v.split(','); let parts=v.split(',');
@ -306,12 +314,14 @@ function updateMsgDetails(key, details) {
let counters={ let counters={
count2Kin: 'NMEA2000 in', count2Kin: 'NMEA2000 in',
count2Kout: 'NMEA2000 out', count2Kout: 'NMEA2000 out',
countTCPin: 'TCP in', countTCPin: 'TCPserver in',
countTCPout: 'TCP out', countTCPout: 'TCPserver out',
countTCPClientin: 'TCPclient in',
countTCPClientout: 'TCPclient out',
countUSBin: 'USB in', countUSBin: 'USB in',
countUSBout: 'USB out', countUSBout: 'USB out',
countSerialIn: 'Serial in', countSERIn: 'Serial in',
countSerialOut: 'Serial out' countSEROut: 'Serial out'
} }
function showOverlay(text, isHtml) { function showOverlay(text, isHtml) {
let el = document.getElementById('overlayContent'); let el = document.getElementById('overlayContent');
@ -1360,7 +1370,9 @@ function sourceName(v){
if (v == 0) return "N2K"; if (v == 0) return "N2K";
if (v == 1) return "USB"; if (v == 1) return "USB";
if (v == 2) return "SER"; if (v == 2) return "SER";
if (v >= 3) return "TCP"; if (v == 3) return "TCPcl"
if (v >= 4 && v <= 20) return "TCPser";
if (v >= 200) return "USER";
return "---"; return "---";
} }
let lastSelectList=[]; let lastSelectList=[];