1
0
mirror of https://github.com/thooge/esp32-nmea2000-obp60.git synced 2025-12-13 22:13:07 +01:00

Merge branch 'wellenvogel:master' into master

This commit is contained in:
Norbert Walter
2024-11-20 14:22:19 +01:00
committed by GitHub
46 changed files with 2305 additions and 516 deletions

View File

@@ -6,7 +6,9 @@
#include "GWConfig.h"
#include "GwBoatData.h"
#include "GwXDRMappings.h"
#include "GwSynchronized.h"
#include <map>
#include <ESPAsyncWebServer.h>
class GwApi;
typedef void (*GwUserTaskFunction)(GwApi *);
//API to be used for additional tasks
@@ -95,6 +97,8 @@ class GwApi{
unsigned long ser2Tx=0;
unsigned long tcpSerRx=0;
unsigned long tcpSerTx=0;
unsigned long udpwTx=0;
unsigned long udprRx=0;
int tcpClients=0;
unsigned long tcpClRx=0;
unsigned long tcpClTx=0;
@@ -169,6 +173,20 @@ class GwApi{
virtual void remove(int idx){}
virtual TaskInterfaces * taskInterfaces()=0;
/**
* register handler for web URLs
* Please be aware that this handler function will always be called from a separate
* task. So you must ensure proper synchronization!
*/
using HandlerFunction=std::function<void(AsyncWebServerRequest *)>;
/**
* @param url: the url of that will trigger the handler.
* it will be prefixed with /api/user/<taskname>
* taskname is the name that you used in addUserTask
* @param handler: the handler function (see remark above about thread synchronization)
*/
virtual void registerRequestHandler(const String &url,HandlerFunction handler)=0;
/**
* only allowed during init methods
*/

View File

@@ -48,12 +48,36 @@ GwBoatItemBase::GwBoatItemBase(String name, String format, GwBoatItemBase::TOTyp
this->type = 0;
this->lastUpdateSource = -1;
}
void GwBoatItemBase::setInvalidTime(unsigned long it, bool force){
if (toType != TOType::user || force ){
invalidTime=it;
void GwBoatItemBase::setInvalidTime(GwConfigHandler *cfg){
if (toType != TOType::user){
unsigned long timeout=GwBoatItemBase::INVALID_TIME;
switch(getToType()){
case GwBoatItemBase::TOType::ais:
timeout=cfg->getInt(GwConfigDefinitions::timoAis);
break;
case GwBoatItemBase::TOType::def:
timeout=cfg->getInt(GwConfigDefinitions::timoDefault);
break;
case GwBoatItemBase::TOType::lng:
timeout=cfg->getInt(GwConfigDefinitions::timoLong);
break;
case GwBoatItemBase::TOType::sensor:
timeout=cfg->getInt(GwConfigDefinitions::timoSensor);
break;
case GwBoatItemBase::TOType::keep:
timeout=0;
break;
}
invalidTime=timeout;
}
}
size_t GwBoatItemBase::getJsonSize() { return JSON_OBJECT_SIZE(10); }
void GwBoatItemBase::GwBoatItemMap::add(const String &name,GwBoatItemBase *item){
boatData->setInvalidTime(item);
(*this)[name]=item;
}
#define STRING_SIZE 40
GwBoatItemBase::StringWriter::StringWriter()
{
@@ -127,7 +151,7 @@ GwBoatItem<T>::GwBoatItem(String name, String formatInfo, unsigned long invalidT
this->type = GwBoatItemTypes::getType(dummy);
if (map)
{
(*map)[name] = this;
map->add(name,this);
}
}
template <class T>
@@ -137,7 +161,7 @@ GwBoatItem<T>::GwBoatItem(String name, String formatInfo, GwBoatItemBase::TOType
this->type = GwBoatItemTypes::getType(dummy);
if (map)
{
(*map)[name] = this;
map->add(name,this);
}
}
@@ -322,28 +346,12 @@ void GwBoatDataSatList::toJsonDoc(GwJsonDocument *doc, unsigned long minTime)
GwBoatData::GwBoatData(GwLog *logger, GwConfigHandler *cfg)
{
this->logger = logger;
this->config = cfg;
}
void GwBoatData::begin(){
for (auto &&it : values){
unsigned long timeout=GwBoatItemBase::INVALID_TIME;
switch(it.second->getToType()){
case GwBoatItemBase::TOType::ais:
timeout=cfg->getInt(GwConfigDefinitions::timoAis);
break;
case GwBoatItemBase::TOType::def:
timeout=cfg->getInt(GwConfigDefinitions::timoDefault);
break;
case GwBoatItemBase::TOType::lng:
timeout=cfg->getInt(GwConfigDefinitions::timoLong);
break;
case GwBoatItemBase::TOType::sensor:
timeout=cfg->getInt(GwConfigDefinitions::timoSensor);
break;
case GwBoatItemBase::TOType::keep:
timeout=0;
break;
}
it.second->setInvalidTime(timeout);
it.second->setInvalidTime(config);
}
}
GwBoatData::~GwBoatData()
{
@@ -456,6 +464,10 @@ double GwBoatData::getDoubleValue(String name, double defaultv)
return defaultv;
return it->second->getDoubleValue();
}
void GwBoatData::setInvalidTime(GwBoatItemBase *item){
if (config != nullptr) item->setInvalidTime(config);
}
double formatCourse(double cv)
{
double rt = cv * 180.0 / M_PI;

View File

@@ -14,6 +14,8 @@
#define ROT_WA_FACTOR 60
class GwJsonDocument;
class GwBoatData;
class GwBoatItemBase{
public:
using TOType=enum{
@@ -56,7 +58,6 @@ class GwBoatItemBase{
GWSC(formatRot);
GWSC(formatDate);
GWSC(formatTime);
typedef std::map<String,GwBoatItemBase*> GwBoatItemMap;
protected:
int type;
unsigned long lastSet=0;
@@ -93,10 +94,15 @@ class GwBoatItemBase{
virtual double getDoubleValue()=0;
String getName(){return name;}
const String & getFormat() const{return format;}
virtual void setInvalidTime(unsigned long it, bool force=true);
virtual void setInvalidTime(GwConfigHandler *cfg);
TOType getToType(){return toType;}
class GwBoatItemMap : public std::map<String,GwBoatItemBase*>{
GwBoatData *boatData;
public:
GwBoatItemMap(GwBoatData *bd):boatData(bd){}
void add(const String &name,GwBoatItemBase *item);
};
};
class GwBoatData;
template<class T> class GwBoatItem : public GwBoatItemBase{
protected:
T data;
@@ -186,8 +192,9 @@ public:
clazz *name=new clazz(#name,GwBoatItemBase::fmt,toType,&values) ;
class GwBoatData{
private:
GwLog *logger;
GwBoatItemBase::GwBoatItemMap values;
GwLog *logger=nullptr;
GwConfigHandler *config=nullptr;
GwBoatItemBase::GwBoatItemMap values{this};
public:
GWBOATDATA(double,COG,formatCourse) // course over ground
@@ -231,9 +238,11 @@ class GwBoatData{
public:
GwBoatData(GwLog *logger, GwConfigHandler *cfg);
~GwBoatData();
void begin();
template<class T> GwBoatItem<T> *getOrCreate(T initial,GwBoatItemNameProvider *provider);
template<class T> bool update(T value,int source,GwBoatItemNameProvider *provider);
template<class T> T getDataWithDefault(T defaultv, GwBoatItemNameProvider *provider);
void setInvalidTime(GwBoatItemBase *item);
bool isValid(String name);
double getDoubleValue(String name,double defaultv);
GwBoatItemBase *getBase(String name);

View File

@@ -58,8 +58,6 @@ GwChannel::GwChannel(GwLog *logger,
this->name=name;
this->sourceId=sourceId;
this->maxSourceId=maxSourceId;
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;
@@ -100,6 +98,12 @@ void GwChannel::begin(
actisenseReader->SetReadStream(channelStream);
}
}
if (nmeaIn || readActisense){
this->countIn=new GwCounter<String>(String("count")+name+String("in"));
}
if (nmeaOut || seaSmartOut || writeActisense){
this->countOut=new GwCounter<String>(String("count")+name+String("out"));
}
}
void GwChannel::setImpl(GwChannelInterface *impl){
this->impl=impl;
@@ -135,10 +139,10 @@ void GwChannel::updateCounter(const char *msg, bool out)
}
if (key[0] == 0) return;
if (out){
countOut->add(key);
if (countOut) countOut->add(key);
}
else{
countIn->add(key);
if (countIn) countIn->add(key);
}
}
@@ -209,7 +213,7 @@ void GwChannel::parseActisense(N2kHandler handler){
tN2kMsg N2kMsg;
while (actisenseReader->GetMessageFromStream(N2kMsg)) {
countIn->add(String(N2kMsg.PGN));
if(countIn) countIn->add(String(N2kMsg.PGN));
handler(N2kMsg,sourceId);
}
}
@@ -218,14 +222,23 @@ void GwChannel::sendActisense(const tN2kMsg &msg, int sourceId){
if (!enabled || ! impl || ! writeActisense || ! channelStream) return;
//currently actisense only for channels with a single source id
//so we can check it here
if (isOwnSource(sourceId)) return;
countOut->add(String(msg.PGN));
if (maxSourceId < 0 && this->sourceId == sourceId) return;
if (sourceId >= this->sourceId && sourceId <= maxSourceId) return;
if(countOut) countOut->add(String(msg.PGN));
msg.SendInActisenseFormat(channelStream);
}
bool GwChannel::isOwnSource(int id){
if (maxSourceId < 0) return id == sourceId;
else return (id >= sourceId && id <= maxSourceId);
bool GwChannel::overlaps(const GwChannel *other) const{
if (maxSourceId < 0){
if (other->maxSourceId < 0) return sourceId == other->sourceId;
return (other->sourceId <= sourceId && other->maxSourceId >= sourceId);
}
if (other->maxSourceId < 0){
return other->sourceId >= sourceId && other->sourceId <= maxSourceId;
}
if (other->maxSourceId < sourceId) return false;
if (other->sourceId > maxSourceId) return false;
return true;
}
unsigned long GwChannel::countRx(){

View File

@@ -50,7 +50,7 @@ class GwChannel{
);
void setImpl(GwChannelInterface *impl);
bool isOwnSource(int id);
bool overlaps(const GwChannel *) const;
void enable(bool enabled){
this->enabled=enabled;
}
@@ -73,5 +73,11 @@ class GwChannel{
void sendActisense(const tN2kMsg &msg, int sourceId);
unsigned long countRx();
unsigned long countTx();
bool isOwnSource(int source){
if (maxSourceId < 0) return source == sourceId;
return (source >= sourceId && source <= maxSourceId);
}
String getMode(){return impl->getMode();}
int getMinId(){return sourceId;};
};

View File

@@ -6,4 +6,5 @@ class GwChannelInterface{
virtual void readMessages(GwMessageFetcher *writer)=0;
virtual size_t sendToClients(const char *buffer, int sourceId, bool partial=false)=0;
virtual Stream * getStream(bool partialWrites){ return NULL;}
virtual String getMode(){return "UNKNOWN";}
};

View File

@@ -6,6 +6,8 @@
#include "GwSocketServer.h"
#include "GwSerial.h"
#include "GwTcpClient.h"
#include "GwUdpWriter.h"
#include "GwUdpReader.h"
class SerInit{
public:
int serial=-1;
@@ -18,108 +20,110 @@ class SerInit{
};
std::vector<SerInit> serialInits;
static int typeFromMode(const char *mode){
if (strcmp(mode,"UNI") == 0) return GWSERIAL_TYPE_UNI;
if (strcmp(mode,"BI") == 0) return GWSERIAL_TYPE_BI;
if (strcmp(mode,"RX") == 0) return GWSERIAL_TYPE_RX;
if (strcmp(mode,"TX") == 0) return GWSERIAL_TYPE_TX;
return GWSERIAL_TYPE_UNK;
}
#define CFG_SERIAL(ser,...) \
__MSG("serial config " #ser); \
static GwInitializer<SerInit> __serial ## ser ## _init \
(serialInits,SerInit(ser,__VA_ARGS__));
#ifdef _GWI_SERIAL1
CFG_SERIAL(1,_GWI_SERIAL1)
CFG_SERIAL(SERIAL1_CHANNEL_ID,_GWI_SERIAL1)
#endif
#ifdef _GWI_SERIAL2
CFG_SERIAL(2,_GWI_SERIAL2)
CFG_SERIAL(SERIAL2_CHANNEL_ID,_GWI_SERIAL2)
#endif
class GwSerialLog : public GwLogWriter
{
static const size_t bufferSize = 4096;
char *logBuffer = NULL;
int wp = 0;
GwSerial *writer;
bool disabled = false;
long flushTimeout=200;
public:
GwSerialLog(GwSerial *writer, bool disabled,long flushTimeout=200)
// handle separate defines
// serial 1
#ifndef GWSERIAL_TX
#define GWSERIAL_TX -1
#endif
#ifndef GWSERIAL_RX
#define GWSERIAL_RX -1
#endif
#ifdef GWSERIAL_TYPE
CFG_SERIAL(SERIAL1_CHANNEL_ID, GWSERIAL_RX, GWSERIAL_TX, GWSERIAL_TYPE)
#else
#ifdef GWSERIAL_MODE
CFG_SERIAL(SERIAL1_CHANNEL_ID, GWSERIAL_RX, GWSERIAL_TX, typeFromMode(GWSERIAL_MODE))
#endif
#endif
// serial 2
#ifndef GWSERIAL2_TX
#define GWSERIAL2_TX -1
#endif
#ifndef GWSERIAL2_RX
#define GWSERIAL2_RX -1
#endif
#ifdef GWSERIAL2_TYPE
CFG_SERIAL(SERIAL2_CHANNEL_ID, GWSERIAL2_RX, GWSERIAL2_TX, GWSERIAL2_TYPE)
#else
#ifdef GWSERIAL2_MODE
CFG_SERIAL(SERIAL2_CHANNEL_ID, GWSERIAL2_RX, GWSERIAL2_TX, typeFromMode(GWSERIAL2_MODE))
#endif
#endif
class GwSerialLog : public GwLogWriter
{
this->writer = writer;
this->disabled = disabled;
this->flushTimeout=flushTimeout;
logBuffer = new char[bufferSize];
wp = 0;
}
virtual ~GwSerialLog() {}
virtual void write(const char *data)
{
if (disabled)
return;
int len = strlen(data);
if ((wp + len) >= (bufferSize - 1))
return;
strncpy(logBuffer + wp, data, len);
wp += len;
logBuffer[wp] = 0;
}
virtual void flush()
{
size_t handled = 0;
if (!disabled)
static const size_t bufferSize = 4096;
char *logBuffer = NULL;
int wp = 0;
GwSerial *writer;
bool disabled = false;
public:
GwSerialLog(GwSerial *writer, bool disabled)
{
while (handled < wp)
{
if ( !writer->flush(flushTimeout)) break;
size_t rt = writer->sendToClients(logBuffer + handled, -1, true);
handled += rt;
}
if (handled < wp){
if (handled > 0){
memmove(logBuffer,logBuffer+handled,wp-handled);
wp-=handled;
logBuffer[wp]=0;
}
this->writer = writer;
this->disabled = disabled;
logBuffer = new char[bufferSize];
wp = 0;
}
virtual ~GwSerialLog() {}
virtual void write(const char *data)
{
if (disabled)
return;
int len = strlen(data);
if ((wp + len) >= (bufferSize - 1))
return;
strncpy(logBuffer + wp, data, len);
wp += len;
logBuffer[wp] = 0;
}
virtual void flush()
{
size_t handled = 0;
if (!disabled)
{
while (handled < wp)
{
if (!writer->flush())
break;
size_t rt = writer->sendToClients(logBuffer + handled, -1, true);
handled += rt;
}
if (handled < wp)
{
if (handled > 0)
{
memmove(logBuffer, logBuffer + handled, wp - handled);
wp -= handled;
logBuffer[wp] = 0;
}
return;
}
}
wp = 0;
logBuffer[0] = 0;
}
wp = 0;
logBuffer[0] = 0;
}
};
template<typename T>
class SerialWrapper : public GwChannelList::SerialWrapperBase{
private:
template<class C>
void beginImpl(C *s,unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1){}
void beginImpl(HardwareSerial *s,unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1){
s->begin(baud,config,rxPin,txPin);
}
template<class C>
void setError(C* s, GwLog *logger){}
void setError(HardwareSerial *s,GwLog *logger){
LOG_DEBUG(GwLog::LOG,"enable serial errors for channel %d",id);
s->onReceiveError([logger,this](hardwareSerial_error_t err){
LOG_DEBUG(GwLog::ERROR,"serial error on id %d: %d",this->id,(int)err);
});
}
#if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3
void beginImpl(HWCDC *s,unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1){
s->begin(baud);
}
#endif
T *serial;
int id;
public:
SerialWrapper(T* s,int i):serial(s),id(i){}
virtual void begin(GwLog* logger,unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1) override{
beginImpl(serial,baud,config,rxPin,txPin);
setError(serial,logger);
};
virtual Stream *getStream() override{
return serial;
}
virtual int getId() override{
return id;
}
};
GwChannelList::GwChannelList(GwLog *logger, GwConfigHandler *config){
this->logger=logger;
@@ -139,10 +143,35 @@ typedef struct {
const char *toN2K;
const char *readF;
const char *writeF;
const char *preventLog;
const char *readAct;
const char *writeAct;
const char *sendSeasmart;
const char *name;
} SerialParam;
int maxId;
size_t rxstatus;
size_t txstatus;
} ChannelParam;
static SerialParam serialParameters[]={
static ChannelParam channelParameters[]={
{
.id=USB_CHANNEL_ID,
.baud=GwConfigDefinitions::usbBaud,
.receive=GwConfigDefinitions::receiveUsb,
.send=GwConfigDefinitions::sendUsb,
.direction="",
.toN2K=GwConfigDefinitions::usbToN2k,
.readF=GwConfigDefinitions::usbReadFilter,
.writeF=GwConfigDefinitions::usbWriteFilter,
.preventLog=GwConfigDefinitions::usbActisense,
.readAct=GwConfigDefinitions::usbActisense,
.writeAct=GwConfigDefinitions::usbActSend,
.sendSeasmart="",
.name="USB",
.maxId=-1,
.rxstatus=offsetof(GwApi::Status,GwApi::Status::usbRx),
.txstatus=offsetof(GwApi::Status,GwApi::Status::usbTx)
},
{
.id=SERIAL1_CHANNEL_ID,
.baud=GwConfigDefinitions::serialBaud,
@@ -152,7 +181,14 @@ static SerialParam serialParameters[]={
.toN2K=GwConfigDefinitions::serialToN2k,
.readF=GwConfigDefinitions::serialReadF,
.writeF=GwConfigDefinitions::serialWriteF,
.name="Serial"
.preventLog="",
.readAct="",
.writeAct="",
.sendSeasmart="",
.name="Serial",
.maxId=-1,
.rxstatus=offsetof(GwApi::Status,GwApi::Status::serRx),
.txstatus=offsetof(GwApi::Status,GwApi::Status::serTx)
},
{
.id=SERIAL2_CHANNEL_ID,
@@ -163,81 +199,162 @@ static SerialParam serialParameters[]={
.toN2K=GwConfigDefinitions::serial2ToN2k,
.readF=GwConfigDefinitions::serial2ReadF,
.writeF=GwConfigDefinitions::serial2WriteF,
.name="Serial2"
.preventLog="",
.readAct="",
.writeAct="",
.sendSeasmart="",
.name="Serial2",
.maxId=-1,
.rxstatus=offsetof(GwApi::Status,GwApi::Status::ser2Rx),
.txstatus=offsetof(GwApi::Status,GwApi::Status::ser2Tx)
},
{
.id=MIN_TCP_CHANNEL_ID,
.baud="",
.receive=GwConfigDefinitions::readTCP,
.send=GwConfigDefinitions::sendTCP,
.direction="",
.toN2K=GwConfigDefinitions::tcpToN2k,
.readF=GwConfigDefinitions::tcpReadFilter,
.writeF=GwConfigDefinitions::tcpWriteFilter,
.preventLog="",
.readAct="",
.writeAct="",
.sendSeasmart=GwConfigDefinitions::sendSeasmart,
.name="TCPServer",
.maxId=MIN_TCP_CHANNEL_ID+10,
.rxstatus=offsetof(GwApi::Status,GwApi::Status::tcpSerRx),
.txstatus=offsetof(GwApi::Status,GwApi::Status::tcpSerTx)
},
{
.id=TCP_CLIENT_CHANNEL_ID,
.baud="",
.receive=GwConfigDefinitions::readTCL,
.send=GwConfigDefinitions::sendTCL,
.direction="",
.toN2K=GwConfigDefinitions::tclToN2k,
.readF=GwConfigDefinitions::tclReadFilter,
.writeF=GwConfigDefinitions::tclWriteFilter,
.preventLog="",
.readAct="",
.writeAct="",
.sendSeasmart=GwConfigDefinitions::tclSeasmart,
.name="TCPClient",
.maxId=-1,
.rxstatus=offsetof(GwApi::Status,GwApi::Status::tcpClRx),
.txstatus=offsetof(GwApi::Status,GwApi::Status::tcpClTx)
},
{
.id=UDPW_CHANNEL_ID,
.baud="",
.receive="",
.send=GwConfigDefinitions::udpwEnabled,
.direction="",
.toN2K="",
.readF="",
.writeF=GwConfigDefinitions::udpwWriteFilter,
.preventLog="",
.readAct="",
.writeAct="",
.sendSeasmart=GwConfigDefinitions::udpwSeasmart,
.name="UDPWriter",
.maxId=-1,
.rxstatus=0,
.txstatus=offsetof(GwApi::Status,GwApi::Status::udpwTx)
},
{
.id=UDPR_CHANNEL_ID,
.baud="",
.receive=GwConfigDefinitions::udprEnabled,
.send="",
.direction="",
.toN2K=GwConfigDefinitions::udprToN2k,
.readF=GwConfigDefinitions::udprReadFilter,
.writeF="",
.preventLog="",
.readAct="",
.writeAct="",
.sendSeasmart="",
.name="UDPReader",
.maxId=-1,
.rxstatus=offsetof(GwApi::Status,GwApi::Status::udprRx),
.txstatus=0
}
};
static SerialParam *getSerialParam(int id){
for (size_t idx=0;idx<sizeof(serialParameters)/sizeof(SerialParam*);idx++){
if (serialParameters[idx].id == id) return &serialParameters[idx];
}
return nullptr;
}
void GwChannelList::addSerial(int id, int rx, int tx, int type){
if (id == 1){
addSerial(new SerialWrapper<decltype(Serial1)>(&Serial1,SERIAL1_CHANNEL_ID),type,rx,tx);
return;
}
if (id == 2){
addSerial(new SerialWrapper<decltype(Serial2)>(&Serial2,SERIAL2_CHANNEL_ID),type,rx,tx);
return;
}
LOG_DEBUG(GwLog::ERROR,"invalid serial config with id %d",id);
}
void GwChannelList::addSerial(GwChannelList::SerialWrapperBase *stream,int type,int rx,int tx){
const char *mode=nullptr;
switch (type)
{
case GWSERIAL_TYPE_UNI:
mode="UNI";
break;
case GWSERIAL_TYPE_BI:
mode="BI";
break;
case GWSERIAL_TYPE_RX:
mode="RX";
break;
case GWSERIAL_TYPE_TX:
mode="TX";
break;
}
if (mode == nullptr) {
LOG_DEBUG(GwLog::ERROR,"unknown serial type %d",type);
return;
}
addSerial(stream,mode,rx,tx);
}
void GwChannelList::addSerial(GwChannelList::SerialWrapperBase *serialStream,const String &mode,int rx,int tx){
int id=serialStream->getId();
for (auto &&it:theChannels){
if (it->isOwnSource(id)){
LOG_DEBUG(GwLog::ERROR,"trying to re-add serial id=%d, ignoring",id);
return;
template<typename T>
GwSerial* createSerial(GwLog *logger, T* s,int id, bool canRead=true){
return new GwSerialImpl<T>(logger,s,id,canRead);
}
static ChannelParam * findChannelParam(int id){
ChannelParam *param=nullptr;
for (auto && p: channelParameters){
if (id == p.id){
param=&p;
break;
}
}
SerialParam *param=getSerialParam(id);
return param;
}
static GwSerial * createSerialImpl(GwConfigHandler *config,GwLog *logger, int idx,int rx,int tx, bool setLog=false){
LOG_DEBUG(GwLog::DEBUG,"create serial: channel=%d, rx=%d,tx=%d",
idx,rx,tx);
ChannelParam *param=findChannelParam(idx);
if (param == nullptr){
logger->logDebug(GwLog::ERROR,"trying to set up an unknown serial channel: %d",id);
return;
LOG_DEBUG(GwLog::ERROR,"invalid serial channel id %d",idx);
return nullptr;
}
if (rx < 0 && tx < 0){
logger->logDebug(GwLog::ERROR,"useless config for serial %d: both rx/tx undefined");
return;
GwSerial *serialStream=nullptr;
GwLog *streamLog=setLog?nullptr:logger;
switch(param->id){
case USB_CHANNEL_ID:
serialStream=createSerial(streamLog,&USBSerial,param->id);
break;
case SERIAL1_CHANNEL_ID:
serialStream=createSerial(streamLog,&Serial1,param->id);
break;
case SERIAL2_CHANNEL_ID:
serialStream=createSerial(streamLog,&Serial2,param->id);
break;
}
if (serialStream == nullptr){
LOG_DEBUG(GwLog::ERROR,"invalid serial config with id %d",param->id);
return nullptr;
}
serialStream->begin(config->getInt(param->baud,115200),SERIAL_8N1,rx,tx);
if (setLog){
logger->setWriter(new GwSerialLog(serialStream,config->getBool(param->preventLog,false)));
logger->prefix="GWSERIAL:";
}
return serialStream;
}
static GwChannel * createChannel(GwLog *logger, GwConfigHandler *config, int id,GwChannelInterface *impl, int type=GWSERIAL_TYPE_BI){
ChannelParam *param=findChannelParam(id);
if (param == nullptr){
LOG_DEBUG(GwLog::ERROR,"invalid channel id %d",id);
return nullptr;
}
modes[id]=String(mode);
bool canRead=false;
bool canWrite=false;
if (mode == "BI"){
bool validType=false;
if (type == GWSERIAL_TYPE_BI){
canRead=config->getBool(param->receive);
canWrite=config->getBool(param->send);
validType=true;
}
if (mode == "TX"){
if (type == GWSERIAL_TYPE_TX){
canWrite=true;
validType=true;
}
if (mode == "RX"){
if (type == GWSERIAL_TYPE_RX){
canRead=true;
validType=true;
}
if (mode == "UNI"){
if (type == GWSERIAL_TYPE_UNI ){
String cfgMode=config->getString(param->direction);
if (cfgMode == "receive"){
canRead=true;
@@ -245,138 +362,102 @@ void GwChannelList::addSerial(GwChannelList::SerialWrapperBase *serialStream,con
if (cfgMode == "send"){
canWrite=true;
}
validType=true;
}
if (rx < 0) canRead=false;
if (tx < 0) canWrite=false;
LOG_DEBUG(GwLog::DEBUG,"serial set up: mode=%s,rx=%d,canRead=%d,tx=%d,canWrite=%d",
mode.c_str(),rx,(int)canRead,tx,(int)canWrite);
serialStream->begin(logger,config->getInt(param->baud,115200),SERIAL_8N1,rx,tx);
GwSerial *serial = new GwSerial(logger, serialStream->getStream(), id, canRead);
LOG_DEBUG(GwLog::LOG, "starting serial %d ", id);
GwChannel *channel = new GwChannel(logger, param->name, id);
channel->setImpl(serial);
if (! validType){
LOG_DEBUG(GwLog::ERROR,"invalid type for channel %d: %d",param->id,type);
return nullptr;
}
GwChannel *channel = new GwChannel(logger, param->name,param->id,param->maxId);
bool sendSeaSmart=config->getBool(param->sendSeasmart);
bool readAct=config->getBool(param->readAct);
bool writeAct=config->getBool(param->writeAct);
channel->setImpl(impl);
channel->begin(
canRead || canWrite,
canRead || canWrite || readAct || writeAct|| sendSeaSmart,
canWrite,
canRead,
config->getString(param->readF),
config->getString(param->writeF),
false,
sendSeaSmart,
config->getBool(param->toN2K),
false,
false);
LOG_DEBUG(GwLog::LOG, "%s", channel->toString().c_str());
readAct,
writeAct);
LOG_INFO("created channel %s",channel->toString().c_str());
return channel;
}
void GwChannelList::addChannel(GwChannel * channel){
if (channel == nullptr) return;
for (auto &&it:theChannels){
if (it->overlaps(channel)){
LOG_DEBUG(GwLog::ERROR,"trying to add channel with overlapping ids %s (%s), ignoring",
channel->toString().c_str(),
it->toString().c_str());
return;
}
}
LOG_INFO("adding channel %s", channel->toString().c_str());
theChannels.push_back(channel);
}
void GwChannelList::preinit(){
for (auto &&init:serialInits){
LOG_INFO("serial config found for %d",init.serial);
if (init.fixedBaud >= 0){
switch(init.serial){
case 1:
{
LOG_DEBUG(GwLog::DEBUG,"setting fixed baud %d for serial",init.fixedBaud);
config->setValue(GwConfigDefinitions::serialBaud,String(init.fixedBaud),GwConfigInterface::READONLY);
}
break;
case 2:
{
LOG_DEBUG(GwLog::DEBUG,"setting fixed baud %d for serial2",init.fixedBaud);
config->setValue(GwConfigDefinitions::serial2Baud,String(init.fixedBaud),GwConfigInterface::READONLY);
}
break;
default:
LOG_DEBUG(GwLog::ERROR,"invalid serial definition %d found",init.serial)
ChannelParam *param=findChannelParam(init.serial);
if (! param){
LOG_ERROR("invalid serial definition %d found",init.serial)
return;
}
LOG_DEBUG(GwLog::DEBUG,"setting fixed baud %d for serial %d",init.fixedBaud,init.serial);
config->setValue(param->baud,String(init.fixedBaud),GwConfigInterface::READONLY);
}
}
}
template<typename S>
long getFlushTimeout(S &s){
return 200;
}
template<>
long getFlushTimeout(HardwareSerial &s){
return 2000;
}
#ifndef GWUSB_TX
#define GWUSB_TX -1
#endif
#ifndef GWUSB_RX
#define GWUSB_RX -1
#endif
void GwChannelList::begin(bool fallbackSerial){
LOG_DEBUG(GwLog::DEBUG,"GwChannelList::begin");
GwChannel *channel=NULL;
//usb
if (! fallbackSerial){
GwSerial *usb=new GwSerial(NULL,&USBSerial,USB_CHANNEL_ID);
USBSerial.begin(config->getInt(config->usbBaud));
logger->setWriter(new GwSerialLog(usb,config->getBool(config->usbActisense),getFlushTimeout(USBSerial)));
logger->prefix="GWSERIAL:";
channel=new GwChannel(logger,"USB",USB_CHANNEL_ID);
channel->setImpl(usb);
channel->begin(true,
config->getBool(config->sendUsb),
config->getBool(config->receiveUsb),
config->getString(config->usbReadFilter),
config->getString(config->usbWriteFilter),
false,
config->getBool(config->usbToN2k),
config->getBool(config->usbActisense),
config->getBool(config->usbActSend)
);
theChannels.push_back(channel);
LOG_DEBUG(GwLog::LOG,"%s",channel->toString().c_str());
GwSerial *usbSerial=createSerialImpl(config, logger,USB_CHANNEL_ID,GWUSB_RX,GWUSB_TX,true);
if (usbSerial != nullptr){
GwChannel *usbChannel=createChannel(logger,config,USB_CHANNEL_ID,usbSerial,GWSERIAL_TYPE_BI);
if (usbChannel != nullptr){
addChannel(usbChannel);
}
else{
delete usbSerial;
}
}
}
//TCP server
sockets=new GwSocketServer(config,logger,MIN_TCP_CHANNEL_ID);
sockets->begin();
channel=new GwChannel(logger,"TCPserver",MIN_TCP_CHANNEL_ID,MIN_TCP_CHANNEL_ID+10);
channel->setImpl(sockets);
channel->begin(
true,
config->getBool(config->sendTCP),
config->getBool(config->readTCP),
config->getString(config->tcpReadFilter),
config->getString(config->tcpWriteFilter),
config->getBool(config->sendSeasmart),
config->getBool(config->tcpToN2k),
false,
false
);
LOG_DEBUG(GwLog::LOG,"%s",channel->toString().c_str());
theChannels.push_back(channel);
addChannel(createChannel(logger,config,MIN_TCP_CHANNEL_ID,sockets));
//new serial config handling
for (auto &&init:serialInits){
addSerial(init.serial,init.rx,init.tx,init.mode);
LOG_INFO("creating serial channel %d, rx=%d,tx=%d,type=%d",init.serial,init.rx,init.tx,init.mode);
GwSerial *ser=createSerialImpl(config,logger,init.serial,init.rx,init.tx);
if (ser != nullptr){
channel=createChannel(logger,config,init.serial,ser,init.mode);
if (channel != nullptr){
addChannel(channel);
}
else{
delete ser;
}
}
}
//handle separate defines
//serial 1
#ifndef GWSERIAL_TX
#define GWSERIAL_TX -1
#endif
#ifndef GWSERIAL_RX
#define GWSERIAL_RX -1
#endif
#ifdef GWSERIAL_TYPE
addSerial(new SerialWrapper<decltype(Serial1)>(&Serial1,SERIAL1_CHANNEL_ID),GWSERIAL_TYPE,GWSERIAL_RX,GWSERIAL_TX);
#else
#ifdef GWSERIAL_MODE
addSerial(new SerialWrapper<decltype(Serial1)>(&Serial1,SERIAL1_CHANNEL_ID),GWSERIAL_MODE,GWSERIAL_RX,GWSERIAL_TX);
#endif
#endif
//serial 2
#ifndef GWSERIAL2_TX
#define GWSERIAL2_TX -1
#endif
#ifndef GWSERIAL2_RX
#define GWSERIAL2_RX -1
#endif
#ifdef GWSERIAL2_TYPE
addSerial(new SerialWrapper<decltype(Serial2)>(&Serial2,SERIAL2_CHANNEL_ID),GWSERIAL2_TYPE,GWSERIAL2_RX,GWSERIAL2_TX);
#else
#ifdef GWSERIAL2_MODE
addSerial(new SerialWrapper<decltype(Serial2)>(&Serial2,SERIAL2_CHANNEL_ID),GWSERIAL2_MODE,GWSERIAL2_RX,GWSERIAL2_TX);
#endif
#endif
//tcp client
bool tclEnabled=config->getBool(config->tclEnabled);
channel=new GwChannel(logger,"TCPClient",TCP_CLIENT_CHANNEL_ID);
if (tclEnabled){
client=new GwTcpClient(logger);
client->begin(TCP_CLIENT_CHANNEL_ID,
@@ -384,26 +465,27 @@ void GwChannelList::begin(bool fallbackSerial){
config->getInt(config->remotePort),
config->getBool(config->readTCL)
);
channel->setImpl(client);
}
channel->begin(
tclEnabled,
config->getBool(config->sendTCL),
config->getBool(config->readTCL),
config->getString(config->tclReadFilter),
config->getString(config->tclReadFilter),
config->getBool(config->tclSeasmart),
config->getBool(config->tclToN2k),
false,
false
);
theChannels.push_back(channel);
LOG_DEBUG(GwLog::LOG,"%s",channel->toString().c_str());
addChannel(createChannel(logger,config,TCP_CLIENT_CHANNEL_ID,client));
//udp writer
if (config->getBool(GwConfigDefinitions::udpwEnabled)){
GwUdpWriter *writer=new GwUdpWriter(config,logger,UDPW_CHANNEL_ID);
writer->begin();
addChannel(createChannel(logger,config,UDPW_CHANNEL_ID,writer));
}
//udp reader
if (config->getBool(GwConfigDefinitions::udprEnabled)){
GwUdpReader *reader=new GwUdpReader(config,logger,UDPR_CHANNEL_ID);
reader->begin();
addChannel(createChannel(logger,config,UDPR_CHANNEL_ID,reader));
}
logger->flush();
}
String GwChannelList::getMode(int id){
auto it=modes.find(id);
if (it != modes.end()) return it->second;
for (auto && c: theChannels){
if (c->isOwnSource(id)) return c->getMode();
}
return "UNKNOWN";
}
int GwChannelList::getJsonSize(){
@@ -428,36 +510,28 @@ void GwChannelList::toJson(GwJsonDocument &doc){
});
}
GwChannel *GwChannelList::getChannelById(int sourceId){
for (auto it=theChannels.begin();it != theChannels.end();it++){
if ((*it)->isOwnSource(sourceId)) return *it;
for (auto && it: theChannels){
if (it->isOwnSource(sourceId)) return it;
}
return NULL;
}
/**
* slightly tricky generic setter for the API status
* we expect all values to be unsigned long
* the offsets are always offsetof(GwApi::Status,GwApi::Status::xxx)
*/
static void setStatus(GwApi::Status *status,size_t offset,unsigned long v){
if (offset == 0) return;
*((unsigned long *)(((unsigned char *)status)+offset))=v;
}
void GwChannelList::fillStatus(GwApi::Status &status){
GwChannel *channel=getChannelById(USB_CHANNEL_ID);
if (channel){
status.usbRx=channel->countRx();
status.usbTx=channel->countTx();
}
channel=getChannelById(SERIAL1_CHANNEL_ID);
if (channel){
status.serRx=channel->countRx();
status.serTx=channel->countTx();
}
channel=getChannelById(SERIAL2_CHANNEL_ID);
if (channel){
status.ser2Rx=channel->countRx();
status.ser2Tx=channel->countTx();
}
channel=getChannelById(MIN_TCP_CHANNEL_ID);
if (channel){
status.tcpSerRx=channel->countRx();
status.tcpSerTx=channel->countTx();
}
channel=getChannelById(TCP_CLIENT_CHANNEL_ID);
if (channel){
status.tcpClRx=channel->countRx();
status.tcpClTx=channel->countTx();
for (auto && channel: theChannels){
ChannelParam *param=findChannelParam(channel->getMinId());
if (param != nullptr){
setStatus(&status,param->rxstatus,channel->countRx());
setStatus(&status,param->txstatus,channel->countTx());
}
}
}

View File

@@ -8,6 +8,7 @@
#include "GWConfig.h"
#include "GwJsonDocument.h"
#include "GwApi.h"
#include "GwSerial.h"
#include <HardwareSerial.h>
//NMEA message channels
@@ -17,29 +18,22 @@
#define SERIAL2_CHANNEL_ID 3
#define TCP_CLIENT_CHANNEL_ID 4
#define MIN_TCP_CHANNEL_ID 5
#define UDPW_CHANNEL_ID 20
#define UDPR_CHANNEL_ID 21
#define MIN_USER_TASK 200
class GwSocketServer;
class GwTcpClient;
class GwChannelList{
private:
class SerialWrapperBase{
public:
virtual void begin(GwLog* logger,unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1)=0;
virtual Stream *getStream()=0;
virtual int getId()=0;
};
GwLog *logger;
GwConfigHandler *config;
typedef std::vector<GwChannel *> ChannelList;
ChannelList theChannels;
std::map<int,String> modes;
GwSocketServer *sockets;
GwTcpClient *client;
void addSerial(SerialWrapperBase *stream,const String &mode,int rx,int tx);
void addSerial(SerialWrapperBase *stream,int type,int rx,int tx);
public:
void addSerial(int id, int rx, int tx, int type);
void addChannel(GwChannel *);
GwChannelList(GwLog *logger, GwConfigHandler *config);
typedef std::function<void(GwChannel *)> ChannelAction;
void allChannels(ChannelAction action);

View File

@@ -7,6 +7,7 @@
#include <vector>
#include "N2kMessages.h"
#include "GwXdrTypeMappings.h"
/**
* INVALID!!! - the next interface declaration will not work
* as it is not in the correct header file
@@ -144,6 +145,26 @@ String formatValue(GwApi::BoatValue *value){
return String(buffer);
}
class ExampleWebData{
SemaphoreHandle_t lock;
int data=0;
public:
ExampleWebData(){
lock=xSemaphoreCreateMutex();
}
~ExampleWebData(){
vSemaphoreDelete(lock);
}
void set(int v){
GWSYNCHRONIZED(&lock);
data=v;
}
int get(){
GWSYNCHRONIZED(&lock);
return data;
}
};
void exampleTask(GwApi *api){
GwLog *logger=api->getLogger();
//get some configuration data
@@ -172,8 +193,24 @@ void exampleTask(GwApi *api){
LOG_DEBUG(GwLog::LOG,"exampleNotWorking update returned %d",(int)nwrs);
String voltageTransducer=api->getConfig()->getString(GwConfigDefinitions::exTransducer);
int voltageInstance=api->getConfig()->getInt(GwConfigDefinitions::exInstanceId);
ExampleWebData webData;
/**
* an example web request handler
* it uses a synchronized data structure as it gets called from a different thread
* be aware that you must not block for longer times here!
*/
api->registerRequestHandler("data",[&webData](AsyncWebServerRequest *request){
int data=webData.get();
char buffer[30];
snprintf(buffer,29,"%d",data);
buffer[29]=0;
request->send(200,"text/plain",buffer);
});
int loopcounter=0;
while(true){
delay(1000);
loopcounter++;
webData.set(loopcounter);
/*
* getting values from the internal data store (boatData) requires some special handling
* our tasks runs (potentially) at some time on a different core then the main code

View File

@@ -32,6 +32,26 @@ Files
This file allows to add some config definitions that are needed for our task. For the possible options have a look at the global [config.json](../../web/config.json). Be careful not to overwrite config defitions from the global file. A good practice wood be to prefix the names of definitions with parts of the library name. Always put them in a separate category so that they do not interfere with the system ones.
The defined config items can later be accessed in the code (see the example in [GwExampleTask.cpp](GwExampleTask.cpp)).
* [index.js](index.js)<br>
You can add javascript code that will contribute to the UI of the system. The WebUI provides a small API that allows you to "hook" into some functions to include your own parts of the UI. This includes adding new tabs, modifying/replacing the data display items, modifying the status display or accessing the config items.
For the API refer to [../../web/index.js](../../web/index.js#L2001).
To start interacting just register for some events like api.EVENTS.init. You can check the capabilities you have defined to see if your task is active.
By registering an own formatter [api.addUserFormatter](../../web/index.js#L2054) you can influence the way boat data items are shown.
You can even go for an own display by registering for the event *dataItemCreated* and replace the dom element content with your own html. By additionally having added a user formatter you can now fill your own html with the current value.
By using [api.addTabPage](../../web/index.js#L2046) you can add new tabs that you can populate with your own code. Or you can link to an external URL.<br>
Please be aware that your js code is always combined with the code from the core into one js file.<br>
For fast testing there is a small python script that allow you to test the UI without always flushing each change.
Just run it with
```
tools/testServer.py nnn http://x.x.x.x/api
```
with nnn being the local port and x.x.x.x the address of a running system. Open `http://localhost:nnn` in your browser.<br>
After a change just start the compilation and reload the page.
* [index.css](index.css)<br>
You can add own css to influence the styling of the display.
Interfaces
----------
The task init function and the task function interact with the core using an [API](../api/GwApi.h) that they get when started.
@@ -50,7 +70,8 @@ Files
* add capabilities (since 20231105 - as an alternative to a static DECLARE_CAPABILITY )
* add a user task (since 20231105 - as an alternative to a static DECLARE_USERTASK)
* store or read task interface data (see below)
* add a request handler for web requests (since 202411xx) - see registerRequestHandler in the API
__Interfacing between Task__

View File

@@ -5,84 +5,97 @@
//on our case this is "testboard"
//so we only start any action when we receive the init event
//and we successfully checked that our requested capability is there
let isActive=false;
const tabName="example";
const configName="exampleBDSel";
const infoUrl='https://github.com/wellenvogel/esp32-nmea2000/tree/master/lib/exampletask';
let boatItemName;
let boatItemElement;
api.registerListener((id,data)=>{
if (id === api.EVENTS.init){
//data is capabilities
//check if our requested capability is there (see GwExampleTask.h)
if (data.testboard) isActive=true;
if (isActive){
//add a simple additional tab page
//you will have to build the content of the page dynamically
//using normal dom manipulation methods
//you can use the helper addEl to create elements
let page=api.addTabPage(tabName,"Example");
api.addEl('div','hdg',page,"this is a test tab");
api.addEl('button','',page,'Info').addEventListener('click',function(ev){
window.open(infoUrl,'info');
api.registerListener((id, data) => {
//data is capabilities
//check if our requested capability is there (see GwExampleTask.h)
if (!data.testboard) return; //do nothing if we are not active
//add a simple additional tab page
//you will have to build the content of the page dynamically
//using normal dom manipulation methods
//you can use the helper addEl to create elements
let page = api.addTabPage(tabName, "Example");
api.addEl('div', 'hdg', page, "this is a test tab");
let vrow = api.addEl('div', 'row', page);
api.addEl('span', 'label', vrow, 'loops: ');
let lcount = api.addEl('span', 'value', vrow, '0');
//query the loop count
window.setInterval(() => {
fetch('/api/user/exampleTask/data')
.then((res) => {
if (!res.ok) throw Error("server error: " + res.status);
return res.text();
})
//add a tab for an external URL
api.addTabPage('exhelp','Info',infoUrl);
.then((txt) => {
//set the text content of our value element with what we received
lcount.textContent = txt;
})
.catch((e) => console.log("rq:", e));
}, 1000);
api.addEl('button', '', page, 'Info').addEventListener('click', function (ev) {
window.open(infoUrl, 'info');
})
//add a tab for an external URL
api.addTabPage('exhelp', 'Info', infoUrl);
//now as we know we are active - register all the listeners we need
api.registerListener((id, data) => {
console.log("exampletask status listener", data);
}, api.EVENTS.status)
api.registerListener((id, data) => {
if (data === tabName) {
//maybe we need some activity when our page is being activated
console.log("example tab activated");
}
}
if (isActive){
//console.log("exampletask listener",id,data);
if (id === api.EVENTS.tab){
if (data === tabName){
//maybe we need some activity when our page is being activated
console.log("example tab activated");
}
}, api.EVENTS.tab);
api.registerListener((id, data) => {
//we have a configuration that
//gives us the name of a boat data item we would like to
//handle special
//in our case we just use an own formatter and add some
//css to the display field
//as this item can change we need to keep track of the
//last item we handled
let nextboatItemName = data[configName];
console.log("value of " + configName, nextboatItemName);
if (nextboatItemName) {
//register a user formatter that will be called whenever
//there is a new valid value
//we simply add an "X:" in front
api.addUserFormatter(nextboatItemName, "m(x)", function (v, valid) {
if (!valid) return;
return "X:" + v;
})
//after this call the item will be recreated
}
if (id == api.EVENTS.config){
//we have a configuration that
//gives us the name of a boat data item we would like to
//handle special
//in our case we just use an own formatter and add some
//css to the display field
//as this item can change we need to keep track of the
//last item we handled
let nextboatItemName=data[configName];
console.log("value of "+configName,nextboatItemName);
if (nextboatItemName){
//register a user formatter that will be called whenever
//there is a new valid value
//we simply add an "X:" in front
api.addUserFormatter(nextboatItemName,"m(x)",function(v,valid){
if (!valid) return;
return "X:"+v;
})
//after this call the item will be recreated
}
if (boatItemName !== undefined && boatItemName != nextboatItemName){
//if the boat item that we handle has changed, remove
//the previous user formatter (this will recreate the item)
api.removeUserFormatter(boatItemName);
}
boatItemName=nextboatItemName;
boatItemElement=undefined;
if (boatItemName !== undefined && boatItemName != nextboatItemName) {
//if the boat item that we handle has changed, remove
//the previous user formatter (this will recreate the item)
api.removeUserFormatter(boatItemName);
}
if (id == api.EVENTS.dataItemCreated){
//this event is called whenever a data item has
//been created (or recreated)
//if this is the item we handle, we just add a css class
//we could also completely rebuild the dom below the element
//and use our formatter to directly write/draw the data
//avoid direct manipulation of the element (i.e. changing the classlist)
//as this element remains there all the time
if (boatItemName && boatItemName == data.name){
boatItemElement=data.element;
//use the helper forEl to find elements within the dashboard item
//the value element has the class "dashValue"
api.forEl(".dashValue",function(el){
el.classList.add("examplecss");
},boatItemElement);
}
boatItemName = nextboatItemName;
boatItemElement = undefined;
}, api.EVENTS.config);
api.registerListener((id, data) => {
//this event is called whenever a data item has
//been created (or recreated)
//if this is the item we handle, we just add a css class
//we could also completely rebuild the dom below the element
//and use our formatter to directly write/draw the data
//avoid direct manipulation of the element (i.e. changing the classlist)
//as this element remains there all the time
if (boatItemName && boatItemName == data.name) {
boatItemElement = data.element;
//use the helper forEl to find elements within the dashboard item
//the value element has the class "dashValue"
api.forEl(".dashValue", function (el) {
el.classList.add("examplecss");
}, boatItemElement);
}
}
})
}, api.EVENTS.dataItemCreated);
}, api.EVENTS.init);
})();

View File

@@ -4,6 +4,7 @@
#include <functional>
#include "GwMessage.h"
#include "GwLog.h"
#include "GwApi.h"
class GwWebServer{
private:
AsyncWebServer *server;
@@ -11,7 +12,7 @@ class GwWebServer{
GwLog *logger;
public:
typedef GwRequestMessage *(RequestCreator)(AsyncWebServerRequest *request);
using HandlerFunction=std::function<void(AsyncWebServerRequest *)>;
using HandlerFunction=GwApi::HandlerFunction;
GwWebServer(GwLog *logger, GwRequestQueue *queue,int port);
~GwWebServer();
void begin();

View File

@@ -24,6 +24,7 @@
#define GWSERIAL_TYPE_BI 2
#define GWSERIAL_TYPE_RX 3
#define GWSERIAL_TYPE_TX 4
#define GWSERIAL_TYPE_UNK 0
#include <GwConfigItem.h>
#include <HardwareSerial.h>
#include "GwAppInfo.h"

View File

@@ -63,6 +63,17 @@
#define _GWQMP6988
#endif
#GROVE
#ifdef M5_ENV4$GS$
#ifndef M5_GROOVEIIC$GS$
#define M5_GROOVEIIC$GS$
#endif
GROOVE_IIC(SHT3X,$Z$,1)
GROOVE_IIC(BMP280,$Z$,1)
#define _GWSHT3X
#define _GWBMP280
#endif
#GROVE
//example: -DSHT3XG1_A : defines STH3Xn1 on grove A - x depends on the other devices
#ifdef GWSHT3XG1$GS$
@@ -118,6 +129,24 @@
#define _GWBME280
#endif
#GROVE
#ifdef GWBMP280G1$GS$
#ifndef M5_GROOVEIIC$GS$
#define M5_GROOVEIIC$GS$
#endif
GROOVE_IIC(BMP280,$Z$,1)
#define _GWBMP280
#endif
#GROVE
#ifdef GWBMP280G2$GS$
#ifndef M5_GROOVEIIC$GS$
#define M5_GROOVEIIC$GS$
#endif
GROOVE_IIC(BMP280,$Z$,2)
#define _GWBMP280
#endif
#GROVE
//select up to 2 IIC devices for grove usage
#ifdef M5_GROOVEIIC$GS$
@@ -143,3 +172,5 @@
#error "both serial devices already in use"
#endif
#endif

View File

@@ -68,16 +68,20 @@ class BME280Config : public IICSensorBase{
if (!device)
return;
GwLog *logger = api->getLogger();
float pressure = N2kDoubleNA;
float temperature = N2kDoubleNA;
float humidity = N2kDoubleNA;
float computed = N2kDoubleNA;
if (prAct)
{
float pressure = device->readPressure();
float computed = pressure + prOff;
pressure = device->readPressure();
computed = pressure + prOff;
LOG_DEBUG(GwLog::DEBUG, "%s measure %2.0fPa, computed %2.0fPa", prefix.c_str(), pressure, computed);
sendN2kPressure(api, *this, computed, counterId);
}
if (tmAct)
{
float temperature = device->readTemperature(); // offset is handled internally
temperature = device->readTemperature(); // offset is handled internally
temperature = CToKelvin(temperature);
LOG_DEBUG(GwLog::DEBUG, "%s measure temp=%2.1f", prefix.c_str(), temperature);
sendN2kTemperature(api, *this, temperature, counterId);
@@ -88,6 +92,10 @@ class BME280Config : public IICSensorBase{
LOG_DEBUG(GwLog::DEBUG, "%s read humid=%02.0f", prefix.c_str(), humidity);
sendN2kHumidity(api, *this, humidity, counterId);
}
if (tmAct || prAct || (huAct && sensorId == 0x60))
{
sendN2kEnvironmentalParameters(api, *this, temperature, humidity, computed,counterId);
}
}
#define CFG280(prefix) \
CFG_GET(prAct,prefix); \
@@ -132,6 +140,7 @@ class BME280Config : public IICSensorBase{
busId = 2;
addr = 0x77;
CFG280(BME28022);
ok=true;
}
intv *= 1000;
}

182
lib/iictask/GwBMP280.cpp Normal file
View File

@@ -0,0 +1,182 @@
#include "GwBMP280.h"
#ifdef _GWIIC
#if defined(GWBMP280) || defined(GWBMP28011) || defined(GWBMP28012)|| defined(GWBMP28021)|| defined(GWBMP28022)
#define _GWBMP280
#endif
#else
#undef _GWBMP280
#undef GWBMP280
#undef GWBMP28011
#undef GWBMP28012
#undef GWBMP28021
#undef GWBMP28022
#endif
#ifdef _GWBMP280
#include <Adafruit_BMP280.h>
#endif
#ifdef _GWBMP280
#define TYPE "BMP280"
#define PRFX1 TYPE "11"
#define PRFX2 TYPE "12"
#define PRFX3 TYPE "21"
#define PRFX4 TYPE "22"
class BMP280Config : public IICSensorBase{
public:
bool prAct=true;
bool tmAct=true;
tN2kTempSource tmSrc=tN2kTempSource::N2kts_InsideTemperature;
tN2kPressureSource prSrc=tN2kPressureSource::N2kps_Atmospheric;
tN2kHumiditySource huSrc=tN2kHumiditySource::N2khs_Undef;
String tmNam="Temperature";
String prNam="Pressure";
float tmOff=0;
float prOff=0;
Adafruit_BMP280 *device=nullptr;
uint32_t sensorId=-1;
BMP280Config(GwApi * api, const String &prfx):SensorBase(TYPE,api,prfx){
}
virtual bool isActive(){return prAct||tmAct;}
virtual bool initDevice(GwApi *api,TwoWire *wire){
GwLog *logger=api->getLogger();
device= new Adafruit_BMP280(wire);
if (! device->begin(addr)){
LOG_DEBUG(GwLog::ERROR,"unable to initialize %s at %d",prefix.c_str(),addr);
delete device;
device=nullptr;
return false;
}
sensorId=device->sensorID();
LOG_DEBUG(GwLog::LOG, "initialized %s at %d, sensorId 0x%x", prefix.c_str(), addr, sensorId);
return (sensorId == 0x56 || sensorId == 0x57 || sensorId == 0x58)?true:false;
}
virtual bool preinit(GwApi * api){
GwLog *logger=api->getLogger();
LOG_DEBUG(GwLog::LOG,"%s configured",prefix.c_str());
api->addCapability(prefix,"true");
addPressureXdr(api,*this);
addTempXdr(api,*this);
return isActive();
}
virtual void measure(GwApi *api, TwoWire *wire, int counterId)
{
if (!device)
return;
GwLog *logger = api->getLogger();
float pressure = N2kDoubleNA;
float temperature = N2kDoubleNA;
float humidity = N2kDoubleNA;
float computed = N2kDoubleNA;
if (prAct)
{
pressure = device->readPressure();
computed = pressure + prOff;
LOG_DEBUG(GwLog::DEBUG, "%s measure %2.0fPa, computed %2.0fPa", prefix.c_str(), pressure, computed);
sendN2kPressure(api, *this, computed, counterId);
}
if (tmAct)
{
temperature = device->readTemperature(); // offset is handled internally
temperature = CToKelvin(temperature);
LOG_DEBUG(GwLog::DEBUG, "%s measure temp=%2.1f", prefix.c_str(), temperature);
sendN2kTemperature(api, *this, temperature, counterId);
}
if (tmAct || prAct )
{
sendN2kEnvironmentalParameters(api, *this, temperature, humidity, computed,counterId);
}
}
#define CFGBMP280(prefix) \
CFG_GET(prAct,prefix); \
CFG_GET(tmAct,prefix);\
CFG_GET(tmSrc,prefix);\
CFG_GET(iid,prefix);\
CFG_GET(intv,prefix);\
CFG_GET(tmNam,prefix);\
CFG_GET(prNam,prefix);\
CFG_GET(tmOff,prefix);\
CFG_GET(prOff,prefix);
virtual void readConfig(GwConfigHandler *cfg) override
{
if (prefix == PRFX1)
{
busId = 1;
addr = 0x76;
CFGBMP280(BMP28011);
ok=true;
}
if (prefix == PRFX2)
{
busId = 1;
addr = 0x77;
CFGBMP280(BMP28012);
ok=true;
}
if (prefix == PRFX3)
{
busId = 2;
addr = 0x76;
CFGBMP280(BMP28021);
ok=true;
}
if (prefix == PRFX4)
{
busId = 2;
addr = 0x77;
CFGBMP280(BMP28022);
ok=true;
}
intv *= 1000;
}
};
static IICSensorBase::Creator creator([](GwApi *api, const String &prfx){
return new BMP280Config(api,prfx);
});
IICSensorBase::Creator registerBMP280(GwApi *api,IICSensorList &sensors){
#if defined(GWBMP280) || defined(GWBMP28011)
{
auto *cfg=creator(api,PRFX1);
//BMP280Config *cfg=new BMP280Config(api,PRFX1);
sensors.add(api,cfg);
CHECK_IIC1();
#pragma message "GWBMP28011 defined"
}
#endif
#if defined(GWBMP28012)
{
auto *cfg=creator(api,PRFX2);
//BMP280Config *cfg=new BMP280Config(api,PRFX2);
sensors.add(api,cfg);
CHECK_IIC1();
#pragma message "GWBMP28012 defined"
}
#endif
#if defined(GWBMP28021)
{
auto *cfg=creator(api,PRFX3);
//BMP280Config *cfg=new BMP280Config(api,PRFX3);
sensors.add(api,cfg);
CHECK_IIC2();
#pragma message "GWBMP28021 defined"
}
#endif
#if defined(GWBMP28022)
{
auto *cfg=creator(api,PRFX4);
//BMP280Config *cfg=new BMP280Config(api,PRFX4);
sensors.add(api,cfg);
CHECK_IIC1();
#pragma message "GWBMP28022 defined"
}
#endif
return creator;
}
#else
IICSensorBase::Creator registerBMP280(GwApi *api,IICSensorList &sensors){
return IICSensorBase::Creator();
}
#endif

6
lib/iictask/GwBMP280.h Normal file
View File

@@ -0,0 +1,6 @@
#ifndef _GWBMP280_H
#define _GWBMP280_H
#include "GwIicSensors.h"
IICSensorBase::Creator registerBMP280(GwApi *api,IICSensorList &sensors);
#endif

View File

@@ -102,6 +102,15 @@ void sendN2kTemperature(GwApi *api,CFG &cfg,double value, int counterId){
api->increment(counterId,cfg.prefix+String("temp"));
}
template <class CFG>
void sendN2kEnvironmentalParameters(GwApi *api,CFG &cfg,double tmValue, double huValue, double prValue, int counterId){
tN2kMsg msg;
SetN2kEnvironmentalParameters(msg,1,cfg.tmSrc,tmValue,cfg.huSrc,huValue,prValue);
api->sendN2kMessage(msg);
api->increment(counterId,cfg.prefix+String("hum"));
api->increment(counterId,cfg.prefix+String("press"));
api->increment(counterId,cfg.prefix+String("temp"));
}
#ifndef _GWI_IIC1
#define CHECK_IIC1() checkDef(GWIIC_SCL,GWIIC_SDA)
@@ -114,4 +123,4 @@ void sendN2kTemperature(GwApi *api,CFG &cfg,double value, int counterId){
#define CHECK_IIC2()
#endif
#endif
#endif

View File

@@ -21,6 +21,7 @@ static std::vector<IICGrove> iicGroveList;
#include "GwIicSensors.h"
#include "GwHardware.h"
#include "GwBME280.h"
#include "GwBMP280.h"
#include "GwQMP6988.h"
#include "GwSHT3X.h"
#include <map>
@@ -92,6 +93,7 @@ void initIicTask(GwApi *api){
creators.push_back(registerSHT3X(api,sensors));
creators.push_back(registerQMP6988(api,sensors));
creators.push_back(registerBME280(api,sensors));
creators.push_back(registerBMP280(api,sensors));
#ifdef _GWI_IIC1
addGroveItems(creators,api,sensors,"1",_GWI_IIC1);
#endif

View File

@@ -530,5 +530,204 @@
}
}
]
},
{
"type": "array",
"name": "BMP280",
"replace": [
{
"b": "1",
"i": "11",
"n": "93"
},
{
"b": "1",
"i": "12",
"n": "92"
},
{
"b": "2",
"i": "21",
"n": "103"
},
{
"b": "2",
"i": "22",
"n": "102"
}
],
"children": [
{
"name": "BMP280$itmAct",
"label": "BMP280-$i Temp",
"type": "boolean",
"default": "true",
"description": "Enable the $i. I2C BMP280 temp sensor (bus $b)",
"category": "iicsensors$b",
"capabilities": {
"BMP280$i": "true"
}
},
{
"name": "BMP280$itmSrc",
"label": "BMP280-$i Temp Type",
"type": "list",
"default": "2",
"description": "the NMEA2000 source type for the temperature",
"list": [
{
"l": "SeaTemperature",
"v": "0"
},
{
"l": "OutsideTemperature",
"v": "1"
},
{
"l": "InsideTemperature",
"v": "2"
},
{
"l": "EngineRoomTemperature",
"v": "3"
},
{
"l": "MainCabinTemperature",
"v": "4"
},
{
"l": "LiveWellTemperature",
"v": "5"
},
{
"l": "BaitWellTemperature",
"v": "6"
},
{
"l": "RefridgerationTemperature",
"v": "7"
},
{
"l": "HeatingSystemTemperature",
"v": "8"
},
{
"l": "DewPointTemperature",
"v": "9"
},
{
"l": "ApparentWindChillTemperature",
"v": "10"
},
{
"l": "TheoreticalWindChillTemperature",
"v": "11"
},
{
"l": "HeatIndexTemperature",
"v": "12"
},
{
"l": "FreezerTemperature",
"v": "13"
},
{
"l": "ExhaustGasTemperature",
"v": "14"
},
{
"l": "ShaftSealTemperature",
"v": "15"
}
],
"category": "iicsensors$b",
"capabilities": {
"BMP280$i": "true"
}
},
{
"name": "BMP280$itmOff",
"label": "BMP280-$i Temperature Offset",
"type": "number",
"description": "offset (in °) to be added to the BMP280 temperature measurements",
"default": "0",
"category": "iicsensors$b",
"capabilities": {
"BMP280$i": "true"
}
},
{
"name": "BMP280$iprAct",
"label": "BMP280-$i Pressure",
"type": "boolean",
"default": "true",
"description": "Enable the $i. I2C BMP280 pressure sensor (bus $b)",
"category": "iicsensors$b",
"capabilities": {
"BMP280$i": "true"
}
},
{
"name": "BMP280$iprOff",
"label": "BMP280 Pressure Offset",
"type": "number",
"description": "offset (in pa) to be added to the BMP280 pressure measurements",
"default": "0",
"category": "iicsensors$b",
"capabilities": {
"BMP280$i": "true"
}
},
{
"name": "BMP280$iiid",
"label": "BMP280-$i N2K iid",
"type": "number",
"default": "$n",
"description": "the N2K instance id for the BMP280 Temperature",
"category": "iicsensors$b",
"min": 0,
"max": 253,
"check": "checkMinMax",
"capabilities": {
"BMP280$i": "true"
}
},
{
"name": "BMP280$iintv",
"label": "BMP280-$i Interval",
"type": "number",
"default": 2,
"description": "Interval(s) to query BME280 Temperature (1...300)",
"category": "iicsensors$b",
"min": 1,
"max": 300,
"check": "checkMinMax",
"capabilities": {
"BMP280$i": "true"
}
},
{
"name": "BMP280$itmNam",
"label": "BMP280-$i Temp XDR",
"type": "String",
"default": "BTemp$i",
"description": "set the XDR transducer name for the BMP280 Temperature, leave empty to disable NMEA0183 XDR ",
"category": "iicsensors$b",
"capabilities": {
"BMP280$i": "true"
}
},
{
"name": "BMP280$iprNam",
"label": "BMP280-$i Pressure XDR",
"type": "String",
"default": "BPressure$i",
"description": "set the XDR transducer name for the BMP280 Pressure, leave empty to disable NMEA0183 XDR",
"category": "iicsensors$b",
"capabilities": {
"BMP280$i": "true"
}
}
]
}
]
]

View File

@@ -11,6 +11,17 @@ build_flags=
-D M5_CAN_KIT
${env.build_flags}
[env:m5stack-atom-env4]
extends = sensors
board = m5stack-atom
lib_deps =
${env.lib_deps}
${sensors.lib_deps}
build_flags=
-D M5_ENV4
-D M5_CAN_KIT
${env.build_flags}
[env:m5stack-atom-bme280]
extends = sensors
board = m5stack-atom
@@ -35,3 +46,15 @@ build_flags=
-D M5_GROOVEIIC
-D M5_CAN_KIT
${env.build_flags}
[env:m5stack-atom-envbps]
extends = sensors
board = m5stack-atom
lib_deps =
${env.lib_deps}
${sensors.lib_deps}
build_flags=
-D GWBMP280G1
-D M5_GROOVEIIC
-D M5_CAN_KIT
${env.build_flags}

View File

@@ -38,5 +38,7 @@ class GwLog{
long long getRecordCounter(){return recordCounter;}
};
#define LOG_DEBUG(level,...){ if (logger != NULL && logger->isActive(level)) logger->logDebug(level,__VA_ARGS__);}
#define LOG_INFO(...){ if (logger != NULL && logger->isActive(GwLog::LOG)) logger->logDebug(GwLog::LOG,__VA_ARGS__);}
#define LOG_ERROR(...){ if (logger != NULL && logger->isActive(GwLog::ERROR)) logger->logDebug(GwLog::ERROR,__VA_ARGS__);}
#endif

View File

@@ -860,7 +860,7 @@ private:
LOG_DEBUG(GwLog::DEBUG,"GSV invalid current %u %s",current,msg.line);
return;
}
for (int idx=2;idx < msg.FieldCount();idx+=4){
for (int idx=3;idx < msg.FieldCount();idx+=4){
if (msg.FieldLen(idx) < 1 ||
msg.FieldLen(idx+1) < 1 ||
msg.FieldLen(idx+2) < 1 ||

View File

@@ -219,7 +219,7 @@ bool SetAISClassBMessage18(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, u
bool SetAISClassBMessage24PartA(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, uint32_t UserID, char *Name) {
bool found = false;
for (int i = 0; i < vships.size(); i++) {
for (size_t i = 0; i < vships.size(); i++) {
if ( vships[i]->_userID == UserID ) {
found = true;
break;

View File

@@ -21,7 +21,7 @@ GwBuffer::~GwBuffer(){
}
void GwBuffer::reset(String reason)
{
LOG_DEBUG(GwLog::LOG,"reseting buffer %s, reason %s",this->name.c_str(),reason.c_str());
if (! reason.isEmpty())LOG_DEBUG(GwLog::LOG,"reseting buffer %s, reason %s",this->name.c_str(),reason.c_str());
writePointer = buffer;
readPointer = buffer;
lp("reset");

View File

@@ -18,9 +18,9 @@ class GwMessageFetcher{
* buffer to safely inserte data if it fits
* and to write out data if possible
*/
typedef size_t (*GwBufferHandleFunction)(uint8_t *buffer, size_t len, void *param);
class GwBuffer{
public:
using GwBufferHandleFunction=std::function<size_t(uint8_t *buffer, size_t len, void *param)>;
static const size_t TX_BUFFER_SIZE=1620; // app. 20 NMEA messages
static const size_t RX_BUFFER_SIZE=600; // enough for 1 NMEA message or actisense message or seasmart message
typedef enum {
@@ -33,7 +33,7 @@ class GwBuffer{
uint8_t *buffer;
uint8_t *writePointer;
uint8_t *readPointer;
size_t offset(uint8_t* ptr){
size_t offset(uint8_t* ptr) const{
return (size_t)(ptr-buffer);
}
GwLog *logger;

View File

@@ -7,10 +7,10 @@ class GwSynchronized{
public:
GwSynchronized(SemaphoreHandle_t *locker){
this->locker=locker;
xSemaphoreTake(*locker, portMAX_DELAY);
if (locker != nullptr) xSemaphoreTake(*locker, portMAX_DELAY);
}
~GwSynchronized(){
xSemaphoreGive(*locker);
if (locker != nullptr) xSemaphoreGive(*locker);
}
};

View File

@@ -1,4 +1,5 @@
#include "GwSerial.h"
#include "GwHardware.h"
class GwSerialStream: public Stream{
private:
@@ -40,11 +41,13 @@ class GwSerialStream: public Stream{
GwSerial::GwSerial(GwLog *logger, Stream *s, int id,bool allowRead):serial(s)
GwSerial::GwSerial(GwLog *logger, Stream * stream,int id,int type,bool allowRead)
{
LOG_DEBUG(GwLog::DEBUG,"creating GwSerial %p id %d",this,id);
this->id=id;
this->logger = logger;
this->id=id;
this->stream=stream;
this->type=type;
String bufName="Ser(";
bufName+=String(id);
bufName+=")";
@@ -62,6 +65,20 @@ GwSerial::~GwSerial()
if (readBuffer) delete readBuffer;
}
String GwSerial::getMode(){
switch (type){
case GWSERIAL_TYPE_UNI:
return "UNI";
case GWSERIAL_TYPE_BI:
return "BI";
case GWSERIAL_TYPE_RX:
return "RX";
case GWSERIAL_TYPE_TX:
return "TX";
}
return "UNKNOWN";
}
bool GwSerial::isInitialized() { return initialized; }
size_t GwSerial::enqueue(const uint8_t *data, size_t len, bool partial)
{
@@ -70,9 +87,9 @@ size_t GwSerial::enqueue(const uint8_t *data, size_t len, bool partial)
}
GwBuffer::WriteStatus GwSerial::write(){
if (! isInitialized()) return GwBuffer::ERROR;
size_t numWrite=serial->availableForWrite();
size_t numWrite=availableForWrite();
size_t rt=buffer->fetchData(numWrite,[](uint8_t *buffer,size_t len, void *p){
return ((GwSerial *)p)->serial->write(buffer,len);
return ((GwSerial *)p)->stream->write(buffer,len);
},this);
if (rt != 0){
LOG_DEBUG(GwLog::DEBUG+1,"Serial %d write %d",id,rt);
@@ -93,11 +110,11 @@ void GwSerial::loop(bool handleRead,bool handleWrite){
write();
if (! isInitialized()) return;
if (! handleRead) return;
size_t available=serial->available();
size_t available=stream->available();
if (! available) return;
if (allowRead){
size_t rd=readBuffer->fillData(available,[](uint8_t *buffer, size_t len, void *p)->size_t{
return ((GwSerial *)p)->serial->readBytes(buffer,len);
return ((GwSerial *)p)->stream->readBytes(buffer,len);
},this);
if (rd != 0){
LOG_DEBUG(GwLog::DEBUG+2,"GwSerial %d read %d bytes",id,rd);
@@ -106,7 +123,7 @@ void GwSerial::loop(bool handleRead,bool handleWrite){
else{
uint8_t buffer[10];
if (available > 10) available=10;
serial->readBytes(buffer,available);
stream->readBytes(buffer,available);
}
}
void GwSerial::readMessages(GwMessageFetcher *writer){
@@ -115,10 +132,11 @@ void GwSerial::readMessages(GwMessageFetcher *writer){
writer->handleBuffer(readBuffer);
}
bool GwSerial::flush(long max){
bool GwSerial::flush(){
if (! isInitialized()) return false;
long max=getFlushTimeout();
if (! availableWrite) {
if ( serial->availableForWrite() < 1){
if ( availableForWrite() < 1){
return false;
}
availableWrite=true;
@@ -128,7 +146,7 @@ bool GwSerial::flush(long max){
if (write() != GwBuffer::AGAIN) return true;
vTaskDelay(1);
}
availableWrite=(serial->availableForWrite() > 0);
availableWrite=(availableForWrite() > 0);
return false;
}
Stream * GwSerial::getStream(bool partialWrite){

View File

@@ -4,31 +4,119 @@
#include "GwLog.h"
#include "GwBuffer.h"
#include "GwChannelInterface.h"
#if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3
#include "hal/usb_serial_jtag_ll.h"
#endif
#define USBCDC_RESTART_TIME 100
class GwSerialStream;
class GwSerial : public GwChannelInterface{
private:
protected:
GwBuffer *buffer;
GwBuffer *readBuffer=NULL;
GwLog *logger;
Stream *stream;
bool initialized=false;
bool allowRead=true;
GwBuffer::WriteStatus write();
int id=-1;
int overflows=0;
size_t enqueue(const uint8_t *data, size_t len,bool partial=false);
Stream *serial;
bool availableWrite=false; //if this is false we will wait for availabkleWrite until we flush again
virtual long getFlushTimeout(){return 2000;}
virtual int availableForWrite()=0;
int type=0;
public:
static const int bufferSize=200;
GwSerial(GwLog *logger,Stream *stream,int id,bool allowRead=true);
~GwSerial();
GwSerial(GwLog *logger,Stream *stream,int id,int type,bool allowRead=true);
virtual ~GwSerial();
bool isInitialized();
virtual size_t sendToClients(const char *buf,int sourceId,bool partial=false);
virtual void loop(bool handleRead=true,bool handleWrite=true);
virtual void readMessages(GwMessageFetcher *writer);
bool flush(long millis=200);
bool flush();
virtual Stream *getStream(bool partialWrites);
bool getAvailableWrite(){return availableWrite;}
virtual void begin(unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1)=0;
virtual String getMode() override;
friend GwSerialStream;
};
template<typename T>
class GwSerialImpl : public GwSerial{
private:
unsigned long lastWritable=0;
template<class C>
void beginImpl(C *s,unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1){}
void beginImpl(HardwareSerial *s,unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1){
s->begin(baud,config,rxPin,txPin);
}
template<class C>
void setError(C* s, GwLog *logger){}
void setError(HardwareSerial *s,GwLog *logger){
LOG_DEBUG(GwLog::LOG,"enable serial errors for channel %d",id);
s->onReceiveError([logger,this](hardwareSerial_error_t err){
LOG_DEBUG(GwLog::ERROR,"serial error on id %d: %d",this->id,(int)err);
});
}
#if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3
void beginImpl(HWCDC *s,unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1){
s->begin(baud);
}
#endif
template<class C>
long getFlushTimeoutImpl(const C*){return 2000;}
#if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3
long getFlushTimeoutImpl(HWCDC *){return 200;}
#endif
template<class C>
int availableForWrite(C* c){
return c->availableForWrite();
}
#if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3
/**
* issue #81
* workaround for the HWCDC beeing stuck at some point in time
* with availableForWrite == 0 but the ISR being disabled
* we simply give a small delay of 100ms for availableForWrite being 0
* and afterwards retrigger the ISR
*/
int availableForWrite(HWCDC* c){
int rt=c->availableForWrite();
if (rt > 0) {
lastWritable=millis();
return rt;
}
unsigned long now=millis();
if (now > (lastWritable+USBCDC_RESTART_TIME)){
lastWritable=now;
if (c->isConnected()){
//this retriggers the ISR
usb_serial_jtag_ll_ena_intr_mask(USB_SERIAL_JTAG_INTR_SERIAL_IN_EMPTY);
}
}
return rt;
}
#endif
T *serial;
protected:
virtual long getFlushTimeout() override{
return getFlushTimeoutImpl(serial);
}
virtual int availableForWrite(){
return availableForWrite(serial);
}
public:
GwSerialImpl(GwLog* logger,T* s,int i,int type,bool allowRead=true): GwSerial(logger,s,i,type,allowRead),serial(s){}
virtual ~GwSerialImpl(){}
virtual void begin(unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1) override{
beginImpl(serial,baud,config,rxPin,txPin);
setError(serial,logger);
};
};
#endif

View File

@@ -17,4 +17,12 @@ class GwSocketHelper{
if (setsockopt(socket, IPPROTO_TCP, TCP_KEEPCNT, &val, sizeof(val)) != ESP_OK) return false;
return true;
}
static bool isMulticast(const String &addr){
in_addr iaddr;
if (inet_pton(AF_INET,addr.c_str(),&iaddr) != 1) return false;
return IN_MULTICAST(ntohl(iaddr.s_addr));
}
static bool equals(const in_addr &left, const in_addr &right){
return left.s_addr == right.s_addr;
}
};

View File

@@ -0,0 +1,167 @@
#include "GwUdpReader.h"
#include <ESPmDNS.h>
#include <errno.h>
#include "GwBuffer.h"
#include "GwSocketConnection.h"
#include "GwSocketHelper.h"
#include "GWWifi.h"
GwUdpReader::GwUdpReader(const GwConfigHandler *config, GwLog *logger, int minId)
{
this->config = config;
this->logger = logger;
this->minId = minId;
port=config->getInt(GwConfigDefinitions::udprPort);
buffer= new GwBuffer(logger,GwBuffer::RX_BUFFER_SIZE,"udprd");
}
void GwUdpReader::createAndBind(){
if (fd >= 0){
::close(fd);
}
if (currentStationIp.isEmpty() && (type == T_STA || type == T_MCSTA)) return;
fd=socket(AF_INET,SOCK_DGRAM,IPPROTO_IP);
if (fd < 0){
LOG_ERROR("UDPR: unable to create udp socket: %d",errno);
return;
}
int enable = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
if (type == T_STA)
{
if (inet_pton(AF_INET, currentStationIp.c_str(), &listenA.sin_addr) != 1)
{
LOG_ERROR("UDPR: invalid station ip address %s", currentStationIp.c_str());
close(fd);
fd = -1;
return;
}
}
if (bind(fd,(struct sockaddr *)&listenA,sizeof(listenA)) < 0){
LOG_ERROR("UDPR: unable to bind: %d",errno);
close(fd);
fd=-1;
return;
}
LOG_INFO("UDPR: socket created and bound");
if (type != T_MCALL && type != T_MCAP && type != T_MCSTA) {
return;
}
struct ip_mreq mc;
mc.imr_multiaddr=listenA.sin_addr;
if (type == T_MCALL || type == T_MCAP){
mc.imr_interface=apAddr;
int res=setsockopt(fd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mc,sizeof(mc));
if (res != 0){
LOG_ERROR("UDPR: unable to add MC membership for AP:%d",errno);
}
else{
LOG_INFO("UDPR: membership for for AP");
}
}
if (!currentStationIp.isEmpty() && (type == T_MCALL || type == T_MCSTA))
{
mc.imr_interface = staAddr;
int res = setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mc, sizeof(mc));
if (res != 0)
{
LOG_ERROR("UDPR: unable to add MC membership for STA:%d", errno);
}
else{
LOG_INFO("UDPR: membership for STA %s",currentStationIp.c_str());
}
}
}
void GwUdpReader::begin()
{
if (type != T_UNKNOWN) return; //already started
type=(UType)(config->getInt(GwConfigDefinitions::udprType));
LOG_INFO("UDPR begin, mode=%d",(int)type);
port=config->getInt(GwConfigDefinitions::udprPort);
listenA.sin_family=AF_INET;
listenA.sin_port=htons(port);
listenA.sin_addr.s_addr=htonl(INADDR_ANY); //default
String ap=WiFi.softAPIP().toString();
if (inet_pton(AF_INET, ap.c_str(), &apAddr) != 1)
{
LOG_ERROR("UDPR: invalid ap ip address %s", ap.c_str());
return;
}
if (type == T_MCALL || type == T_MCAP || type == T_MCSTA){
String mcAddr=config->getString(GwConfigDefinitions::udprMC);
if (inet_pton(AF_INET, mcAddr.c_str(), &listenA.sin_addr) != 1)
{
LOG_ERROR("UDPR: invalid mc address %s", mcAddr.c_str());
close(fd);
fd = -1;
return;
}
LOG_INFO("UDPR: using multicast address %s",mcAddr.c_str());
}
if (type == T_AP){
listenA.sin_addr=apAddr;
}
String sta;
if (WiFi.isConnected()) sta=WiFi.localIP().toString();
setStationAdd(sta);
createAndBind();
}
bool GwUdpReader::setStationAdd(const String &sta){
if (sta == currentStationIp) return false;
currentStationIp=sta;
if (inet_pton(AF_INET, currentStationIp.c_str(), &staAddr) != 1){
LOG_ERROR("UDPR: invalid station ip address %s", currentStationIp.c_str());
return false;
}
LOG_INFO("UDPR: new station IP %s",currentStationIp.c_str());
return true;
}
void GwUdpReader::loop(bool handleRead, bool handleWrite)
{
if (handleRead){
if (type == T_STA || type == T_MCALL || type == T_MCSTA){
//only change anything if we considered the station IP
String nextStationIp;
if (WiFi.isConnected()){
nextStationIp=WiFi.localIP().toString();
}
if (setStationAdd(nextStationIp)){
LOG_INFO("UDPR: wifi client IP changed, restart");
createAndBind();
}
}
}
}
void GwUdpReader::readMessages(GwMessageFetcher *writer)
{
if (fd < 0) return;
//we expect one NMEA message in one UDP packet
buffer->reset();
size_t rd=buffer->fillData(buffer->freeSpace(),
[this](uint8_t *rcvb,size_t rcvlen,void *param)->size_t{
struct sockaddr_in from;
socklen_t fromLen=sizeof(from);
ssize_t res=recvfrom(fd,rcvb,rcvlen,MSG_DONTWAIT,
(struct sockaddr*)&from,&fromLen);
if (res <= 0) return 0;
if (GwSocketHelper::equals(from.sin_addr,apAddr)) return 0;
if (!currentStationIp.isEmpty() && (GwSocketHelper::equals(from.sin_addr,staAddr))) return 0;
return res;
},this);
if (buffer->usedSpace() > 0)(GwLog::DEBUG,"UDPR: received %d bytes",buffer->usedSpace());
writer->handleBuffer(buffer);
}
size_t GwUdpReader::sendToClients(const char *buf, int source,bool partial)
{
return 0;
}
GwUdpReader::~GwUdpReader()
{
}

View File

@@ -0,0 +1,45 @@
#ifndef _GWUDPREADER_H
#define _GWUDPREADER_H
#include "GWConfig.h"
#include "GwLog.h"
#include "GwBuffer.h"
#include "GwChannelInterface.h"
#include <memory>
#include <sys/socket.h>
#include <arpa/inet.h>
class GwUdpReader: public GwChannelInterface{
public:
using UType=enum{
T_ALL=0,
T_AP=1,
T_STA=2,
T_MCALL=4,
T_MCAP=5,
T_MCSTA=6,
T_UNKNOWN=-1
};
private:
const GwConfigHandler *config;
GwLog *logger;
int minId;
int port;
int fd=-1;
struct sockaddr_in listenA;
String listenIp;
String currentStationIp;
struct in_addr apAddr;
struct in_addr staAddr;
UType type=T_UNKNOWN;
void createAndBind();
bool setStationAdd(const String &sta);
GwBuffer *buffer=nullptr;
public:
GwUdpReader(const GwConfigHandler *config,GwLog *logger,int minId);
~GwUdpReader();
void begin();
virtual void loop(bool handleRead=true,bool handleWrite=true);
virtual size_t sendToClients(const char *buf,int sourceId, bool partialWrite=false);
virtual void readMessages(GwMessageFetcher *writer);
};
#endif

View File

@@ -0,0 +1,203 @@
#include "GwUdpWriter.h"
#include <ESPmDNS.h>
#include <errno.h>
#include "GwBuffer.h"
#include "GwSocketConnection.h"
#include "GwSocketHelper.h"
#include "GWWifi.h"
GwUdpWriter::WriterSocket::WriterSocket(GwLog *l,int p,const String &src,const String &dst, SourceMode sm) :
sourceMode(sm), source(src), destination(dst), port(p),logger(l)
{
if (inet_pton(AF_INET, dst.c_str(), &dstA.sin_addr) != 1)
{
LOG_ERROR("UDPW: invalid destination ip address %s", dst.c_str());
return;
}
if (sourceMode != SourceMode::S_UNBOUND)
{
if (inet_pton(AF_INET, src.c_str(), &srcA) != 1)
{
LOG_ERROR("UDPW: invalid source ip address %s", src.c_str());
return;
}
}
dstA.sin_family=AF_INET;
dstA.sin_port=htons(port);
fd=socket(AF_INET,SOCK_DGRAM,IPPROTO_IP);
if (fd < 0){
LOG_ERROR("UDPW: unable to create udp socket: %d",errno);
return;
}
int enable = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &enable, sizeof(int));
switch (sourceMode)
{
case SourceMode::S_SRC:
{
sockaddr_in bindA;
bindA.sin_family = AF_INET;
bindA.sin_port = htons(0); // let system select
bindA.sin_addr = srcA;
if (bind(fd, (struct sockaddr *)&bindA, sizeof(bindA)) != 0)
{
LOG_ERROR("UDPW: bind failed for address %s: %d", source.c_str(), errno);
::close(fd);
fd = -1;
return;
}
}
break;
case SourceMode::S_MC:
{
if (setsockopt(fd,IPPROTO_IP,IP_MULTICAST_IF,&srcA,sizeof(srcA)) != 0){
LOG_ERROR("UDPW: unable to set MC source %s: %d",source.c_str(),errno);
::close(fd);
fd=-1;
return;
}
int loop=0;
setsockopt(fd,IPPROTO_IP,IP_MULTICAST_LOOP,&loop,sizeof(loop));
}
break;
default:
//not bound
break;
}
}
bool GwUdpWriter::WriterSocket::changed(const String &newSrc, const String &newDst){
if (newDst != destination) return true;
if (sourceMode == SourceMode::S_UNBOUND) return false;
return newSrc != source;
}
size_t GwUdpWriter::WriterSocket::send(const char *buf,size_t len){
if (fd < 0) return 0;
ssize_t err = sendto(fd,buf,len,0,(struct sockaddr *)&dstA, sizeof(dstA));
if (err < 0){
LOG_DEBUG(GwLog::DEBUG,"UDPW %s error sending: %d",destination.c_str(), errno);
return 0;
}
return err;
}
GwUdpWriter::GwUdpWriter(const GwConfigHandler *config, GwLog *logger, int minId)
{
this->config = config;
this->logger = logger;
this->minId = minId;
port=config->getInt(GwConfigDefinitions::udpwPort);
}
void GwUdpWriter::checkStaSocket(){
String src;
String bc;
if (type == T_BCAP || type == T_MCAP || type == T_NORM || type == T_UNKNOWN ) return;
bool connected=false;
if (WiFi.isConnected()){
src=WiFi.localIP().toString();
bc=WiFi.broadcastIP().toString();
connected=true;
}
else{
if (staSocket == nullptr) return;
}
String dst;
WriterSocket::SourceMode sm=WriterSocket::SourceMode::S_SRC;
switch (type){
case T_BCALL:
case T_BCSTA:
sm=WriterSocket::SourceMode::S_SRC;
dst=bc;
break;
case T_MCALL:
case T_MCSTA:
dst=config->getString(GwConfigDefinitions::udpwMC);
sm=WriterSocket::SourceMode::S_MC;
break;
}
if (staSocket != nullptr)
{
if (!connected || staSocket->changed(src, dst))
{
staSocket->close();
delete staSocket;
staSocket = nullptr;
LOG_INFO("changing/stopping UDPW(sta) socket");
}
}
if (staSocket == nullptr && connected)
{
LOG_INFO("creating new UDP(sta) socket src=%s, dst=%s", src.c_str(), dst.c_str());
staSocket = new WriterSocket(logger, port, src, dst, WriterSocket::SourceMode::S_SRC);
}
}
void GwUdpWriter::begin()
{
if (type != T_UNKNOWN) return; //already started
type=(UType)(config->getInt(GwConfigDefinitions::udpwType));
LOG_INFO("UDPW begin, mode=%d",(int)type);
String src=WiFi.softAPIP().toString();
String dst;
WriterSocket::SourceMode sm=WriterSocket::SourceMode::S_UNBOUND;
bool createApSocket=false;
switch(type){
case T_BCALL:
case T_BCAP:
createApSocket=true;
dst=WiFi.softAPBroadcastIP().toString();
sm=WriterSocket::SourceMode::S_SRC;
break;
case T_MCALL:
case T_MCAP:
createApSocket=true;
dst=config->getString(GwConfigDefinitions::udpwMC);
sm=WriterSocket::SourceMode::S_SRC;
break;
case T_NORM:
createApSocket=true;
dst=config->getString(GwConfigDefinitions::udpwAddress);
sm=WriterSocket::SourceMode::S_UNBOUND;
}
if (createApSocket){
LOG_INFO("creating new UDPW(ap) socket src=%s, dst=%s", src.c_str(), dst.c_str());
apSocket=new WriterSocket(logger,port,src,dst,sm);
}
checkStaSocket();
}
void GwUdpWriter::loop(bool handleRead, bool handleWrite)
{
if (handleWrite){
checkStaSocket();
}
}
void GwUdpWriter::readMessages(GwMessageFetcher *writer)
{
}
size_t GwUdpWriter::sendToClients(const char *buf, int source,bool partial)
{
if (source == minId) return 0;
size_t len=strlen(buf);
bool hasSent=false;
size_t res=0;
if (apSocket != nullptr){
res=apSocket->send(buf,len);
if (res > 0) hasSent=true;
}
if (staSocket != nullptr){
res=staSocket->send(buf,len);
if (res > 0) hasSent=true;
}
return hasSent?len:0;
}
GwUdpWriter::~GwUdpWriter()
{
}

View File

@@ -0,0 +1,73 @@
#ifndef _GWUDPWRITER_H
#define _GWUDPWRITER_H
#include "GWConfig.h"
#include "GwLog.h"
#include "GwBuffer.h"
#include "GwChannelInterface.h"
#include <memory>
#include <sys/socket.h>
#include <arpa/inet.h>
class GwUdpWriter: public GwChannelInterface{
public:
using UType=enum{
T_BCALL=0,
T_BCAP=1,
T_BCSTA=2,
T_NORM=3,
T_MCALL=4,
T_MCAP=5,
T_MCSTA=6,
T_UNKNOWN=-1
};
private:
class WriterSocket{
public:
int fd=-1;
struct in_addr srcA;
struct sockaddr_in dstA;
String source;
String destination;
int port;
GwLog *logger;
using SourceMode=enum {
S_UNBOUND=0,
S_MC,
S_SRC
};
SourceMode sourceMode;
WriterSocket(GwLog *logger,int p,const String &src,const String &dst, SourceMode sm);
void close(){
if (fd > 0){
::close(fd);
}
fd=-1;
}
~WriterSocket(){
close();
}
bool changed(const String &newSrc, const String &newDst);
size_t send(const char *buf,size_t len);
};
const GwConfigHandler *config;
GwLog *logger;
/**
* we use fd/address to send to the AP network
* and fd2,address2 to send to the station network
* for type "normal" we only use fd
*/
WriterSocket *apSocket=nullptr; //also for T_NORM
WriterSocket *staSocket=nullptr;
int minId;
int port;
UType type=T_UNKNOWN;
void checkStaSocket();
public:
GwUdpWriter(const GwConfigHandler *config,GwLog *logger,int minId);
~GwUdpWriter();
void begin();
virtual void loop(bool handleRead=true,bool handleWrite=true);
virtual size_t sendToClients(const char *buf,int sourceId, bool partialWrite=false);
virtual void readMessages(GwMessageFetcher *writer);
};
#endif

View File

@@ -191,6 +191,7 @@ class TaskApi : public GwApiInternal
SemaphoreHandle_t *mainLock;
SemaphoreHandle_t localLock;
std::map<int,GwCounter<String>> counter;
std::map<String,GwApi::HandlerFunction> webHandlers;
String name;
bool counterUsed=false;
int counterIdx=0;
@@ -315,6 +316,10 @@ public:
virtual bool addXdrMapping(const GwXDRMappingDef &def){
return api->addXdrMapping(def);
}
virtual void registerRequestHandler(const String &url,HandlerFunction handler){
GWSYNCHRONIZED(&localLock);
webHandlers[url]=handler;
}
virtual void addCapability(const String &name, const String &value){
if (! isInit) return;
userCapabilities[name]=value;
@@ -335,7 +340,23 @@ public:
virtual void setCalibrationValue(const String &name, double value){
api->setCalibrationValue(name,value);
}
virtual bool handleWebRequest(const String &url, AsyncWebServerRequest *req)
{
GwApi::HandlerFunction handler;
{
GWSYNCHRONIZED(&localLock);
auto it = webHandlers.find(url);
if (it == webHandlers.end())
{
api->getLogger()->logDebug(GwLog::LOG, "no web handler task=%s url=%s", name.c_str(), url.c_str());
return false;
}
handler = it->second;
}
if (handler)
handler(req);
return true;
}
};
GwUserCode::GwUserCode(GwApiInternal *api,SemaphoreHandle_t *mainLock){
@@ -404,4 +425,19 @@ int GwUserCode::getJsonSize(){
}
}
return rt;
}
void GwUserCode::handleWebRequest(const String &url,AsyncWebServerRequest *req){
int sep1=url.indexOf('/');
String tname;
if (sep1 > 0){
tname=url.substring(0,sep1);
for (auto &&it:userTasks){
if (it.api && it.name == tname){
if (it.api->handleWebRequest(url.substring(sep1+1),req)) return;
break;
}
}
}
LOG_DEBUG(GwLog::DEBUG,"no task found for web request %s[%s]",url.c_str(),tname.c_str());
req->send(404, "text/plain", "not found");
}

View File

@@ -11,6 +11,7 @@ class GwApiInternal : public GwApi{
~GwApiInternal(){}
virtual void fillStatus(GwJsonDocument &status){};
virtual int getJsonSize(){return 0;};
virtual bool handleWebRequest(const String &url,AsyncWebServerRequest *req){return false;}
};
class GwUserTask{
public:
@@ -50,5 +51,6 @@ class GwUserCode{
Capabilities *getCapabilities();
void fillStatus(GwJsonDocument &status);
int getJsonSize();
void handleWebRequest(const String &url,AsyncWebServerRequest *);
};
#endif