Merge branch 'master' of github.com:wellenvogel/esp32-nmea2000

This commit is contained in:
andreas 2023-01-09 16:45:15 +01:00
commit ef2a79fc8b
16 changed files with 310 additions and 149 deletions

View File

@ -50,6 +50,20 @@ M5 Atom CAN with M5 RS485 Module
With this set up you get basically all the features from the plain AtomCAN and the Tal485 combined. You still can connect via USB but have the NMEA0183 connection in parallel.
M5 Atom RS485 with M5 CAN Unit
--------------------------------
* Hardware: [ATOM RS485](https://docs.m5stack.com/en/atom/atomic485) + [CAN Unit](http://docs.m5stack.com/en/unit/can)
* Prebuild Binary: m5stack-atom-rs485-canunit-all.bin
* Build Define: BOARD_M5ATOM_RS485_CANUNIT
* Power: 12V via RS485 Module or via USB
M5 Atom RS232 with M5 CAN Unit
--------------------------------
* Hardware: [ATOM RS232](https://docs.m5stack.com/en/atom/atomic232) + [CAN Unit](http://docs.m5stack.com/en/unit/can)
* Prebuild Binary: m5stack-atom-rs232-canunit-all.bin
* Build Define: BOARD_M5ATOM_RS232_CANUNIT
* Power: 12V via RS232 Module or via USB
M5 Stack Atom Canunit
---------------------
* Hardware: [M5_ATOM](http://docs.m5stack.com/en/core/atom_lite) + [CAN Unit](http://docs.m5stack.com/en/unit/can)

View File

@ -7,6 +7,10 @@
#define GW_BOAT_VALUE_LEN 32
#define GWSC(name) static constexpr const __FlashStringHelper* name=F(#name)
//see https://github.com/wellenvogel/esp32-nmea2000/issues/44
//factor to convert from N2k/SI rad/s to current NMEA rad/min
#define ROT_WA_FACTOR 60
class GwJsonDocument;
class GwBoatItemBase{
public:

View File

@ -14,7 +14,7 @@ class FactoryResetRequest: public GwMessage{
protected:
virtual void processImpl(){
api->getLogger()->logDebug(GwLog::LOG,"reset request processing");
api->getConfig()->reset(true);
api->getConfig()->reset();
xTaskCreate([](void *p){
delay(500);
ESP.restart();

View File

@ -1,6 +1,7 @@
#include "GWConfig.h"
#include <ArduinoJson.h>
#include <string.h>
#include <MD5Builder.h>
#define B(v) (v?"true":"false")
@ -53,6 +54,7 @@ GwConfigInterface * GwConfigHandler::getConfigItem(const String name, bool dummy
#define PREF_NAME "gwprefs"
GwConfigHandler::GwConfigHandler(GwLog *logger): GwConfigDefinitions(){
this->logger=logger;
saltBase=esp_random();
}
bool GwConfigHandler::loadConfig(){
prefs.begin(PREF_NAME,true);
@ -63,18 +65,6 @@ bool GwConfigHandler::loadConfig(){
prefs.end();
return true;
}
bool GwConfigHandler::saveConfig(){
prefs.begin(PREF_NAME,false);
for (int i=0;i<getNumConfig();i++){
if (configs[i]->hasChangedValue){
LOG_DEBUG(GwLog::LOG,"saving %s=%s",configs[i]->getName().c_str(),configs[i]->changedValue.c_str());
prefs.putString(configs[i]->getName().c_str(),configs[i]->changedValue);
}
}
prefs.end();
LOG_DEBUG(GwLog::LOG,"saved config");
return true;
}
bool GwConfigHandler::updateValue(String name, String value){
GwConfigInterface *i=getConfigItem(name);
@ -83,18 +73,24 @@ bool GwConfigHandler::updateValue(String name, String value){
LOG_DEBUG(GwLog::LOG,"skip empty password %s",name.c_str());
}
else{
if (i->asString() == value){
return false;
}
LOG_DEBUG(GwLog::LOG,"update config %s=>%s",name.c_str(),i->isSecret()?"***":value.c_str());
i->updateValue(value);
prefs.begin(PREF_NAME,false);
prefs.putString(i->getName().c_str(),value);
prefs.end();
}
return true;
}
bool GwConfigHandler::reset(bool save){
bool GwConfigHandler::reset(){
LOG_DEBUG(GwLog::LOG,"reset config");
prefs.begin(PREF_NAME,false);
for (int i=0;i<getNumConfig();i++){
configs[i]->updateValue(configs[i]->getDefault());
prefs.putString(configs[i]->getName().c_str(),configs[i]->getDefault());
}
if (!save) return true;
return saveConfig();
prefs.end();
return true;
}
String GwConfigHandler::getString(const String name, String defaultv) const{
GwConfigInterface *i=getConfigItem(name,false);
@ -122,6 +118,47 @@ bool GwConfigHandler::setValue(String name,String value){
return true;
}
bool GwConfigHandler::checkPass(String hash){
if (! getBool(useAdminPass)) return true;
String pass=getString(adminPassword);
unsigned long now=millis()/1000UL & ~0x7UL;
MD5Builder builder;
char buffer[2*sizeof(now)+1];
for (int i=0;i< 5 ;i++){
unsigned long base=saltBase+now;
toHex(base,buffer,2*sizeof(now)+1);
builder.begin();
builder.add(buffer);
builder.add(pass);
builder.calculate();
String md5=builder.toString();
bool rt=hash == md5;
logger->logDebug(GwLog::DEBUG,"checking pass %s, base=%ld, hash=%s, res=%d",
hash.c_str(),base,md5.c_str(),(int)rt);
if (rt) return true;
now -= 8;
}
return false;
}
static char hv(uint8_t nibble){
nibble=nibble&0xf;
if (nibble < 10) return (char)('0'+nibble);
return (char)('A'+nibble-10);
}
void GwConfigHandler::toHex(unsigned long v, char *buffer, size_t bsize)
{
uint8_t *bp = (uint8_t *)&v;
size_t i = 0;
for (; i < sizeof(v) && (2 * i + 1) < bsize; i++)
{
buffer[2 * i] = hv((*bp) >> 4);
buffer[2 * i + 1] = hv(*bp);
bp++;
}
if ((2 * i) < bsize)
buffer[2 * i] = 0;
}
void GwNmeaFilter::handleToken(String token, int index){
switch(index){
case 0:

View File

@ -18,22 +18,25 @@ class GwConfigHandler: public GwConfigDefinitions{
public:
GwConfigHandler(GwLog *logger);
bool loadConfig();
bool saveConfig();
void stopChanges();
bool updateValue(String name, String value);
bool reset(bool save);
bool reset();
String toString() const;
String toJson() const;
String getString(const String name,const String defaultv="") const;
bool getBool(const String name,bool defaultv=false) const ;
int getInt(const String name,int defaultv=0) const;
GwConfigInterface * getConfigItem(const String name, bool dummy=false) const;
bool checkPass(String hash);
/**
* change the value of a config item
* will become a noop after stopChanges has been called
* !use with care! no checks of the value
*/
bool setValue(String name, String value);
static void toHex(unsigned long v,char *buffer,size_t bsize);
unsigned long getSaltBase(){return saltBase;}
private:
unsigned long saltBase=0;
};
#endif

View File

@ -10,18 +10,6 @@ class GwConfigInterface{
const char * initialValue;
String value;
bool secret=false;
String changedValue;
bool hasChangedValue=false;
void updateValue(String value)
{
hasChangedValue = false;
if (value != this->value)
{
changedValue = value;
hasChangedValue = true;
}
}
public:
GwConfigInterface(const String &name, const char * initialValue, bool secret=false){
this->name=name;

View File

@ -57,6 +57,52 @@
//brightness 0...255
#define GWLED_BRIGHTNESS 64
#endif
#ifdef BOARD_M5ATOM_RS232_CANUNIT
#define ESP32_CAN_TX_PIN GPIO_NUM_26
#define ESP32_CAN_RX_PIN GPIO_NUM_32
//if using rs232
#define GWSERIAL_TX 19
#define GWSERIAL_RX 22
#define GWSERIAL_MODE "BI"
#define GWBUTTON_PIN GPIO_NUM_39
#define GWBUTTON_ACTIVE LOW
//if GWBUTTON_PULLUPDOWN we enable a pulup/pulldown
#define GWBUTTON_PULLUPDOWN
//led handling
//if we define GWLED_FASTNET the arduino fastnet lib is used
#define GWLED_FASTLED
#define GWLED_TYPE SK6812
//color schema for fastled
#define GWLED_SCHEMA GRB
#define GWLED_PIN GPIO_NUM_27
//brightness 0...255
#define GWLED_BRIGHTNESS 64
#endif
#ifdef BOARD_M5ATOM_RS485_CANUNIT
#define ESP32_CAN_TX_PIN GPIO_NUM_26
#define ESP32_CAN_RX_PIN GPIO_NUM_32
//if using rs232
#define GWSERIAL_TX 19
#define GWSERIAL_RX 22
#define GWSERIAL_MODE "UNI"
#define GWBUTTON_PIN GPIO_NUM_39
#define GWBUTTON_ACTIVE LOW
//if GWBUTTON_PULLUPDOWN we enable a pulup/pulldown
#define GWBUTTON_PULLUPDOWN
//led handling
//if we define GWLED_FASTNET the arduino fastnet lib is used
#define GWLED_FASTLED
#define GWLED_TYPE SK6812
//color schema for fastled
#define GWLED_SCHEMA GRB
#define GWLED_PIN GPIO_NUM_27
//brightness 0...255
#define GWLED_BRIGHTNESS 64
#endif
#ifdef BOARD_M5STICK_CANUNIT
#define ESP32_CAN_TX_PIN GPIO_NUM_32
#define ESP32_CAN_RX_PIN GPIO_NUM_33

View File

@ -21,6 +21,7 @@ void GwLog::logString(const char *fmt,...){
va_list args;
va_start(args,fmt);
xSemaphoreTake(locker, portMAX_DELAY);
recordCounter++;
vsnprintf(buffer,bufferSize-1,fmt,args);
buffer[bufferSize-1]=0;
if (! writer) {
@ -40,6 +41,7 @@ void GwLog::logDebug(int level,const char *fmt,...){
va_list args;
va_start(args,fmt);
xSemaphoreTake(locker, portMAX_DELAY);
recordCounter++;
vsnprintf(buffer,bufferSize-1,fmt,args);
buffer[bufferSize-1]=0;
if (! writer) {

View File

@ -15,6 +15,7 @@ class GwLog{
int logLevel=1;
GwLogWriter *writer;
SemaphoreHandle_t locker;
long long recordCounter=0;
public:
static const int LOG=1;
static const int ERROR=0;
@ -29,6 +30,7 @@ class GwLog{
int isActive(int level){return level <= logLevel;};
void flush();
void setLevel(int level){this->logLevel=level;}
long long getRecordCounter(){return recordCounter;}
};
#define LOG_DEBUG(level,...){ if (logger != NULL && logger->isActive(level)) logger->logDebug(level,__VA_ARGS__);}

View File

@ -887,6 +887,7 @@ private:
LOG_DEBUG(GwLog::DEBUG,"unable to parse ROT %s",msg.line);
return;
}
ROT=ROT / ROT_WA_FACTOR;
if (! updateDouble(boatData->ROT,ROT,msg.sourceId)) return;
tN2kMsg n2kMsg;
SetN2kRateOfTurn(n2kMsg,1,ROT);

View File

@ -32,7 +32,6 @@
N2kDataToNMEA0183::N2kDataToNMEA0183(GwLog * logger, GwBoatData *boatData,
SendNMEA0183MessageCallback callback, String talkerId)
{
@ -1055,7 +1054,7 @@ private:
}
if (!updateDouble(boatData->ROT,ROT)) return;
tNMEA0183Msg nmeamsg;
if (NMEA0183SetROT(nmeamsg,ROT,talkerId)){
if (NMEA0183SetROT(nmeamsg,ROT * ROT_WA_FACTOR,talkerId)){
SendMessage(nmeamsg);
}
}

View File

@ -118,4 +118,12 @@ bool GwWebServer::registerMainHandler(const char *url,RequestCreator creator){
return true;
}
bool GwWebServer::registerPostHandler(const char *url, ArRequestHandlerFunction requestHandler,
ArBodyHandlerFunction bodyHandler){
server->on(url,HTTP_POST,requestHandler,
[](AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final){},
bodyHandler);
return true;
}

View File

@ -1,6 +1,7 @@
#ifndef _GWWEBSERVER_H
#define _GWWEBSERVER_H
#include <ESPAsyncWebServer.h>
#include <functional>
#include "GwMessage.h"
#include "GwLog.h"
class GwWebServer{
@ -14,6 +15,7 @@ class GwWebServer{
~GwWebServer();
void begin();
bool registerMainHandler(const char *url,RequestCreator creator);
bool registerPostHandler(const char *url, ArRequestHandlerFunction requestHandler, ArBodyHandlerFunction bodyHandler);
void handleAsyncWebRequest(AsyncWebServerRequest *request, GwRequestMessage *msg);
AsyncWebServer * getServer(){return server;}
};

View File

@ -59,6 +59,25 @@ build_flags =
upload_port = /dev/esp32
upload_protocol = esptool
[env:m5stack-atom-rs232-canunit]
board = m5stack-atom
lib_deps = ${env.lib_deps}
build_flags =
-D BOARD_M5ATOM_RS232_CANUNIT
${env.build_flags}
upload_port = /dev/esp32
upload_protocol = esptool
[env:m5stack-atom-rs485-canunit]
board = m5stack-atom
lib_deps = ${env.lib_deps}
build_flags =
-D BOARD_M5ATOM_RS485_CANUNIT
${env.build_flags}
upload_port = /dev/esp32
upload_protocol = esptool
[env:m5stickc-atom-canunit]
board = m5stick-c
lib_deps = ${env.lib_deps}

View File

@ -141,47 +141,12 @@ GwWebServer webserver(&logger,&mainQueue,80);
GwCounter<unsigned long> countNMEA2KIn("count2Kin");
GwCounter<unsigned long> countNMEA2KOut("count2Kout");
unsigned long saltBase=esp_random();
char hv(uint8_t nibble){
nibble=nibble&0xf;
if (nibble < 10) return (char)('0'+nibble);
return (char)('A'+nibble-10);
}
void toHex(unsigned long v,char *buffer,size_t bsize){
uint8_t *bp=(uint8_t *)&v;
size_t i=0;
for (;i<sizeof(v) && (2*i +1)< bsize;i++){
buffer[2*i]=hv((*bp) >> 4);
buffer[2*i+1]=hv(*bp);
bp++;
}
if ((2*i) < bsize) buffer[2*i]=0;
}
bool checkPass(String hash){
if (! config.getBool(config.useAdminPass)) return true;
String pass=config.getString(config.adminPassword);
unsigned long now=millis()/1000UL & ~0x7UL;
MD5Builder builder;
char buffer[2*sizeof(now)+1];
for (int i=0;i< 5 ;i++){
unsigned long base=saltBase+now;
toHex(base,buffer,2*sizeof(now)+1);
builder.begin();
builder.add(buffer);
builder.add(pass);
builder.calculate();
String md5=builder.toString();
bool rt=hash == md5;
logger.logDebug(GwLog::DEBUG,"checking pass %s, base=%ld, hash=%s, res=%d",
hash.c_str(),base,md5.c_str(),(int)rt);
if (rt) return true;
now -= 8;
}
return false;
return config.checkPass(hash);
}
GwUpdate updater(&logger,&webserver,&checkPass);
GwConfigInterface *systemName=config.getConfigItem(config.systemName,true);
@ -398,9 +363,9 @@ protected:
status["clientIP"] = WiFi.localIP().toString();
status["apIp"] = gwWifi.apIP();
size_t bsize=2*sizeof(unsigned long)+1;
unsigned long base=saltBase + ( millis()/1000UL & ~0x7UL);
unsigned long base=config.getSaltBase() + ( millis()/1000UL & ~0x7UL);
char buffer[bsize];
toHex(base,buffer,bsize);
GwConfigHandler::toHex(base,buffer,bsize);
status["salt"] = buffer;
status["fwtype"]= firmwareType;
status["heap"]=(long)xPortGetFreeHeapSize();
@ -477,71 +442,7 @@ protected:
}
};
class SetConfigRequest : public GwRequestMessage
{
public:
//we rely on the message living not longer then the request
AsyncWebServerRequest *request;
SetConfigRequest(AsyncWebServerRequest *rq) : GwRequestMessage(F("application/json"),F("setConfig")),
request(rq)
{};
virtual int getTimeout(){return 4000;}
protected:
virtual void processRequest()
{
bool ok = true;
const char * hashArg="_hash";
String error;
String hash;
if (request->hasArg(hashArg)){
hash=request->arg(hashArg);
}
if (! checkPass(hash)){
result=JSON_INVALID_PASS;
return;
}
logger.logDebug(GwLog::DEBUG,"Heap free=%ld, minFree=%ld",
(long)xPortGetFreeHeapSize(),
(long)xPortGetMinimumEverFreeHeapSize()
);
for (int i = 0; i < request->args(); i++){
String name=request->argName(i);
String value=request->arg(i);
if (name.indexOf("_")>= 0) continue;
if (name == GwConfigDefinitions::apPassword && fixedApPass) continue;
bool rt = config.updateValue(name, value);
if (!rt)
{
logger.logDebug(GwLog::ERROR,"ERR: unable to update %s to %s", name.c_str(), value.c_str());
ok = false;
error += name;
error += "=";
error += value;
error += ",";
}
logger.flush();
}
if (ok)
{
result = JSON_OK;
logger.logDebug(GwLog::ERROR,"update config and restart");
config.saveConfig();
logger.flush();
logger.logDebug(GwLog::DEBUG,"Heap free=%ld, minFree=%ld",
(long)xPortGetFreeHeapSize(),
(long)xPortGetMinimumEverFreeHeapSize()
);
logger.flush();
delayedRestart();
}
else
{
GwJsonDocument rt(100);
rt["status"] = error;
serializeJson(rt, result);
}
}
};
class ResetConfigRequest : public GwRequestMessage
{
String hash;
@ -558,7 +459,7 @@ protected:
result=JSON_INVALID_PASS;
return;
}
config.reset(true);
config.reset();
logger.logDebug(GwLog::ERROR,"reset config, restart");
result = JSON_OK;
delayedRestart();
@ -627,6 +528,134 @@ protected:
};
void handleConfigRequestData(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total){
typedef struct{
char notFirst;
char hashChecked;
char parsingValue;
int bName;
char name[33];
int bValue;
char value[512];
}RequestNV;
long long lastRecords=logger.getRecordCounter();
logger.logDebug(GwLog::DEBUG,"handleConfigRequestData len=%d,idx=%d,total=%d",(int)len,(int)index,(int)total);
if (request->_tempObject == NULL){
logger.logDebug(GwLog::DEBUG,"handleConfigRequestData create receive struct");
//we cannot use new here as it will be deleted with free
request->_tempObject=malloc(sizeof(RequestNV));
memset(request->_tempObject,0,sizeof(RequestNV));
}
RequestNV *nv=(RequestNV*)(request->_tempObject);
if (nv->notFirst && ! nv->hashChecked){
return; //ignore data
}
int parsed=0;
while (parsed < len)
{
if (!nv->parsingValue)
{
int maxSize = sizeof(RequestNV::name) - 1;
if (nv->bName >= maxSize)
{
nv->name[maxSize] = 0;
logger.logDebug(GwLog::DEBUG, "parse error name too long %s", nv->name);
nv->bName = 0;
}
while (nv->bName < maxSize && parsed < len)
{
bool endName = *data == '=';
nv->name[nv->bName] = endName ? 0 : *data;
nv->bName++;
parsed++;
data++;
if (endName)
{
nv->parsingValue = 1;
break;
}
}
}
bool valueDone = false;
if (nv->parsingValue)
{
int maxSize = sizeof(RequestNV::value) - 1;
if (nv->bValue >= maxSize)
{
nv->value[maxSize] = 0;
logger.logDebug(GwLog::DEBUG, "parse error value too long %s:%s", nv->name, nv->value);
nv->bValue = 0;
}
while (nv->bValue < maxSize && parsed < len)
{
valueDone = *data == '&';
nv->value[nv->bValue] = valueDone ? 0 : *data;
nv->bValue++;
parsed++;
data++;
if (valueDone) break;
}
if (! valueDone){
if (parsed >= len && (len+index) >= total){
//request ends here
nv->value[nv->bValue]=0;
valueDone=true;
}
}
if (valueDone){
String name(nv->name);
String value(nv->value);
if (! nv->notFirst){
nv->notFirst=1;
//we expect the _hash as first parameter
if (name != String("_hash")){
logger.logDebug(GwLog::ERROR,"missing first parameter _hash in setConfig");
request->send(200,"application/json","{\"status\":\"missing _hash\"}");
return;
}
if (! config.checkPass(request->urlDecode(value))){
request->send(200,"application/json",JSON_INVALID_PASS);
return;
}
else{
nv->hashChecked=1;
}
}
else{
if (nv->hashChecked){
logger.logDebug(GwLog::DEBUG,"value ns=%d,n=%s,vs=%d,v=%s",nv->bName,nv->name,nv->bValue,nv->value);
if ((logger.getRecordCounter() - lastRecords) > 20){
logger.flush();
lastRecords=logger.getRecordCounter();
}
config.updateValue(request->urlDecode(name),request->urlDecode(value));
}
}
nv->parsingValue=0;
nv->bName=0;
nv->bValue=0;
}
}
}
if (parsed >= len && (len+index)>= total){
if (nv->notFirst){
if (nv->hashChecked){
request->send(200,"application/json",JSON_OK);
logger.flush();
logger.logDebug(GwLog::DEBUG,"Heap free=%ld, minFree=%ld",
(long)xPortGetFreeHeapSize(),
(long)xPortGetMinimumEverFreeHeapSize()
);
logger.flush();
delayedRestart();
}
}
else{
request->send(200,"application/json","{\"status\":\"missing _hash\"}");
}
}
}
void setup() {
mainLock=xSemaphoreCreateMutex();
@ -667,12 +696,6 @@ void setup() {
{ return new StatusRequest(); });
webserver.registerMainHandler("/api/config", [](AsyncWebServerRequest *request)->GwRequestMessage *
{ return new ConfigRequest(); });
webserver.registerMainHandler("/api/setConfig",
[](AsyncWebServerRequest *request)->GwRequestMessage *
{
SetConfigRequest *msg = new SetConfigRequest(request);
return msg;
});
webserver.registerMainHandler("/api/resetConfig", [](AsyncWebServerRequest *request)->GwRequestMessage *
{ return new ResetConfigRequest(request->arg("_hash")); });
webserver.registerMainHandler("/api/boatData", [](AsyncWebServerRequest *request)->GwRequestMessage *
@ -692,6 +715,11 @@ void setup() {
String hash=request->arg("hash");
return new CheckPassRequest(hash);
});
webserver.registerPostHandler("/api/setConfig",
[](AsyncWebServerRequest *request){
},
handleConfigRequestData);
webserver.begin();
xdrMappings.begin();

View File

@ -236,16 +236,24 @@ function changeConfig() {
ensurePass()
.then(function (pass) {
let newAdminPass;
let url = "/api/setConfig?_hash="+encodeURIComponent(pass)+"&";
let url = "/api/setConfig"
let body="_hash="+encodeURIComponent(pass)+"&";
let allValues=getAllConfigs();
if (!allValues) return;
for (let name in allValues){
if (name == 'adminPassword'){
newAdminPass=allValues[name];
}
url += name + "=" + encodeURIComponent(allValues[name]) + "&";
body += encodeURIComponent(name) + "=" + encodeURIComponent(allValues[name]) + "&";
}
getJson(url)
fetch(url,{
method:'POST',
headers:{
'Content-Type': 'application/octet-stream' //we must lie here
},
body: body
})
.then((rs)=>rs.json())
.then(function (status) {
if (status.status == 'OK') {
if (newAdminPass !== undefined) {