Merge branch 'master' into boards

This commit is contained in:
free-x 2021-10-29 07:33:09 +02:00 committed by GitHub
commit 62baaa33f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 629 additions and 388 deletions

2
.gitignore vendored
View File

@ -3,4 +3,4 @@
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
web/*gz
generated/*

View File

@ -2,20 +2,82 @@ print("running extra...")
import gzip
import shutil
import os
FILES=['web/index.html']
import sys
import inspect
import json
GEN_DIR='generated'
CFG_FILE='web/config.json'
FILES=['web/index.html',CFG_FILE]
CFG_INCLUDE='GwConfigDefinitions.h'
def compressFile(inFile):
outfile=inFile+".gz"
def basePath():
#see: https://stackoverflow.com/questions/16771894/python-nameerror-global-name-file-is-not-defined
return os.path.dirname(inspect.getfile(lambda: None))
def outPath():
return os.path.join(basePath(),GEN_DIR)
def checkDir():
dn=outPath()
if not os.path.exists(dn):
os.makedirs(dn)
if not os.path.isdir(dn):
print("unable to create %s"%dn)
return False
return True
def isCurrent(infile,outfile):
if os.path.exists(outfile):
otime=os.path.getmtime(outfile)
itime=os.path.getmtime(inFile)
itime=os.path.getmtime(infile)
if (otime >= itime):
print("%s is newer then %s, no need to recreate"%(outfile,inFile))
return
print("%s is newer then %s, no need to recreate"%(outfile,infile))
return True
return False
def compressFile(inFile):
outfile=os.path.basename(inFile)+".gz"
inFile=os.path.join(basePath(),inFile)
outfile=os.path.join(outPath(),outfile)
if isCurrent(inFile,outfile):
return
with open(inFile, 'rb') as f_in:
with gzip.open(outfile, 'wb') as f_out:
shutil.copyfileobj(f_in, f_out)
def generateCfg():
outfile=os.path.join(outPath(),CFG_INCLUDE)
infile=os.path.join(basePath(),CFG_FILE)
if isCurrent(infile,outfile):
return
print("creating %s"%CFG_INCLUDE)
with open(CFG_FILE,'rb') as ch:
config=json.load(ch)
with open(outfile,'w') as oh:
oh.write("//generated from %s\n"%CFG_FILE)
oh.write('#include "GwConfigItem.h"\n')
l=len(config)
oh.write('class GwConfigDefinitions{\n')
oh.write(' public:\n')
oh.write(' int getNumConfig() const{return %d;}\n'%(l))
for item in config:
n=item.get('name')
if n is None:
continue
oh.write(' const String %s=F("%s");\n'%(n,n))
oh.write(' protected:\n')
oh.write(' GwConfigItem *configs[%d]={\n'%(l))
first=True
for item in config:
if not first:
oh.write(',\n')
first=False
oh.write(" new GwConfigItem(%s,\"%s\")"%(item.get('name'),item.get('default')))
oh.write('};\n')
oh.write('};\n')
oh.close()
if not checkDir():
sys.exit(1)
for f in FILES:
print("compressing %s"%f)
compressFile(f)
compressFile(f)
generateCfg()

View File

@ -54,7 +54,7 @@ GwConfigInterface * GwConfigHandler::getConfigItem(const String name, bool dummy
return &dummyConfig;
}
#define PREF_NAME "gwprefs"
GwConfigHandler::GwConfigHandler(GwLog *logger){
GwConfigHandler::GwConfigHandler(GwLog *logger): GwConfigDefinitions(){
this->logger=logger;
}
bool GwConfigHandler::loadConfig(){

View File

@ -3,74 +3,16 @@
#include <Arduino.h>
#include <Preferences.h>
#include "GwLog.h"
class GwConfigInterface{
public:
virtual String asString() const=0;
virtual const char * asCString() const =0;
virtual bool asBoolean() const = 0;
virtual int asInt() const = 0;
};
class GwConfigItem: public GwConfigInterface{
private:
String name;
String initialValue;
String value;
public:
GwConfigItem(const String &name, const String initialValue){
this->name=name;
this->initialValue=initialValue;
this->value=initialValue;
}
virtual String asString() const{
return value;
}
virtual const char * asCString() const{
return value.c_str();
};
virtual void fromString(const String v){
value=v;
};
virtual bool asBoolean() const{
return strcasecmp(value.c_str(),"true") == 0;
}
virtual int asInt() const{
return (int)value.toInt();
}
String getName() const{
return name;
}
virtual void reset(){
value=initialValue;
}
bool changed() const{
return value != initialValue;
}
String getDefault() const {
return initialValue;
}
};
#include "GwConfigItem.h"
#include "GwConfigDefinitions.h"
class GwConfigHandler{
class GwConfigHandler: public GwConfigDefinitions{
private:
Preferences prefs;
GwLog *logger;
public:
public:
const String sendUsb=F("sendUsb");
const String receiveUsb=F("receiveUsb");
const String wifiClient=F("wifiClient");
const String wifiPass=F("wifiPass");
const String wifiSSID=F("wifiSSID");
const String serverPort=F("serverPort");
const String maxClients=F("maxClients");
const String sendTCP=F("sendTCP");
const String readTCP=F("receiveTCP");
const String sendSeasmart=F("sendSeasmart");
const String usbBaud=F("usbBaud");
const String systemName=F("systemName");
const String stopApTime=F("stopApTime");
GwConfigHandler(GwLog *logger);
bool loadConfig();
bool saveConfig();
@ -85,23 +27,5 @@ class GwConfigHandler{
GwConfigItem * findConfig(const String name, bool dummy=false);
GwConfigInterface * getConfigItem(const String name, bool dummy=false) const;
private:
GwConfigItem* configs[13]={
new GwConfigItem(sendUsb,"true"),
new GwConfigItem (receiveUsb,"false"),
new GwConfigItem (wifiClient,"false"),
new GwConfigItem (wifiSSID,""),
new GwConfigItem (wifiPass,""),
new GwConfigItem (serverPort,"2222"),
new GwConfigItem (maxClients, "10"),
new GwConfigItem (sendTCP,"true"),
new GwConfigItem (readTCP,"true"),
new GwConfigItem (sendSeasmart,"false"),
new GwConfigItem (usbBaud,"115200"),
new GwConfigItem (systemName,"ESP32NMEA2K"),
new GwConfigItem (stopApTime,"0")
};
int getNumConfig() const{
return 13;
}
};
#endif

52
lib/config/GwConfigItem.h Normal file
View File

@ -0,0 +1,52 @@
#ifndef _GWCONFIGITEM_H
#define _GWCONFIGITEM_H
#include "WString.h"
class GwConfigInterface{
public:
virtual String asString() const=0;
virtual const char * asCString() const =0;
virtual bool asBoolean() const = 0;
virtual int asInt() const = 0;
};
class GwConfigItem: public GwConfigInterface{
private:
String name;
String initialValue;
String value;
public:
GwConfigItem(const String &name, const String initialValue){
this->name=name;
this->initialValue=initialValue;
this->value=initialValue;
}
virtual String asString() const{
return value;
}
virtual const char * asCString() const{
return value.c_str();
};
virtual void fromString(const String v){
value=v;
};
virtual bool asBoolean() const{
return strcasecmp(value.c_str(),"true") == 0;
}
virtual int asInt() const{
return (int)value.toInt();
}
String getName() const{
return name;
}
virtual void reset(){
value=initialValue;
}
bool changed() const{
return value != initialValue;
}
String getDefault() const {
return initialValue;
}
};
#endif

View File

@ -31,10 +31,11 @@
N2kDataToNMEA0183::N2kDataToNMEA0183(GwLog * logger, GwBoatData *boatData, tNMEA2000 *NMEA2000, tNMEA0183 *NMEA0183) : tNMEA2000::tMsgHandler(0,NMEA2000){
N2kDataToNMEA0183::N2kDataToNMEA0183(GwLog * logger, GwBoatData *boatData, tNMEA2000 *NMEA2000, tNMEA0183 *NMEA0183, int id)
: tNMEA2000::tMsgHandler(0,NMEA2000){
SendNMEA0183MessageCallback=0;
pNMEA0183=NMEA0183;
sourceId=id;
}
@ -46,10 +47,11 @@ void N2kDataToNMEA0183::loop() {
//*****************************************************************************
void N2kDataToNMEA0183::SendMessage(const tNMEA0183Msg &NMEA0183Msg) {
if ( pNMEA0183 != 0 ) pNMEA0183->SendMessage(NMEA0183Msg);
if ( SendNMEA0183MessageCallback != 0 ) SendNMEA0183MessageCallback(NMEA0183Msg);
if ( SendNMEA0183MessageCallback != 0 ) SendNMEA0183MessageCallback(NMEA0183Msg, sourceId);
}
N2kDataToNMEA0183* N2kDataToNMEA0183::create(GwLog *logger, GwBoatData *boatData, tNMEA2000 *NMEA2000, tNMEA0183 *NMEA0183){
return new N2kToNMEA0183Functions(logger,boatData,NMEA2000,NMEA0183);
N2kDataToNMEA0183* N2kDataToNMEA0183::create(GwLog *logger, GwBoatData *boatData, tNMEA2000 *NMEA2000,
tNMEA0183 *NMEA0183, int sourceId){
return new N2kToNMEA0183Functions(logger,boatData,NMEA2000,NMEA0183, sourceId);
}
//*****************************************************************************

View File

@ -33,22 +33,19 @@ OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
class N2kDataToNMEA0183 : public tNMEA2000::tMsgHandler
{
public:
using tSendNMEA0183MessageCallback = void (*)(const tNMEA0183Msg &NMEA0183Msg);
using tSendNMEA0183MessageCallback = void (*)(const tNMEA0183Msg &NMEA0183Msg, int id);
protected:
GwLog *logger;
GwBoatData *boatData;
tNMEA0183 *pNMEA0183;
int sourceId;
tSendNMEA0183MessageCallback SendNMEA0183MessageCallback;
void SendMessage(const tNMEA0183Msg &NMEA0183Msg);
N2kDataToNMEA0183(GwLog *logger, GwBoatData *boatData, tNMEA2000 *NMEA2000, tNMEA0183 *NMEA0183);
N2kDataToNMEA0183(GwLog *logger, GwBoatData *boatData, tNMEA2000 *NMEA2000, tNMEA0183 *NMEA0183, int sourceId);
public:
static N2kDataToNMEA0183* create(GwLog *logger, GwBoatData *boatData, tNMEA2000 *NMEA2000, tNMEA0183 *NMEA0183);
static N2kDataToNMEA0183* create(GwLog *logger, GwBoatData *boatData, tNMEA2000 *NMEA2000, tNMEA0183 *NMEA0183, int sourceId);
virtual void HandleMsg(const tN2kMsg &N2kMsg) = 0;
void SetSendNMEA0183MessageCallback(tSendNMEA0183MessageCallback _SendNMEA0183MessageCallback)
{

View File

@ -849,7 +849,8 @@ private:
}
public:
N2kToNMEA0183Functions(GwLog *logger, GwBoatData *boatData, tNMEA2000 *NMEA2000, tNMEA0183 *NMEA0183) : N2kDataToNMEA0183(logger, boatData, NMEA2000, NMEA0183)
N2kToNMEA0183Functions(GwLog *logger, GwBoatData *boatData, tNMEA2000 *NMEA2000, tNMEA0183 *NMEA0183, int sourceId)
: N2kDataToNMEA0183(logger, boatData, NMEA2000, NMEA0183,sourceId)
{
LastPosSend = 0;
lastLoopTime = 0;

View File

@ -6,10 +6,9 @@ void GwBuffer::lp(const char *fkt, int p)
fkt, buffer, offset(writePointer), offset(readPointer), usedSpace(), freeSpace(), p);
}
GwBuffer::GwBuffer(GwLog *logger,size_t bufferSize, bool rotate)
GwBuffer::GwBuffer(GwLog *logger,size_t bufferSize)
{
this->logger = logger;
this->rotate=rotate;
this->bufferSize=bufferSize;
this->buffer=new uint8_t[bufferSize];
writePointer = buffer;
@ -26,67 +25,45 @@ void GwBuffer::reset()
}
size_t GwBuffer::freeSpace()
{
if (! rotate){
return bufferSize-offset(writePointer)-1;
if (readPointer <= writePointer){
return readPointer+bufferSize-writePointer-1;
}
if (readPointer < writePointer)
{
size_t rt = bufferSize - offset(writePointer) - 1 + offset(readPointer);
return rt;
}
if (readPointer == writePointer)
return bufferSize - 1;
return readPointer - writePointer - 1;
}
size_t GwBuffer::usedSpace()
{
if (readPointer == writePointer)
return 0;
if (readPointer < writePointer)
if (readPointer <= writePointer)
return writePointer - readPointer;
return bufferSize - offset(readPointer) + offset(writePointer);
return writePointer+bufferSize-readPointer;
}
size_t GwBuffer::addData(const uint8_t *data, size_t len)
size_t GwBuffer::addData(const uint8_t *data, size_t len, bool addPartial)
{
lp("addDataE", len);
if (len == 0)
return 0;
if (freeSpace() < len && rotate)
//in rotating mode (send buffer)
//we only fill in a message if it fit's completely
if (freeSpace() < len && !addPartial)
return 0;
size_t written = 0;
if (writePointer >= readPointer)
{
written = bufferSize - offset(writePointer) - 1;
bool canRotate=rotate && offset(readPointer) > 0;
if (canRotate) written++; //we can also fill the last byte
if (written > len)
written = len;
if (written)
{
memcpy(writePointer, data, written);
len -= written;
data += written;
writePointer += written;
if (offset(writePointer) > (bufferSize - 1))
writePointer = buffer;
size_t written = 0;
for (int i=0;i<2;i++){
size_t currentFree=freeSpace();
size_t toWrite=len-written;
if (toWrite > currentFree) toWrite=currentFree;
if (toWrite > (bufferSize - offset(writePointer))) {
toWrite=bufferSize - offset(writePointer);
}
lp("addData1", written);
if (len <= 0)
{
return written;
if (toWrite != 0){
memcpy(writePointer, data, toWrite);
written+=toWrite;
data += toWrite;
writePointer += toWrite;
if (offset(writePointer) >= bufferSize){
writePointer -= bufferSize;
}
}
if (! rotate) return written;
lp("addData1", toWrite);
}
//now we have the write pointer before the read pointer
int maxLen=readPointer-writePointer-1;
if (maxLen <= 0) return written;
if (len < maxLen) maxLen=len;
memcpy(writePointer, data, maxLen);
writePointer += maxLen;
lp("addData2", maxLen);
return maxLen + written;
lp("addData2", written);
return written;
}
/**
* write some data to the buffer writer
@ -94,92 +71,66 @@ size_t GwBuffer::addData(const uint8_t *data, size_t len)
*/
GwBuffer::WriteStatus GwBuffer::fetchData(GwBufferWriter *writer, int maxLen,bool errorIf0 )
{
lp("fetchDataE");
lp("fetchDataE",maxLen);
size_t len = usedSpace();
if (maxLen > 0 && len > maxLen) len=maxLen;
if (len == 0)
if (len == 0){
lp("fetchData0",maxLen);
writer->done();
return OK;
}
size_t written = 0;
size_t plen = len;
if (writePointer < readPointer)
{
//we need to write from readPointer till end and then till writePointer-1
plen = bufferSize - offset(readPointer) - 1;
int rt = writer->write(readPointer, plen);
lp("fetchData1", rt);
if (rt < 0)
{
LOG_DEBUG(GwLog::DEBUG + 1, "buffer: write returns error %d", rt);
return ERROR;
for (int i=0;i<2;i++){
size_t currentUsed=usedSpace();
size_t toWrite=len-written;
if (toWrite > currentUsed) toWrite=currentUsed;
if (toWrite > (bufferSize - offset(readPointer))) {
toWrite=bufferSize - offset(readPointer);
}
if (rt > plen)
lp("fetchData1", toWrite);
if (toWrite > 0)
{
LOG_DEBUG(GwLog::DEBUG + 1, "buffer: write too many bytes(1) %d", rt);
return ERROR;
int rt = writer->write(readPointer, toWrite);
lp("fetchData2", rt);
if (rt < 0)
{
LOG_DEBUG(GwLog::DEBUG + 1, "buffer: write returns error %d", rt);
writer->done();
return ERROR;
}
if (rt > toWrite)
{
LOG_DEBUG(GwLog::DEBUG + 1, "buffer: write too many bytes(1) %d", rt);
writer->done();
return ERROR;
}
readPointer += rt;
if (offset(readPointer) >= bufferSize)
readPointer -= bufferSize;
written += rt;
if (rt == 0) break; //no need to try again
}
if (rt == 0)
{
LOG_DEBUG(GwLog::DEBUG + 1, "buffer: write returns 0 (1)");
return (errorIf0 ? ERROR : AGAIN);
}
readPointer += rt;
if (offset(readPointer) > (bufferSize - 1))
readPointer = buffer;
if (rt < plen)
return AGAIN;
if (plen >= len)
return OK;
len -= rt;
written += rt;
//next part - readPointer should be at buffer now
}
plen = writePointer - readPointer;
if (plen == 0)
return OK;
int rt = writer->write(readPointer, plen);
lp("fetchData2", rt);
if (rt < 0)
{
LOG_DEBUG(GwLog::DEBUG + 1, "buffer: write returns error %d", rt);
return ERROR;
}
if (rt == 0)
{
LOG_DEBUG(GwLog::DEBUG + 1, "buffer: write returns 0 (1)");
writer->done();
if (written == 0){
return (errorIf0 ? ERROR : AGAIN);
}
if (rt > plen)
{
LOG_DEBUG(GwLog::DEBUG + 1, "buffer: write too many bytes(2)");
return ERROR;
}
readPointer += rt;
if (offset(readPointer) > (bufferSize - 1))
readPointer = buffer;
lp("fetchData3");
written += rt;
if (written < len)
return AGAIN;
return OK;
return (written == len)?OK:AGAIN;
}
int GwBuffer::findChar(char x){
lp("findChar",x);
int offset=0;
int of=0;
uint8_t *p;
for (p=readPointer; p != writePointer && p < (buffer+bufferSize);p++){
if (*p == x) return offset;
offset++;
}
if (p >= (buffer+bufferSize)){
//we reached the end of the buffer without "hitting" the write pointer
//so we can start from the beginning if rotating...
if (! rotate) return -1;
for (p=buffer;p < writePointer && p < (buffer+bufferSize);p++){
if (*p == x) return offset;
offset++;
for (p=readPointer; of < usedSpace();p++){
if (offset(p) >= bufferSize) p -=bufferSize;
if (*p == x) {
lp("findChar1",of);
return of;
}
of++;
}
lp("findChar2");
return -1;
}
@ -193,22 +144,5 @@ GwBuffer::WriteStatus GwBuffer::fetchMessage(GwBufferWriter *writer,char delimit
}
return AGAIN;
}
if (! rotate){
//in a non rotating buffer we discard the found message
//and copy the remain to the start
int len=pos+1;
int wr=writer->write(readPointer,len);
//in any case discard the data now
int remain=usedSpace()-len;
if (remain > 0){
memcpy(buffer,readPointer+len,remain);
readPointer=buffer;
writePointer=buffer+remain;
}
else{
reset();
}
return (wr == len)?OK:ERROR;
}
return fetchData(writer,pos+1,true);
}

View File

@ -8,6 +8,7 @@ class GwBufferWriter{
public:
int id=0; //can be set be users
virtual int write(const uint8_t *buffer,size_t len)=0;
virtual void done(){}
virtual ~GwBufferWriter(){};
};
@ -18,7 +19,8 @@ class GwBufferWriter{
*/
class GwBuffer{
public:
static const size_t 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=200; // enough for 1 NMEA message...
typedef enum {
OK,
ERROR,
@ -26,7 +28,6 @@ class GwBuffer{
} WriteStatus;
private:
size_t bufferSize;
bool rotate;
uint8_t *buffer;
uint8_t *writePointer;
uint8_t *readPointer;
@ -40,12 +41,12 @@ class GwBuffer{
*/
int findChar(char x);
public:
GwBuffer(GwLog *logger,size_t bufferSize,bool rotate=true);
GwBuffer(GwLog *logger,size_t bufferSize);
~GwBuffer();
void reset();
size_t freeSpace();
size_t usedSpace();
size_t addData(const uint8_t *data,size_t len);
size_t addData(const uint8_t *data,size_t len,bool addPartial=false);
/**
* write some data to the buffer writer
* return an error if the buffer writer returned < 0

View File

@ -13,17 +13,24 @@ class SerialWriter : public GwBufferWriter{
};
GwSerial::GwSerial(GwLog *logger, uart_port_t num)
GwSerial::GwSerial(GwLog *logger, uart_port_t num, int id,bool allowRead)
{
this->id=id;
this->logger = logger;
this->num = num;
this->buffer = new GwBuffer(logger,1600);
this->buffer = new GwBuffer(logger,GwBuffer::TX_BUFFER_SIZE);
this->writer = new SerialWriter(num);
this->allowRead=allowRead;
if (allowRead){
this->readBuffer=new GwBuffer(logger, GwBuffer::RX_BUFFER_SIZE);
}
}
GwSerial::~GwSerial()
{
delete buffer;
delete writer;
if (readBuffer) delete readBuffer;
}
int GwSerial::setup(int baud, int rxpin, int txpin)
{
@ -54,13 +61,32 @@ int GwSerial::setup(int baud, int rxpin, int txpin)
bool GwSerial::isInitialized() { return initialized; }
size_t GwSerial::enqueue(const uint8_t *data, size_t len)
{
if (! isInitialized()) return 0;
return buffer->addData(data, len);
}
GwBuffer::WriteStatus GwSerial::write(){
if (! isInitialized()) return GwBuffer::ERROR;
return buffer->fetchData(writer,false);
}
const char *GwSerial::read(){
void GwSerial::sendToClients(const char *buf,int sourceId){
if ( sourceId == id) return;
size_t len=strlen(buf);
size_t enqueued=enqueue((const uint8_t*)buf,len);
if (enqueued != len){
LOG_DEBUG(GwLog::DEBUG,"GwSerial overflow on channel %d",id);
overflows++;
}
}
void GwSerial::loop(bool handleRead){
write();
if (! handleRead) return;
char buffer[10];
uart_read_bytes(num,(uint8_t *)(&buffer),10,0);
return NULL;
int rt=uart_read_bytes(num,(uint8_t *)(&buffer),10,0);
if (allowRead & rt > 0){
readBuffer->addData((uint8_t *)(&buffer),rt,true);
}
}
bool GwSerial::readMessages(GwBufferWriter *writer){
if (! allowRead) return false;
return readBuffer->fetchMessage(writer,'\n',true) == GwBuffer::OK;
}

View File

@ -7,19 +7,24 @@ class SerialWriter;
class GwSerial{
private:
GwBuffer *buffer;
GwBuffer *readBuffer;
GwBuffer *readBuffer=NULL;
GwLog *logger;
SerialWriter *writer;
uart_port_t num;
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);
public:
static const int bufferSize=200;
GwSerial(GwLog *logger,uart_port_t num);
GwSerial(GwLog *logger,uart_port_t num,int id,bool allowRead=true);
~GwSerial();
int setup(int baud,int rxpin,int txpin);
bool isInitialized();
size_t enqueue(const uint8_t *data, size_t len);
GwBuffer::WriteStatus write();
const char *read();
void sendToClients(const char *buf,int sourceId);
void loop(bool handleRead=true);
bool readMessages(GwBufferWriter *writer);
};
#endif

View File

@ -3,8 +3,6 @@
#include <lwip/sockets.h>
#include "GwBuffer.h"
#define WRITE_BUFFER_SIZE 1600
#define READ_BUFFER_SIZE 200
class Writer : public GwBufferWriter{
public:
wiFiClientPtr client;
@ -63,9 +61,9 @@ class GwClient{
this->client=client;
this->logger=logger;
this->allowRead=allowRead;
buffer=new GwBuffer(logger,WRITE_BUFFER_SIZE);
buffer=new GwBuffer(logger,GwBuffer::TX_BUFFER_SIZE);
if (allowRead){
readBuffer=new GwBuffer(logger,READ_BUFFER_SIZE,false);
readBuffer=new GwBuffer(logger,GwBuffer::RX_BUFFER_SIZE);
}
overflows=0;
if (client != NULL){
@ -177,7 +175,7 @@ void GwSocketServer::begin(){
MDNS.addService("_nmea-0183","_tcp",config->getInt(config->serverPort));
}
void GwSocketServer::loop()
void GwSocketServer::loop(bool handleRead)
{
WiFiClient client = server->available(); // listen for incoming clients
@ -230,7 +228,7 @@ void GwSocketServer::loop()
}
else
{
client->read();
if (handleRead) client->read();
}
}
}
@ -247,12 +245,6 @@ bool GwSocketServer::readMessages(GwBufferWriter *writer){
}
void GwSocketServer::sendToClients(const char *buf,int source){
int len=strlen(buf);
char buffer[len+2];
memcpy(buffer,buf,len);
buffer[len]=0x0d;
len++;
buffer[len]=0x0a;
len++;
int sourceIndex=source-minId;
for (int i = 0; i < maxClients; i++)
{
@ -260,7 +252,7 @@ void GwSocketServer::sendToClients(const char *buf,int source){
gwClientPtr client = clients[i];
if (! client->hasClient()) continue;
if ( client->client->connected() ) {
bool rt=client->enqueue((uint8_t*)buffer,len);
bool rt=client->enqueue((uint8_t*)buf,len);
if (!rt){
LOG_DEBUG(GwLog::DEBUG,"overflow in send to %s",client->remoteIp.c_str());
}

View File

@ -22,7 +22,7 @@ class GwSocketServer{
GwSocketServer(const GwConfigHandler *config,GwLog *logger,int minId);
~GwSocketServer();
void begin();
void loop();
void loop(bool handleRead=true);
void sendToClients(const char *buf,int sourceId);
int numClients();
bool readMessages(GwBufferWriter *writer);

View File

@ -41,7 +41,7 @@ bool GwWifi::connectInternal(){
}
return false;
}
#define RETRY_MILLIS 5000
#define RETRY_MILLIS 10000
void GwWifi::loop(){
if (wifiClient->asBoolean() && ! clientConnected()){
long now=millis();

View File

@ -18,26 +18,35 @@ lib_deps =
bblanchon/ArduinoJson@^6.18.5
ottowinter/ESPAsyncWebServer-esphome@^2.0.1
board_build.embed_files =
web/index.html.gz
generated/index.html.gz
generated/config.json.gz
extra_scripts = extra_script.py
build_flags=
-Igenerated
[env:m5stack-atom]
board = m5stack-atom
lib_deps =
${env.lib_deps}
build_flags = -D BOARD_M5ATOM
build_flags =
-D BOARD_M5ATOM
${env.build_flags}
upload_port = /dev/esp32
[env:m5stack-atom-canunit]
board = m5stack-atom
lib_deps =
${env.lib_deps}
build_flags = -D BOARD_M5ATOM_CANUNIT
build_flags =
-D BOARD_M5ATOM_CANUNIT
${env.build_flags}
upload_port = /dev/esp32
[env:m5stickc-atom-canunit]
board = m5stack-atom
lib_deps =
${env.lib_deps}
build_flags = -D BOARD_M5STICK_CANUNIT -D HAS_RTC -D HAS_M5LCD
build_flags =
-D BOARD_M5STICK_CANUNIT -D HAS_RTC -D HAS_M5LCD
${env.build_flags}
upload_port = /dev/esp32

View File

@ -12,7 +12,7 @@
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#define VERSION "0.1.0"
#define VERSION "0.1.2"
#include "GwHardware.h"
#include <Arduino.h>
@ -65,7 +65,7 @@ Preferences preferences; // Nonvolatile storage on ESP32 - To store
bool SendNMEA0183Conversion = true; // Do we send NMEA2000 -> NMEA0183 conversion
bool SendSeaSmart = false; // Do we send NMEA2000 messages in SeaSmart format
N2kDataToNMEA0183 *nmea0183Converter=N2kDataToNMEA0183::create(&logger, &boatData,&NMEA2000, 0);
N2kDataToNMEA0183 *nmea0183Converter=N2kDataToNMEA0183::create(&logger, &boatData,&NMEA2000, 0, N2K_CHANNEL_ID);
// Set the information for other bus devices, which messages we support
const unsigned long TransmitMessages[] PROGMEM = {127489L, // Engine dynamic
@ -73,7 +73,7 @@ const unsigned long TransmitMessages[] PROGMEM = {127489L, // Engine dynamic
};
// Forward declarations
void HandleNMEA2000Msg(const tN2kMsg &N2kMsg);
void SendNMEA0183Message(const tNMEA0183Msg &NMEA0183Msg);
void SendNMEA0183Message(const tNMEA0183Msg &NMEA0183Msg,int id);
AsyncWebServer webserver(80);
@ -135,11 +135,39 @@ void handleAsyncWebRequest(AsyncWebServerRequest *request, RequestMessage *msg,
}
#define JSON_OK "{\"status\":\"OK\"}"
//embedded files
extern const uint8_t indexFile[] asm("_binary_web_index_html_gz_start");
extern const uint8_t indexFileEnd[] asm("_binary_web_index_html_gz_end");
extern const uint8_t indexFileLen[] asm("_binary_web_index_html_gz_size");
class EmbeddedFile;
static std::map<String,EmbeddedFile*> embeddedFiles;
class EmbeddedFile {
public:
const uint8_t *start;
int len;
EmbeddedFile(String name,const uint8_t *start,int len){
this->start=start;
this->len=len;
embeddedFiles[name]=this;
}
} ;
#define EMBED_GZ_FILE(fileName, fileExt) \
extern const uint8_t fileName##_##fileExt##_File[] asm("_binary_generated_" #fileName "_" #fileExt "_gz_start"); \
extern const uint8_t fileName##_##fileExt##_FileLen[] asm("_binary_generated_" #fileName "_" #fileExt "_gz_size"); \
const EmbeddedFile fileName##_##fileExt##_Config(#fileName "." #fileExt,(const uint8_t*)fileName##_##fileExt##_File,(int)fileName##_##fileExt##_FileLen);
EMBED_GZ_FILE(index,html)
EMBED_GZ_FILE(config,json)
void sendEmbeddedFile(String name,String contentType,AsyncWebServerRequest *request){
std::map<String,EmbeddedFile*>::iterator it=embeddedFiles.find(name);
if (it != embeddedFiles.end()){
EmbeddedFile* found=it->second;
AsyncWebServerResponse *response=request->beginResponse_P(200,contentType,found->start,found->len);
response->addHeader(F("Content-Encoding"), F("gzip"));
request->send(response);
}
else{
request->send(404, "text/plain", "Not found");
}
}
String js_status(){
int numPgns=nmea0183Converter->numPgns();
@ -165,15 +193,24 @@ GwConfigInterface *sendTCP=NULL;
GwConfigInterface *sendSeasmart=NULL;
GwConfigInterface *systemName=NULL;
GwSerial usbSerial(&logger, UART_NUM_0);
GwSerial usbSerial(&logger, UART_NUM_0, USB_CHANNEL_ID);
class GwSerialLog : public GwLogWriter{
public:
virtual ~GwSerialLog(){}
virtual void write(const char *data){
usbSerial.enqueue((const uint8_t*)data,strlen(data)); //ignore any errors
usbSerial.sendToClients(data,-1); //ignore any errors
}
};
void delayedRestart(){
xTaskCreate([](void *p){
delay(500);
ESP.restart();
vTaskDelete(NULL);
},"reset",1000,NULL,0,NULL);
}
void setup() {
uint8_t chipid[6];
@ -212,13 +249,14 @@ void setup() {
// Start Web Server
webserver.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
AsyncWebServerResponse *response=request->beginResponse_P(200,"text/html",(const uint8_t *)indexFile,(int)indexFileLen);
response->addHeader(F("Content-Encoding"), F("gzip"));
request->send(response);
sendEmbeddedFile("index.html","text/html",request);
});
webserver.on("/config.json", HTTP_GET, [](AsyncWebServerRequest *request){
sendEmbeddedFile("config.json","application/json",request);
});
webserver.on("/api/reset", HTTP_GET,[](AsyncWebServerRequest *request){
logger.logDebug(GwLog::LOG,"Reset Button");
ESP.restart();
delayedRestart();
});
class StatusRequest : public RequestMessage{
public:
@ -268,8 +306,7 @@ void setup() {
result=JSON_OK;
logger.logString("update config and restart");
config.saveConfig();
delay(100);
ESP.restart();
delayedRestart();
}
else{
DynamicJsonDocument rt(100);
@ -295,8 +332,7 @@ void setup() {
config.reset(true);
logger.logString("reset config, restart");
result=JSON_OK;
delay(100);
ESP.restart();
delayedRestart();
}
};
webserver.on("/api/resetConfig",HTTP_GET,[](AsyncWebServerRequest *request){
@ -385,45 +421,82 @@ void HandleNMEA2000Msg(const tN2kMsg &N2kMsg) {
socketServer.sendToClients(buf,N2K_CHANNEL_ID);
}
//*****************************************************************************
void SendNMEA0183Message(const tNMEA0183Msg &NMEA0183Msg) {
if ( ! sendTCP->asBoolean() && ! sendUsb->asBoolean() ) return;
char buf[MAX_NMEA0183_MESSAGE_SIZE];
if ( !NMEA0183Msg.GetMessage(buf, MAX_NMEA0183_MESSAGE_SIZE) ) return;
void sendBufferToChannels(const char * buffer, int sourceId){
if (sendTCP->asBoolean()){
socketServer.sendToClients(buf,N2K_CHANNEL_ID);
socketServer.sendToClients(buffer,sourceId);
}
if (sendUsb->asBoolean()){
int len=strlen(buf);
if (len >= (MAX_NMEA0183_MESSAGE_SIZE -2)) return;
buf[len]=0x0d;
len++;
buf[len]=0x0a;
len++;
buf[len]=0;
usbSerial.enqueue((const uint8_t*)buf,len);
usbSerial.sendToClients(buffer,sourceId);
}
}
//*****************************************************************************
void SendNMEA0183Message(const tNMEA0183Msg &NMEA0183Msg, int sourceId) {
if ( ! sendTCP->asBoolean() && ! sendUsb->asBoolean() ) return;
char buf[MAX_NMEA0183_MESSAGE_SIZE+3];
if ( !NMEA0183Msg.GetMessage(buf, MAX_NMEA0183_MESSAGE_SIZE) ) return;
size_t len=strlen(buf);
buf[len]=0x0d;
buf[len+1]=0x0a;
buf[len+2]=0;
sendBufferToChannels(buf,sourceId);
}
void handleReceivedNmeaMessage(const char *buf, int sourceId){
//TODO - for now only send out again
//add the conversion to N2K here
sendBufferToChannels(buf,sourceId);
}
void handleSendAndRead(bool handleRead){
socketServer.loop(handleRead);
usbSerial.loop(handleRead);
}
class NMEAMessageReceiver : public GwBufferWriter{
uint8_t buffer[GwBuffer::RX_BUFFER_SIZE+4];
uint8_t *writePointer=buffer;
public:
virtual int write(const uint8_t *buffer,size_t len){
char nbuf[len+1];
memcpy(nbuf,buffer,len);
nbuf[len]=0;
logger.logDebug(GwLog::DEBUG,"NMEA[%d]: %s",id,nbuf);
return len;
size_t toWrite=GwBuffer::RX_BUFFER_SIZE-(writePointer-buffer);
if (toWrite > len) toWrite=len;
memcpy(writePointer,buffer,toWrite);
writePointer+=toWrite;
*writePointer=0;
return toWrite;
}
virtual void done(){
if (writePointer == buffer) return;
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;
}
};
void loop() {
gwWifi.loop();
socketServer.loop();
if (usbSerial.write() == GwBuffer::ERROR){
//logger.logDebug(GwLog::DEBUG,"overflow in USB serial");
}
handleSendAndRead(true);
NMEA2000.ParseMessages();
int SourceAddress = NMEA2000.GetN2kSource();
@ -447,6 +520,6 @@ void loop() {
socketServer.readMessages(&receiver);
//read channels
usbSerial.read();
usbSerial.readMessages(&receiver);
}

View File

@ -16,15 +16,19 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler):
apiurl=self.server.proxyUrl
url=apiurl+p.replace("/api","")
print("proxy to %s"%url)
with urllib.request.urlopen(url) as response:
self.send_response(http.HTTPStatus.OK)
self.send_header("Content-type", response.getheader("Content-type"))
try:
with urllib.request.urlopen(url,timeout=10) as response:
self.send_response(http.HTTPStatus.OK)
self.send_header("Content-type", response.getheader("Content-type"))
self.end_headers()
shutil.copyfileobj(response,self.wfile)
self.end_headers()
shutil.copyfileobj(response,self.wfile)
return None
self.send_error(http.HTTPStatus.NOT_FOUND, "api not found")
return None
except Exception as e:
self.send_error(http.HTTPStatus.INTERNAL_SERVER_ERROR, "api error %s"%str(e))
return None
self.send_error(http.HTTPStatus.NOT_FOUND, "api not found")
return None
super().do_GET()
def translate_path(self, path):
"""Translate a /-separated PATH to the local filename syntax.

99
web/config.json Normal file
View File

@ -0,0 +1,99 @@
[
{
"name": "systemName",
"label": "system name",
"type": "string",
"default": "ESP32NMEA2K",
"check": "checkSystemName",
"description": "system name, used for the access point and for services"
},
{
"name": "usbBaud",
"label": "USB baud rate",
"type": "list",
"default": "115200",
"description": "baud rate for the USB port",
"list": [1200,2400,4800,9600,14400,19200,28800,38400,57600,115200,230400,460800]
},
{
"name": "sendUsb",
"label": "NMEA to USB",
"type": "boolean",
"default": "true",
"description": "send out NMEA data on the USB port"
},
{
"name": "receiveUsb",
"label": "NMEA from USB",
"type": "boolean",
"default": "true",
"description": "receive NMEA data on the USB port"
},
{
"name": "serverPort",
"label": "TCP port",
"type": "number",
"default": "2222",
"description": "the TCP port we listen on"
},
{
"name": "maxClients",
"label": "max. TCP clients",
"type": "number",
"default": "10",
"check":"checkMaxClients",
"description": "the number of clients we allow to connect to us"
},
{
"name": "sendTCP",
"label": "NMEA to TCP",
"type": "boolean",
"default": "true",
"description": "send out NMEA data to connected TCP clients"
},
{
"name": "readTCP",
"label": "NMEA from TCP",
"type": "boolean",
"default": "true",
"description": "receive NMEA data from connected TCP clients"
},
{
"name": "sendSeasmart",
"label": "Seasmart to TCP",
"type": "boolean",
"default": "false",
"description": "send NMEA2000 as seasmart to connected TCP clients"
},
{
"name": "wifiClient",
"label": "wifi client",
"type": "boolean",
"default": "false",
"description": "connect to an external WIFI network"
},
{
"name": "wifiPass",
"label": "wifi client password",
"type": "password",
"default": "",
"description": "the password for an external WIFI network"
},
{
"name": "wifiSSID",
"label": "wifi client SSID",
"type": "string",
"default": "",
"check": "checkSSID",
"description": "the SSID for an external WIFI network"
},
{
"name": "stopApTime",
"type": "number",
"default": "0",
"check": "checkStopApTime",
"description": "stop the access point after that many minutes if not used"
}
]

View File

@ -5,6 +5,7 @@
<title>NMEA 2000 Gateway</title>
<script type="text/javascript">
let self=this;
let lastUpdate=(new Date()).getTime();
function alertRestart(){
alert("Board reset triggered, reconnect WLAN if necessary");
@ -50,15 +51,39 @@
if (el){
let v=jsonData[k];
el.value=v;
el.setAttribute('data-loaded',v);
checkChange(el);
}
}
});
}
function checkMaxClients(v){
let parsed=parseInt(v);
if (isNaN(parsed)) return "not a valid number";
if (parsed < 0) return "must be >= 0";
if (parsed > 10) return "max is 10";
}
function checkSystemName(v){
//2...32 characters for ssid
let allowed=v.replace(/[^a-zA-Z0-9]*/g,'');
if (allowed != v) return "contains invalid characters, only a-z, A-Z, 0-9";
if (v.length < 2 || v.length > 32) return "invalid length (2...32)";
}
function changeConfig(){
let url="/api/setConfig?";
let values=document.querySelectorAll('.configForm select , .configForm input');
for (let i=0;i<values.length;i++){
let v=values[i];
let check=v.getAttribute('data-check');
if (check){
if (typeof(self[check]) === 'function'){
let res=self[check](v.value);
if (res){
alert("invalid config for "+v.getAttribute('name')+"("+v.value+"):\n"+res);
return;
}
}
}
url+=v.getAttribute('name')+"="+encodeURIComponent(v.value)+"&";
}
getJson(url)
@ -107,6 +132,101 @@
}
}
}
function checkChange(el) {
let loaded = el.getAttribute('data-loaded');
if (loaded !== undefined) {
if (loaded != el.value) {
el.classList.add('changed');
}
else {
el.classList.remove("changed");
}
}
}
function createInput(configItem){
let el;
if (configItem.type === 'boolean' || configItem.type === 'list'){
el=document.createElement('select')
el.setAttribute('name',configItem.name)
let slist=[];
if (configItem.list){
configItem.list.forEach(function(v){
slist.push({l:v,v:v});
})
}
else{
slist.push({l:'on',v:'true'})
slist.push({l:'off',v:'false'})
}
slist.forEach(function(sitem){
let sitemEl=document.createElement('option');
sitemEl.setAttribute('value',sitem.v);
sitemEl.textContent=sitem.l;
el.appendChild(sitemEl);
})
return el;
}
el=document.createElement('input');
el.setAttribute('name',configItem.name)
if (configItem.type === 'password'){
el.setAttribute('type','password');
}
else if (configItem.type === 'number'){
el.setAttribute('type','number');
}
else{
el.setAttribute('type','text');
}
return el;
}
let configDefinitions;
function loadConfigDefinitions(){
getJson("config.json")
.then(function(defs){
let frame=document.querySelector('.configFormRows');
if (! frame) throw Error("no config form");
frame.innerHTML='';
configDefinitions=defs;
defs.forEach(function(item){
if (! item.type) return;
let row=document.createElement('div');
row.classList.add('row');
let label=item.label || item.name;
let labelEl=document.createElement('span');
labelEl.classList.add('label');
labelEl.textContent=label;
row.appendChild(labelEl);
let valueEl=createInput(item);
if (!valueEl) return;
valueEl.setAttribute('data-default',item.default);
valueEl.addEventListener('change',function(ev){
let el=ev.target;
checkChange(el);
})
if (item.check) valueEl.setAttribute('data-check',item.check);
row.appendChild(valueEl);
let bt=document.createElement('button');
bt.classList.add('defaultButton');
bt.setAttribute('data-default',item.default);
bt.addEventListener('click',function(ev){
valueEl.value=valueEl.getAttribute('data-default');
checkChange(valueEl);
})
bt.textContent="X";
row.appendChild(bt);
bt=document.createElement('button');
bt.classList.add('infoButton');
bt.addEventListener('click',function(ev){
alert(item.description);
});
bt.textContent="?";
row.appendChild(bt);
frame.appendChild(row);
})
resetForm();
})
.catch(function(err){alert("unable to load config: "+err)})
}
window.setInterval(update,1000);
window.addEventListener('load',function(){
let buttons=document.querySelectorAll('button');
@ -118,7 +238,7 @@
cd.addEventListener('change',function(ev){
showCanDetails(ev.target.checked);
});
resetForm();
loadConfigDefinitions();
});
</script>
<style type="text/css">
@ -130,6 +250,9 @@
border: 1px solid grey;
max-width: 40em;
}
.changed {
color: green
}
span.label {
width: 10em;
display: inline-block;
@ -151,6 +274,10 @@ span#connected.ok{
.buttons {
padding-left: 1em;
}
button.infoButton {
margin-left: 1em;
vertical-align: bottom;
}
#canDetails{
display:none;
}
@ -201,75 +328,8 @@ span#connected.ok{
</div>
<button id="reset" >Reset</button>
<div class="configForm">
<div class="row">
<span class="label">system name</span>
<input name="systemName" type="text">
</div>
<div class="row">
<span class="label">NMEAtoUSB</span>
<select name="sendUsb">
<option value="true">On</option>
<option value="false" selected="selected">Off</option>
</select>
</div>
<!--"1200","2400","4800","9600","14400","19200","28800","38400","57600","115200","230400","460800"-->
<div class="row">
<span class="label">USB baud rate</span>
<select name="usbBaud">
<option value="1200">1200</option>
<option value="2400">2400</option>
<option value="4800">4800</option>
<option value="9600">9600</option>
<option value="14400">14400</option>
<option value="19200">19200</option>
<option value="28800">28800</option>
<option value="38400">38400</option>
<option value="57600">57600</option>
<option value="115200">115200</option>
<option value="230400">230400</option>
<option value="460800">460800</option>
</select>
</div>
<div class="row">
<span class="label">TCP Port</span>
<input name="serverPort" type="number">
</div>
<div class="row">
<span class="label">maxTCPClients</span>
<input name="maxClients" type="number">
</div>
<div class="row">
<span class="label">NMEAtoTCP</span>
<select name="sendTCP">
<option value="true">On</option>
<option value="false" selected="selected">Off</option>
</select>
</div>
<div class="row">
<span class="label">SeasmartToTCP</span>
<select name="sendSeasmart">
<option value="true">On</option>
<option value="false" selected="selected">Off</option>
</select>
</div>
<div class="row">
<span class="label">shutdown AP after (min)</span>
<input name="stopApTime" type="number">
</div>
<div class="row">
<span class="label">wifiClient</span>
<select name="wifiClient">
<option value="true">On</option>
<option value="false" selected="selected">Off</option>
</select>
</div>
<div class="row">
<span class="label">wifiClientPass</span>
<input name="wifiPass" type="text">
</div>
<div class="row">
<span class="label">wifiClientSSID</span>
<input name="wifiSSID" type="text">
<div class="configFormRows">
</div>
<div class="buttons">
<button id="resetForm">ReloadConfig</button>