1
0
mirror of https://github.com/thooge/esp32-nmea2000-obp60.git synced 2026-03-28 18:06:37 +01:00

Integrate many changes from master

This commit is contained in:
2026-03-18 13:29:57 +01:00
parent b2e67880d3
commit caf833e6ac
107 changed files with 8565 additions and 2688 deletions

View File

@@ -62,5 +62,5 @@ jobs:
with: with:
repo_token: ${{ secrets.GITHUB_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ steps.version.outputs.version}} tag: ${{ steps.version.outputs.version}}
file: ./.pio/build/*/*-{all,update}.bin file: ./.pio/build/*/*${{ steps.version.outputs.version }}*-{all,update}.bin
file_glob: true file_glob: true

View File

@@ -43,6 +43,10 @@ What is included
For the details of the mapped PGNs and NMEA sentences refer to [Conversions](doc/Conversions.pdf). For the details of the mapped PGNs and NMEA sentences refer to [Conversions](doc/Conversions.pdf).
License
-------
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either [version 2 of the License](LICENSE), or (at your option) any later version.
Hardware Hardware
-------- --------
The software is prepared to run on different kinds of ESP32 based modules and accessoirs. For some of them prebuild binaries are available that only need to be flashed, others would require to add some definitions of the used PINs and features and to build the binary. The software is prepared to run on different kinds of ESP32 based modules and accessoirs. For some of them prebuild binaries are available that only need to be flashed, others would require to add some definitions of the used PINs and features and to build the binary.
@@ -170,6 +174,25 @@ For details refer to the [example description](lib/exampletask/Readme.md).
Changelog Changelog
--------- ---------
[20251126](../../releases/tag/20251126)
* fix a bug in the Actisense reader that could lead to an endless loop (making the device completely non responsive)
* upgrade to 4.24.1 of the NMEA2000 library (2025/11/01) - refer to the [changes](https://github.com/ttlappalainen/NMEA2000/blob/master/Documents/src/changes.md) - Especially UTF8 support
*********
[20251007](../../releases/tag/20251007)
*********
* add AIS Aton translations (PGN 129041 <-> Ais class 21)
* improved mapping of AIS transducer information (NMEA2000) to AIS channel and Talker on NMEA0183
* use a forked version of the NMEA2000 library (as an intermediate workaround)
* [#114](../../issues/114) correctly translate AIS type 1/3 from PGN 129038
* add support for a generic S3 build in the build UI
* [#117](../../issues/117) add support for a transmit enable pin for RS 485 conections (also in the build UI)
* [#116](../../issues/116) SDA and SCL are swapped in the build UI
* [#112](../../issues/112) clearify licenses
* [#110](../../issues/110) / [#115](../../pull/115) support for the M5 GPS unit v1.1
* [#102](../../issues/102) optimize Wifi reconnect handling
* [#111](../../pull/111) allow for a custom python build script
* [#113](../../issues/113) support for M5 stack Env4
[20250305](../../releases/tag/20250305) [20250305](../../releases/tag/20250305)
********* *********
* better handling for reconnect to a raspberry pi after reset [#102](../../issues/102) * better handling for reconnect to a raspberry pi after reset [#102](../../issues/102)

Binary file not shown.

Binary file not shown.

View File

@@ -10,7 +10,7 @@ from datetime import datetime
import re import re
import pprint import pprint
from platformio.project.config import ProjectConfig from platformio.project.config import ProjectConfig
from platformio.project.exception import InvalidProjectConfError
Import("env") Import("env")
#print(env.Dump()) #print(env.Dump())
@@ -104,18 +104,7 @@ def writeFileIfChanged(fileName,data):
return True return True
def mergeConfig(base,other): def mergeConfig(base,other):
try: for cname in other:
customconfig = env.GetProjectOption("custom_config")
except InvalidProjectConfError:
customconfig = None
for bdir in other:
if customconfig and os.path.exists(os.path.join(bdir,customconfig)):
cname=os.path.join(bdir,customconfig)
print("merge custom config {}".format(cname))
with open(cname,'rb') as ah:
base += json.load(ah)
continue
cname=os.path.join(bdir,"config.json")
if os.path.exists(cname): if os.path.exists(cname):
print("merge config %s"%cname) print("merge config %s"%cname)
with open(cname,'rb') as ah: with open(cname,'rb') as ah:
@@ -161,13 +150,25 @@ def expandConfig(config):
rt.append(replaceTexts(c,replace)) rt.append(replaceTexts(c,replace))
return rt return rt
def generateMergedConfig(inFile,outFile,addDirs=[]): def createUserItemList(dirs,itemName,files):
rt=[]
for d in dirs:
iname=os.path.join(d,itemName)
if os.path.exists(iname):
rt.append(iname)
for f in files:
if not os.path.exists(f):
raise Exception("user item %s not found"%f)
rt.append(f)
return rt
def generateMergedConfig(inFile,outFile,addFiles=[]):
if not os.path.exists(inFile): if not os.path.exists(inFile):
raise Exception("unable to read cfg file %s"%inFile) raise Exception("unable to read cfg file %s"%inFile)
data="" data=""
with open(inFile,'rb') as ch: with open(inFile,'rb') as ch:
config=json.load(ch) config=json.load(ch)
config=mergeConfig(config,addDirs) config=mergeConfig(config,addFiles)
config=expandConfig(config) config=expandConfig(config)
data=json.dumps(config,indent=2) data=json.dumps(config,indent=2)
writeFileIfChanged(outFile,data) writeFileIfChanged(outFile,data)
@@ -205,11 +206,6 @@ def generateCfg(inFile,outFile,impl):
secret="false"; secret="false";
if item.get('type') == 'password': if item.get('type') == 'password':
secret="true" secret="true"
"""
PSRAM Allocator TODO Tests
new (heap_caps_malloc(sizeof(GwConfigInterface), MALLOC_CAP_SPIRAM))
"""
#data+=" new (heap_caps_malloc(sizeof(GwConfigInterface), MALLOC_CAP_SPIRAM)) GwConfigInterface(%s,\"%s\",%s);\n"%(name,item.get('default'),secret)
data+=" new GwConfigInterface(%s,\"%s\",%s);\n"%(name,item.get('default'),secret) data+=" new GwConfigInterface(%s,\"%s\",%s);\n"%(name,item.get('default'),secret)
data+='}\n' data+='}\n'
writeFileIfChanged(outFile,data) writeFileIfChanged(outFile,data)
@@ -392,12 +388,7 @@ def getLibs():
def joinFiles(target,pattern,dirlist): def joinFiles(target,flist):
flist=[]
for dir in dirlist:
fn=os.path.join(dir,pattern)
if os.path.exists(fn):
flist.append(fn)
current=False current=False
if os.path.exists(target): if os.path.exists(target):
current=True current=True
@@ -468,7 +459,28 @@ def handleDeps(env):
) )
env.AddBuildMiddleware(injectIncludes) env.AddBuildMiddleware(injectIncludes)
def getOption(env,name,toArray=True):
try:
opt=env.GetProjectOption(name)
if toArray:
if opt is None:
return []
if isinstance(opt,list):
return opt
return opt.split("\n" if "\n" in opt else ",")
return opt
except:
pass
if toArray:
return []
def getFileList(files):
base=basePath()
rt=[]
for f in files:
if f is not None and f != "":
rt.append(os.path.join(base,f))
return rt
def prebuild(env): def prebuild(env):
global userTaskDirs global userTaskDirs
print("#prebuild running") print("#prebuild running")
@@ -478,14 +490,18 @@ def prebuild(env):
if ldf_mode == 'off': if ldf_mode == 'off':
print("##ldf off - own dependency handling") print("##ldf off - own dependency handling")
handleDeps(env) handleDeps(env)
extraConfigs=getOption(env,'custom_config',toArray=True)
extraJs=getOption(env,'custom_js',toArray=True)
extraCss=getOption(env,'custom_css',toArray=True)
userTaskDirs=getUserTaskDirs() userTaskDirs=getUserTaskDirs()
mergedConfig=os.path.join(outPath(),os.path.basename(CFG_FILE)) mergedConfig=os.path.join(outPath(),os.path.basename(CFG_FILE))
generateMergedConfig(os.path.join(basePath(),CFG_FILE),mergedConfig,userTaskDirs) generateMergedConfig(os.path.join(basePath(),CFG_FILE),mergedConfig,createUserItemList(userTaskDirs,"config.json", getFileList(extraConfigs)))
compressFile(mergedConfig,mergedConfig+".gz") compressFile(mergedConfig,mergedConfig+".gz")
generateCfg(mergedConfig,os.path.join(outPath(),CFG_INCLUDE),False) generateCfg(mergedConfig,os.path.join(outPath(),CFG_INCLUDE),False)
generateCfg(mergedConfig,os.path.join(outPath(),CFG_INCLUDE_IMPL),True) generateCfg(mergedConfig,os.path.join(outPath(),CFG_INCLUDE_IMPL),True)
joinFiles(os.path.join(outPath(),INDEXJS+".gz"),INDEXJS,["web"]+userTaskDirs) joinFiles(os.path.join(outPath(),INDEXJS+".gz"),createUserItemList(["web"]+userTaskDirs,INDEXJS,getFileList(extraJs)))
joinFiles(os.path.join(outPath(),INDEXCSS+".gz"),INDEXCSS,["web"]+userTaskDirs) joinFiles(os.path.join(outPath(),INDEXCSS+".gz"),createUserItemList(["web"]+userTaskDirs,INDEXCSS,getFileList(extraCss)))
embedded=getEmbeddedFiles(env) embedded=getEmbeddedFiles(env)
filedefs=[] filedefs=[]
for ef in embedded: for ef in embedded:
@@ -506,12 +522,10 @@ def prebuild(env):
genereateUserTasks(os.path.join(outPath(), TASK_INCLUDE)) genereateUserTasks(os.path.join(outPath(), TASK_INCLUDE))
generateFile(os.path.join(basePath(),XDR_FILE),os.path.join(outPath(),XDR_INCLUDE),generateXdrMappings) generateFile(os.path.join(basePath(),XDR_FILE),os.path.join(outPath(),XDR_INCLUDE),generateXdrMappings)
generateFile(os.path.join(basePath(),GROVE_CONFIG_IN),os.path.join(outPath(),GROVE_CONFIG),generateGroveDefs,inMode='r') generateFile(os.path.join(basePath(),GROVE_CONFIG_IN),os.path.join(outPath(),GROVE_CONFIG),generateGroveDefs,inMode='r')
version = "dev{}{}".format(datetime.now().strftime("%Y%m%d"), "-ext") version="dev"+datetime.now().strftime("%Y%m%d")
env.Append(CPPDEFINES=[('GWDEVVERSION',version)]) env.Append(CPPDEFINES=[('GWDEVVERSION',version)])
def cleangenerated(source, target, env): def cleangenerated(source, target, env):
# TODO source / target order?
print("CLEAN: {} - {}".format(source, target))
od=outPath() od=outPath()
if os.path.isdir(od): if os.path.isdir(od):
print("#cleaning up %s"%od) print("#cleaning up %s"%od)
@@ -521,6 +535,7 @@ def cleangenerated(source, target, env):
fn=os.path.join(od,f) fn=os.path.join(od,f)
os.unlink(f) os.unlink(f)
print("#prescript...") print("#prescript...")
prebuild(env) prebuild(env)
board="PLATFORM_BOARD_%s"%env["BOARD"].replace("-","_").upper() board="PLATFORM_BOARD_%s"%env["BOARD"].replace("-","_").upper()
@@ -532,17 +547,16 @@ env.Append(
) )
#script does not run on clean yet - maybe in the future #script does not run on clean yet - maybe in the future
env.AddPostAction("clean",cleangenerated) env.AddPostAction("clean",cleangenerated)
extraScripts=getFileList(getOption(env,'custom_script',toArray=True))
#look for extra task scripts and include them here for script in extraScripts:
for taskdir in userTaskDirs:
script = os.path.join(taskdir, "extra_task.py")
if os.path.isfile(script): if os.path.isfile(script):
taskname = os.path.basename(os.path.normpath(taskdir)) print(f"#extra {script}")
print("#extra task script for '{}'".format(taskname))
with open(script) as fh: with open(script) as fh:
try: try:
code = compile(fh.read(), taskname, 'exec') code = compile(fh.read(), script, 'exec')
except SyntaxError: except SyntaxError as e:
print("#ERROR: script does not compile") print(f"#ERROR: script {script} does not compile: {e}")
continue continue
exec(code) exec(code)
else:
print(f"#ERROR: script {script} not found")

View File

@@ -627,7 +627,7 @@ void AisDecoder::decodeType21(PayloadBuffer &_buffer, unsigned int _uMsgType, in
} }
// decode message fields (binary buffer has to go through all fields, but some fields are not used) // decode message fields (binary buffer has to go through all fields, but some fields are not used)
_buffer.getUnsignedValue(2); // repeatIndicator auto repeat=_buffer.getUnsignedValue(2); // repeatIndicator
auto mmsi = _buffer.getUnsignedValue(30); auto mmsi = _buffer.getUnsignedValue(30);
auto aidType = _buffer.getUnsignedValue(5); auto aidType = _buffer.getUnsignedValue(5);
auto name = _buffer.getString(120); auto name = _buffer.getString(120);
@@ -640,11 +640,11 @@ void AisDecoder::decodeType21(PayloadBuffer &_buffer, unsigned int _uMsgType, in
auto toStarboard = _buffer.getUnsignedValue(6); auto toStarboard = _buffer.getUnsignedValue(6);
_buffer.getUnsignedValue(4); // epfd type _buffer.getUnsignedValue(4); // epfd type
_buffer.getUnsignedValue(6); // timestamp auto timestamp=_buffer.getUnsignedValue(6); // timestamp
_buffer.getBoolValue(); // off position auto offPosition=_buffer.getBoolValue(); // off position
_buffer.getUnsignedValue(8); // reserved _buffer.getUnsignedValue(8); // reserved
_buffer.getBoolValue(); // RAIM auto raim=_buffer.getBoolValue(); // RAIM
_buffer.getBoolValue(); // virtual aid auto virtualAton=_buffer.getBoolValue(); // virtual aid
_buffer.getBoolValue(); // assigned mode _buffer.getBoolValue(); // assigned mode
_buffer.getUnsignedValue(1); // spare _buffer.getUnsignedValue(1); // spare
@@ -654,7 +654,9 @@ void AisDecoder::decodeType21(PayloadBuffer &_buffer, unsigned int _uMsgType, in
nameExt = _buffer.getString(88); nameExt = _buffer.getString(88);
} }
onType21(mmsi, aidType, name + nameExt, posAccuracy, posLon, posLat, toBow, toStern, toPort, toStarboard); onType21(mmsi, aidType, name + nameExt, posAccuracy, posLon, posLat,
toBow, toStern, toPort, toStarboard,
repeat,timestamp, raim, virtualAton, offPosition);
} }
/* decode Voyage Report and Static Data (type nibble already pulled from buffer) */ /* decode Voyage Report and Static Data (type nibble already pulled from buffer) */

View File

@@ -297,7 +297,8 @@ namespace AIS
bool assigned, unsigned int repeat, bool raim) = 0; bool assigned, unsigned int repeat, bool raim) = 0;
virtual void onType21(unsigned int _uMmsi, unsigned int _uAidType, const std::string &_strName, bool _bPosAccuracy, int _iPosLon, int _iPosLat, virtual void onType21(unsigned int _uMmsi, unsigned int _uAidType, const std::string &_strName, bool _bPosAccuracy, int _iPosLon, int _iPosLat,
unsigned int _uToBow, unsigned int _uToStern, unsigned int _uToPort, unsigned int _uToStarboard) = 0; unsigned int _uToBow, unsigned int _uToStern, unsigned int _uToPort, unsigned int _uToStarboard,
unsigned int repeat,unsigned int timestamp, bool raim, bool virtualAton, bool offPosition) = 0;
virtual void onType24A(unsigned int _uMsgType, unsigned int _repeat, unsigned int _uMmsi, const std::string &_strName) = 0; virtual void onType24A(unsigned int _uMsgType, unsigned int _repeat, unsigned int _uMmsi, const std::string &_strName) = 0;

View File

@@ -3,6 +3,7 @@
#include "GwMessage.h" #include "GwMessage.h"
#include "N2kMsg.h" #include "N2kMsg.h"
#include "Nmea2kTwai.h" #include "Nmea2kTwai.h"
#include "N2kDeviceList.h"
#include "NMEA0183Msg.h" #include "NMEA0183Msg.h"
#include "GWConfig.h" #include "GWConfig.h"
#include "GwBoatData.h" #include "GwBoatData.h"
@@ -225,6 +226,7 @@ class GwApi{
* you need to use the request pattern as shown in GwExampleTask.cpp * you need to use the request pattern as shown in GwExampleTask.cpp
*/ */
virtual Nmea2kTwai *getNMEA2000()=0; virtual Nmea2kTwai *getNMEA2000()=0;
virtual tN2kDeviceList *getN2kDeviceList()=0;
virtual GwBoatData *getBoatData()=0; virtual GwBoatData *getBoatData()=0;
virtual ~GwApi(){} virtual ~GwApi(){}
}; };

View File

@@ -14,6 +14,9 @@
#define LOGLEVEL GwLog::DEBUG #define LOGLEVEL GwLog::DEBUG
#endif #endif
#endif #endif
#ifdef GWBUILD_NAME
#define FIRMWARE_TYPE GWSTRINGIFY(GWBUILD_NAME)
#else
#define FIRMWARE_TYPE GWSTRINGIFY(PIO_ENV_BUILD) #define FIRMWARE_TYPE GWSTRINGIFY(PIO_ENV_BUILD)
#endif
#define IDF_VERSION GWSTRINGIFY(ESP_IDF_VERSION_MAJOR) "." GWSTRINGIFY(ESP_IDF_VERSION_MINOR) "." GWSTRINGIFY(ESP_IDF_VERSION_PATCH) #define IDF_VERSION GWSTRINGIFY(ESP_IDF_VERSION_MAJOR) "." GWSTRINGIFY(ESP_IDF_VERSION_MINOR) "." GWSTRINGIFY(ESP_IDF_VERSION_PATCH)

View File

@@ -249,3 +249,16 @@ unsigned long GwChannel::countTx(){
if (! countOut) return 0UL; if (! countOut) return 0UL;
return countOut->getGlobal(); return countOut->getGlobal();
} }
String GwChannel::typeString(int type){
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";
}

View File

@@ -77,7 +77,8 @@ class GwChannel{
if (maxSourceId < 0) return source == sourceId; if (maxSourceId < 0) return source == sourceId;
return (source >= sourceId && source <= maxSourceId); return (source >= sourceId && source <= maxSourceId);
} }
String getMode(){return impl->getMode();} static String typeString(int type);
String getMode(){return typeString(impl->getType());}
int getMinId(){return sourceId;}; int getMinId(){return sourceId;};
}; };

View File

@@ -1,10 +1,11 @@
#pragma once #pragma once
#include "GwBuffer.h" #include "GwBuffer.h"
#include "GwChannelModes.h"
class GwChannelInterface{ class GwChannelInterface{
public: public:
virtual void loop(bool handleRead,bool handleWrite)=0; virtual void loop(bool handleRead,bool handleWrite)=0;
virtual void readMessages(GwMessageFetcher *writer)=0; virtual void readMessages(GwMessageFetcher *writer)=0;
virtual size_t sendToClients(const char *buffer, int sourceId, bool partial=false)=0; virtual size_t sendToClients(const char *buffer, int sourceId, bool partial=false)=0;
virtual Stream * getStream(bool partialWrites){ return NULL;} virtual Stream * getStream(bool partialWrites){ return NULL;}
virtual String getMode(){return "UNKNOWN";} virtual int getType(){ return GWSERIAL_TYPE_BI;} //return the numeric type
}; };

View File

@@ -15,8 +15,10 @@ class SerInit{
int tx=-1; int tx=-1;
int mode=-1; int mode=-1;
int fixedBaud=-1; int fixedBaud=-1;
SerInit(int s,int r,int t, int m, int b=-1): int ena=-1;
serial(s),rx(r),tx(t),mode(m),fixedBaud(b){} int elow=1;
SerInit(int s,int r,int t, int m, int b=-1,int en=-1,int el=-1):
serial(s),rx(r),tx(t),mode(m),fixedBaud(b),ena(en),elow(el){}
}; };
std::vector<SerInit> serialInits; std::vector<SerInit> serialInits;
@@ -47,11 +49,20 @@ static int typeFromMode(const char *mode){
#ifndef GWSERIAL_RX #ifndef GWSERIAL_RX
#define GWSERIAL_RX -1 #define GWSERIAL_RX -1
#endif #endif
#ifndef GWSERIAL_ENA
#define GWSERIAL_ENA -1
#endif
#ifndef GWSERIAL_ELO
#define GWSERIAL_ELO 0
#endif
#ifndef GWSERIAL_BAUD
#define GWSERIAL_BAUD -1
#endif
#ifdef GWSERIAL_TYPE #ifdef GWSERIAL_TYPE
CFG_SERIAL(SERIAL1_CHANNEL_ID, GWSERIAL_RX, GWSERIAL_TX, GWSERIAL_TYPE) CFG_SERIAL(SERIAL1_CHANNEL_ID, GWSERIAL_RX, GWSERIAL_TX, GWSERIAL_TYPE,GWSERIAL_BAUD,GWSERIAL_ENA,GWSERIAL_ELO)
#else #else
#ifdef GWSERIAL_MODE #ifdef GWSERIAL_MODE
CFG_SERIAL(SERIAL1_CHANNEL_ID, GWSERIAL_RX, GWSERIAL_TX, typeFromMode(GWSERIAL_MODE)) CFG_SERIAL(SERIAL1_CHANNEL_ID, GWSERIAL_RX, GWSERIAL_TX, typeFromMode(GWSERIAL_MODE),GWSERIAL_BAUD,GWSERIAL_ENA,GWSERIAL_ELO)
#endif #endif
#endif #endif
// serial 2 // serial 2
@@ -61,11 +72,20 @@ CFG_SERIAL(SERIAL1_CHANNEL_ID, GWSERIAL_RX, GWSERIAL_TX, typeFromMode(GWSERIAL_M
#ifndef GWSERIAL2_RX #ifndef GWSERIAL2_RX
#define GWSERIAL2_RX -1 #define GWSERIAL2_RX -1
#endif #endif
#ifndef GWSERIAL2_ENA
#define GWSERIAL2_ENA -1
#endif
#ifndef GWSERIAL2_ELO
#define GWSERIAL2_ELO 0
#endif
#ifndef GWSERIAL2_BAUD
#define GWSERIAL2_BAUD -1
#endif
#ifdef GWSERIAL2_TYPE #ifdef GWSERIAL2_TYPE
CFG_SERIAL(SERIAL2_CHANNEL_ID, GWSERIAL2_RX, GWSERIAL2_TX, GWSERIAL2_TYPE) CFG_SERIAL(SERIAL2_CHANNEL_ID, GWSERIAL2_RX, GWSERIAL2_TX, GWSERIAL2_TYPE,GWSERIAL2_BAUD,GWSERIAL2_ENA,GWSERIAL2_ELO)
#else #else
#ifdef GWSERIAL2_MODE #ifdef GWSERIAL2_MODE
CFG_SERIAL(SERIAL2_CHANNEL_ID, GWSERIAL2_RX, GWSERIAL2_TX, typeFromMode(GWSERIAL2_MODE)) CFG_SERIAL(SERIAL2_CHANNEL_ID, GWSERIAL2_RX, GWSERIAL2_TX, typeFromMode(GWSERIAL2_MODE),GWSERIAL2_BAUD,GWSERIAL2_ENA,GWSERIAL2_ELO)
#endif #endif
#endif #endif
class GwSerialLog : public GwLogWriter class GwSerialLog : public GwLogWriter
@@ -285,8 +305,8 @@ static ChannelParam channelParameters[]={
}; };
template<typename T> template<typename T>
GwSerial* createSerial(GwLog *logger, T* s,int id, bool canRead=true){ GwSerial* createSerial(GwLog *logger, T* s,int id, int type, bool canRead=true){
return new GwSerialImpl<T>(logger,s,id,canRead); return new GwSerialImpl<T>(logger,s,id,type,canRead);
} }
static ChannelParam * findChannelParam(int id){ static ChannelParam * findChannelParam(int id){
@@ -300,7 +320,7 @@ static ChannelParam * findChannelParam(int id){
return param; return param;
} }
static GwSerial * createSerialImpl(GwConfigHandler *config,GwLog *logger, int idx,int rx,int tx, bool setLog=false){ static GwSerial * createSerialImpl(GwConfigHandler *config,GwLog *logger, int idx,int type,int rx,int tx, bool setLog,int ena=-1,int elow=1){
LOG_DEBUG(GwLog::DEBUG,"create serial: channel=%d, rx=%d,tx=%d", LOG_DEBUG(GwLog::DEBUG,"create serial: channel=%d, rx=%d,tx=%d",
idx,rx,tx); idx,rx,tx);
ChannelParam *param=findChannelParam(idx); ChannelParam *param=findChannelParam(idx);
@@ -312,19 +332,45 @@ static GwSerial * createSerialImpl(GwConfigHandler *config,GwLog *logger, int id
GwLog *streamLog=setLog?nullptr:logger; GwLog *streamLog=setLog?nullptr:logger;
switch(param->id){ switch(param->id){
case USB_CHANNEL_ID: case USB_CHANNEL_ID:
serialStream=createSerial(streamLog,&USBSerial,param->id); serialStream=createSerial(streamLog,&USBSerial,param->id,type);
break; break;
case SERIAL1_CHANNEL_ID: case SERIAL1_CHANNEL_ID:
serialStream=createSerial(streamLog,&Serial1,param->id); serialStream=createSerial(streamLog,&Serial1,param->id,type);
break; break;
case SERIAL2_CHANNEL_ID: case SERIAL2_CHANNEL_ID:
serialStream=createSerial(streamLog,&Serial2,param->id); serialStream=createSerial(streamLog,&Serial2,param->id,type);
break; break;
} }
if (serialStream == nullptr){ if (serialStream == nullptr){
LOG_DEBUG(GwLog::ERROR,"invalid serial config with id %d",param->id); LOG_DEBUG(GwLog::ERROR,"invalid serial config with id %d",param->id);
return nullptr; return nullptr;
} }
if (ena >= 0){
int value=-1;
if (type == GWSERIAL_TYPE_UNI){
String cfgMode=config->getString(param->direction);
if (cfgMode == "send"){
value=elow?0:1;
}
else{
value=elow?1:0;
}
}
if (type == GWSERIAL_TYPE_RX){
value=elow?1:0;
}
if (type == GWSERIAL_TYPE_TX){
value=elow?0:1;
}
if (value >= 0){
LOG_DEBUG(GwLog::LOG,"serial %d: setting output enable %d to %d",param->id,ena,value);
pinMode(ena,OUTPUT);
digitalWrite(ena,value);
}
else{
LOG_DEBUG(GwLog::ERROR,"serial %d: output enable ignored for mode %d",param->id, type);
}
}
serialStream->begin(config->getInt(param->baud,115200),SERIAL_8N1,rx,tx); serialStream->begin(config->getInt(param->baud,115200),SERIAL_8N1,rx,tx);
if (setLog){ if (setLog){
logger->setWriter(new GwSerialLog(serialStream,config->getBool(param->preventLog,false))); logger->setWriter(new GwSerialLog(serialStream,config->getBool(param->preventLog,false)));
@@ -332,12 +378,13 @@ static GwSerial * createSerialImpl(GwConfigHandler *config,GwLog *logger, int id
} }
return serialStream; return serialStream;
} }
static GwChannel * createChannel(GwLog *logger, GwConfigHandler *config, int id,GwChannelInterface *impl, int type=GWSERIAL_TYPE_BI){ static GwChannel * createChannel(GwLog *logger, GwConfigHandler *config, int id,GwChannelInterface *impl){
ChannelParam *param=findChannelParam(id); ChannelParam *param=findChannelParam(id);
if (param == nullptr){ if (param == nullptr){
LOG_DEBUG(GwLog::ERROR,"invalid channel id %d",id); LOG_DEBUG(GwLog::ERROR,"invalid channel id %d",id);
return nullptr; return nullptr;
} }
int type=impl->getType();
bool canRead=false; bool canRead=false;
bool canWrite=false; bool canWrite=false;
bool validType=false; bool validType=false;
@@ -425,10 +472,10 @@ void GwChannelList::begin(bool fallbackSerial){
GwChannel *channel=NULL; GwChannel *channel=NULL;
//usb //usb
if (! fallbackSerial){ if (! fallbackSerial){
GwSerial *usbSerial=createSerialImpl(config, logger,USB_CHANNEL_ID,GWUSB_RX,GWUSB_TX,true); GwSerial *usbSerial=createSerialImpl(config, logger,USB_CHANNEL_ID,GWSERIAL_TYPE_BI,GWUSB_RX,GWUSB_TX,true);
if (usbSerial != nullptr){ if (usbSerial != nullptr){
usbSerial->enableWriteLock(); //as it is used for logging we need this additionally usbSerial->enableWriteLock(); //as it is used for logging we need this additionally
GwChannel *usbChannel=createChannel(logger,config,USB_CHANNEL_ID,usbSerial,GWSERIAL_TYPE_BI); GwChannel *usbChannel=createChannel(logger,config,USB_CHANNEL_ID,usbSerial);
if (usbChannel != nullptr){ if (usbChannel != nullptr){
addChannel(usbChannel); addChannel(usbChannel);
} }
@@ -444,10 +491,11 @@ void GwChannelList::begin(bool fallbackSerial){
//new serial config handling //new serial config handling
for (auto &&init:serialInits){ for (auto &&init:serialInits){
LOG_INFO("creating serial channel %d, rx=%d,tx=%d,type=%d",init.serial,init.rx,init.tx,init.mode); LOG_INFO("creating serial channel %d, rx=%d,tx=%d,type=%d fixedBaud=%d ena=%d elow=%d",
GwSerial *ser=createSerialImpl(config,logger,init.serial,init.rx,init.tx); init.serial,init.rx,init.tx,init.mode,init.fixedBaud,init.ena,init.elow);
GwSerial *ser=createSerialImpl(config,logger,init.serial,init.mode,init.rx,init.tx,false,init.ena,init.elow);
if (ser != nullptr){ if (ser != nullptr){
channel=createChannel(logger,config,init.serial,ser,init.mode); channel=createChannel(logger,config,init.serial,ser);
if (channel != nullptr){ if (channel != nullptr){
addChannel(channel); addChannel(channel);
} }
@@ -466,8 +514,8 @@ void GwChannelList::begin(bool fallbackSerial){
config->getInt(config->remotePort), config->getInt(config->remotePort),
config->getBool(config->readTCL) config->getBool(config->readTCL)
); );
}
addChannel(createChannel(logger,config,TCP_CLIENT_CHANNEL_ID,client)); addChannel(createChannel(logger,config,TCP_CLIENT_CHANNEL_ID,client));
}
//udp writer //udp writer
if (config->getBool(GwConfigDefinitions::udpwEnabled)){ if (config->getBool(GwConfigDefinitions::udpwEnabled)){

View File

@@ -3,7 +3,7 @@
This code is free software; you can redistribute it and/or This code is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version. version 2 of the License, or (at your option) any later version.
This code is distributed in the hope that it will be useful, This code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU

View File

@@ -57,6 +57,44 @@ Files
Starting from Version 20250305 you should normally not use this file name any more as those styles would be added for all build environments. Instead define a parameter _custom_css_ in your [platformio.ini](platformio.ini) for the environments you would like to add some styles for. This parameter accepts a list of file names (relative to the project root, separated by , or as multi line entry) Starting from Version 20250305 you should normally not use this file name any more as those styles would be added for all build environments. Instead define a parameter _custom_css_ in your [platformio.ini](platformio.ini) for the environments you would like to add some styles for. This parameter accepts a list of file names (relative to the project root, separated by , or as multi line entry)
* [script.py](script.py)<br>
Starting from version 20251007 you can define a parameter "custom_script" in your [platformio.ini](platformio.ini).
This parameter can contain a list of file names (relative to the project root) that will be added as a [platformio extra script](https://docs.platformio.org/en/latest/scripting/index.html#scripting). The scripts will be loaded at the end of the main [extra_script](../../extra_script.py).
You can add code there that is specific for your build.
Example:
```
# PlatformIO extra script for obp60task
epdtype = "unknown"
pcbvers = "unknown"
for x in env["BUILD_FLAGS"]:
if x.startswith("-D HARDWARE_"):
pcbvers = x.split('_')[1]
if x.startswith("-D DISPLAY_"):
epdtype = x.split('_')[1]
propfilename = os.path.join(env["PROJECT_LIBDEPS_DIR"], env ["PIOENV"], "GxEPD2/library.properties")
properties = {}
with open(propfilename, 'r') as file:
for line in file:
match = re.match(r'^([^=]+)=(.*)$', line)
if match:
key = match.group(1).strip()
value = match.group(2).strip()
properties[key] = value
gxepd2vers = "unknown"
try:
if properties["name"] == "GxEPD2":
gxepd2vers = properties["version"]
except:
pass
env["CPPDEFINES"].extend([("BOARD", env["BOARD"]), ("EPDTYPE", epdtype), ("PCBVERS", pcbvers), ("GXEPD2VERS", gxepd2vers)])
print("added hardware info to CPPDEFINES")
print("friendly board name is '{}'".format(env.GetProjectOption ("board_name")))
```
Interfaces Interfaces
---------- ----------

View File

@@ -14,5 +14,6 @@ custom_config=
lib/exampletask/exampleConfig.json lib/exampletask/exampleConfig.json
custom_js=lib/exampletask/example.js custom_js=lib/exampletask/example.js
custom_css=lib/exampletask/example.css custom_css=lib/exampletask/example.css
custom_script=lib/exampletask/script.py
upload_port = /dev/esp32 upload_port = /dev/esp32
upload_protocol = esptool upload_protocol = esptool

View File

@@ -0,0 +1,4 @@
Import("env")
print("exampletask extra script running")
syntax error here

View File

@@ -2,6 +2,9 @@
#define _GWWIFI_H #define _GWWIFI_H
#include <WiFi.h> #include <WiFi.h>
#include <GWConfig.h> #include <GWConfig.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
class GwWifi{ class GwWifi{
private: private:
const GwConfigHandler *config; const GwConfigHandler *config;
@@ -16,13 +19,19 @@ class GwWifi{
bool apActive=false; bool apActive=false;
bool fixedApPass=true; bool fixedApPass=true;
bool clientIsConnected=false; bool clientIsConnected=false;
SemaphoreHandle_t wifiMutex=nullptr;
static const TickType_t WIFI_MUTEX_TIMEOUT=pdMS_TO_TICKS(1000);
bool acquireMutex();
void releaseMutex();
public: public:
const char *AP_password = "esp32nmea2k"; const char *AP_password = "esp32nmea2k";
GwWifi(const GwConfigHandler *config,GwLog *log, bool fixedApPass=true); GwWifi(const GwConfigHandler *config,GwLog *log, bool fixedApPass=true);
~GwWifi();
void setup(); void setup();
void loop(); void loop();
bool clientConnected(); bool clientConnected();
bool connectClient(); bool connectClient(); // Blocking version
bool connectClientAsync(); // Non-blocking version for other tasks
String apIP(); String apIP();
bool isApActive(){return apActive;} bool isApActive(){return apActive;}
bool isClientActive(){return wifiClient->asBoolean();} bool isClientActive(){return wifiClient->asBoolean();}

View File

@@ -1,7 +1,6 @@
#include <esp_wifi.h> #include <esp_wifi.h>
#include "GWWifi.h" #include "GWWifi.h"
GwWifi::GwWifi(const GwConfigHandler *config,GwLog *log, bool fixedApPass){ GwWifi::GwWifi(const GwConfigHandler *config,GwLog *log, bool fixedApPass){
this->config=config; this->config=config;
this->logger=log; this->logger=log;
@@ -9,6 +8,28 @@ GwWifi::GwWifi(const GwConfigHandler *config,GwLog *log, bool fixedApPass){
wifiSSID=config->getConfigItem(config->wifiSSID,true); wifiSSID=config->getConfigItem(config->wifiSSID,true);
wifiPass=config->getConfigItem(config->wifiPass,true); wifiPass=config->getConfigItem(config->wifiPass,true);
this->fixedApPass=fixedApPass; this->fixedApPass=fixedApPass;
wifiMutex=xSemaphoreCreateMutex();
if (wifiMutex==nullptr){
LOG_DEBUG(GwLog::ERROR,"GwWifi: unable to create mutex");
}
}
GwWifi::~GwWifi(){
if (wifiMutex!=nullptr){
vSemaphoreDelete(wifiMutex);
wifiMutex=nullptr;
}
}
bool GwWifi::acquireMutex(){
if (wifiMutex==nullptr) return false;
return xSemaphoreTake(wifiMutex,WIFI_MUTEX_TIMEOUT)==pdTRUE;
}
void GwWifi::releaseMutex(){
if (wifiMutex!=nullptr){
xSemaphoreGive(wifiMutex);
}
} }
void GwWifi::setup(){ void GwWifi::setup(){
LOG_DEBUG(GwLog::LOG,"Wifi setup"); LOG_DEBUG(GwLog::LOG,"Wifi setup");
@@ -85,8 +106,14 @@ bool GwWifi::connectInternal(){
if (wifiClient->asBoolean()){ if (wifiClient->asBoolean()){
clientIsConnected=false; clientIsConnected=false;
LOG_DEBUG(GwLog::LOG,"creating wifiClient ssid=%s",wifiSSID->asString().c_str()); LOG_DEBUG(GwLog::LOG,"creating wifiClient ssid=%s",wifiSSID->asString().c_str());
// CRITICAL SECTION: WiFi operations has to be serialized
if (!acquireMutex()){
LOG_DEBUG(GwLog::ERROR,"GwWifi: mutex timeout in connectInternal");
return false;
}
WiFi.setAutoReconnect(false); //#102 WiFi.setAutoReconnect(false); //#102
wl_status_t rt=WiFi.begin(wifiSSID->asCString(),wifiPass->asCString()); wl_status_t rt=WiFi.begin(wifiSSID->asCString(),wifiPass->asCString());
releaseMutex();
LOG_DEBUG(GwLog::LOG,"wifiClient connect returns %d",(int)rt); LOG_DEBUG(GwLog::LOG,"wifiClient connect returns %d",(int)rt);
lastConnectStart=millis(); lastConnectStart=millis();
return true; return true;
@@ -104,9 +131,43 @@ void GwWifi::loop(){
if (lastConnectStart > now || (lastConnectStart + RETRY_MILLIS) < now) if (lastConnectStart > now || (lastConnectStart + RETRY_MILLIS) < now)
{ {
LOG_DEBUG(GwLog::LOG,"wifiClient: retry connect to %s", wifiSSID->asCString()); LOG_DEBUG(GwLog::LOG,"wifiClient: retry connect to %s", wifiSSID->asCString());
WiFi.disconnect();
// Keep locked sections short to avoid cross-core stalls/WDT.
if (acquireMutex()){
WiFi.disconnect(true);
releaseMutex();
}
else{
LOG_DEBUG(GwLog::ERROR,"GwWifi: mutex timeout in loop (disconnect)");
}
delay(300);
if (acquireMutex()){
esp_err_t stopErr=esp_wifi_stop();
releaseMutex();
if (stopErr != ESP_OK){
LOG_DEBUG(GwLog::ERROR,"GwWifi: esp_wifi_stop failed: %d",(int)stopErr);
}
}
else{
LOG_DEBUG(GwLog::ERROR,"GwWifi: mutex timeout in loop (stop)");
}
delay(100);
if (acquireMutex()){
esp_err_t startErr=esp_wifi_start();
releaseMutex();
if (startErr != ESP_OK){
LOG_DEBUG(GwLog::ERROR,"GwWifi: esp_wifi_start failed: %d",(int)startErr);
}
connectInternal(); connectInternal();
} }
else{
LOG_DEBUG(GwLog::ERROR,"GwWifi: mutex timeout in loop (start)");
}
}
} }
else{ else{
if (! clientIsConnected){ if (! clientIsConnected){
@@ -126,11 +187,42 @@ void GwWifi::loop(){
} }
} }
} }
bool GwWifi::clientConnected(){ bool GwWifi::clientConnected(){
return WiFi.status() == WL_CONNECTED; // CRITICAL SECTION: WiFi.status() has to be protected
if (!acquireMutex()){
LOG_DEBUG(GwLog::ERROR,"GwWifi: mutex timeout in clientConnected");
return false; // conservative: assume not connected
}
bool result = WiFi.status() == WL_CONNECTED;
releaseMutex();
return result;
}; };
bool GwWifi::connectClient(){ bool GwWifi::connectClient(){
// CRITICAL SECTION: disconnect and connect has to be atomar
if (!acquireMutex()){
LOG_DEBUG(GwLog::ERROR,"GwWifi: mutex timeout in connectClient");
return false;
}
WiFi.disconnect(); WiFi.disconnect();
releaseMutex();
return connectInternal();
}
bool GwWifi::connectClientAsync(){
// Non-blocking version: Try to get Mutex but give up immediately
// Ideal for tasks which should not block
if (wifiMutex==nullptr){
LOG_DEBUG(GwLog::ERROR,"GwWifi: mutex not initialized in connectClientAsync");
return false;
}
if (xSemaphoreTake(wifiMutex, 0)!=pdTRUE){
LOG_DEBUG(GwLog::LOG,"GwWifi: connectClientAsync skipped - WiFi busy");
return false; // WiFiis busy, try again later
}
WiFi.disconnect();
xSemaphoreGive(wifiMutex);
return connectInternal(); return connectInternal();
} }

View File

@@ -0,0 +1,23 @@
/*
This code is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
defines for the channel modes(types)
*/
#ifndef _GWCHANNELMODES_H
#define _GWCHANNELMODES_H
#define GWSERIAL_TYPE_UNI 1
#define GWSERIAL_TYPE_BI 2
#define GWSERIAL_TYPE_RX 3
#define GWSERIAL_TYPE_TX 4
#define GWSERIAL_TYPE_UNK 0
#endif

View File

@@ -2,7 +2,7 @@
This code is free software; you can redistribute it and/or This code is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version. version 2 of the License, or (at your option) any later version.
This code is distributed in the hope that it will be useful, This code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
@@ -20,11 +20,7 @@
#endif #endif
#ifndef _GWHARDWARE_H #ifndef _GWHARDWARE_H
#define _GWHARDWARE_H #define _GWHARDWARE_H
#define GWSERIAL_TYPE_UNI 1 #include "GwChannelModes.h"
#define GWSERIAL_TYPE_BI 2
#define GWSERIAL_TYPE_RX 3
#define GWSERIAL_TYPE_TX 4
#define GWSERIAL_TYPE_UNK 0
#include <GwConfigItem.h> #include <GwConfigItem.h>
#include <HardwareSerial.h> #include <HardwareSerial.h>
#include "GwAppInfo.h" #include "GwAppInfo.h"

View File

@@ -2,7 +2,7 @@
This code is free software; you can redistribute it and/or This code is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version. version 2 of the License, or (at your option) any later version.
This code is distributed in the hope that it will be useful, This code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
@@ -35,7 +35,12 @@
#ifdef M5_GPS_KIT #ifdef M5_GPS_KIT
GWRESOURCE_USE(BASE,M5_GPS_KIT) GWRESOURCE_USE(BASE,M5_GPS_KIT)
GWRESOURCE_USE(SERIAL1,M5_GPS_KIT) GWRESOURCE_USE(SERIAL1,M5_GPS_KIT)
#define _GWI_SERIAL1 BOARD_LEFT1,-1,GWSERIAL_TYPE_UNI,9600 #define _GWI_SERIAL1 BOARD_LEFT1,-1,GWSERIAL_TYPE_RX,9600
#endif
#ifdef M5_GPSV2_KIT
GWRESOURCE_USE(BASE,M5_GPSV2_KIT)
GWRESOURCE_USE(SERIAL1,M5_GPSV2_KIT)
#define _GWI_SERIAL1 BOARD_LEFT1,-1,GWSERIAL_TYPE_RX,115200
#endif #endif
//M5 ProtoHub //M5 ProtoHub
@@ -61,7 +66,7 @@
#endif #endif
//can kit for M5 Atom //can kit for M5 Atom
#ifdef M5_CAN_KIT #if defined (M5_CAN_KIT)
GWRESOURCE_USE(BASE,M5_CAN_KIT) GWRESOURCE_USE(BASE,M5_CAN_KIT)
GWRESOURCE_USE(CAN,M5_CANKIT) GWRESOURCE_USE(CAN,M5_CANKIT)
#define ESP32_CAN_TX_PIN BOARD_LEFT1 #define ESP32_CAN_TX_PIN BOARD_LEFT1

View File

@@ -2,7 +2,7 @@
This code is free software; you can redistribute it and/or This code is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version. version 2 of the License, or (at your option) any later version.
This code is distributed in the hope that it will be useful, This code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU

View File

@@ -2,7 +2,7 @@
This code is free software; you can redistribute it and/or This code is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version. version 2 of the License, or (at your option) any later version.
This code is distributed in the hope that it will be useful, This code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
@@ -43,6 +43,13 @@
#define _GWI_SERIAL_GROOVE$GS$ GWSERIAL_TYPE_RX,9600 #define _GWI_SERIAL_GROOVE$GS$ GWSERIAL_TYPE_RX,9600
#endif #endif
#GROVE
//https://docs.m5stack.com/en/unit/Unit-GPS%20v1.1
#ifdef M5_GPSV11_UNIT$GS$
GWRESOURCE_USE(GROOVE$G$,M5_GPSV11_UNIT$GS$)
#define _GWI_SERIAL_GROOVE$GS$ GWSERIAL_TYPE_RX,115200
#endif
#GROVE #GROVE
//CAN via groove //CAN via groove
#ifdef M5_CANUNIT$GS$ #ifdef M5_CANUNIT$GS$
@@ -64,15 +71,15 @@
#endif #endif
#GROVE #GROVE
//#ifdef M5_ENV4$GS$ #ifdef M5_ENV4$GS$
// #ifndef M5_GROOVEIIC$GS$ #ifndef M5_GROOVEIIC$GS$
// #define M5_GROOVEIIC$GS$ #define M5_GROOVEIIC$GS$
// #endif #endif
// GROOVE_IIC(SHT3X,$Z$,1) GROOVE_IIC(SHT4X,$Z$,1)
// GROOVE_IIC(BMP280,$Z$,1) GROOVE_IIC(BMP280,$Z$,1)
// #define _GWSHT3X #define _GWSHT4X
// #define _GWBMP280 #define _GWBMP280
//#endif #endif
#GROVE #GROVE
//example: -DSHT3XG1_A : defines STH3Xn1 on grove A - x depends on the other devices //example: -DSHT3XG1_A : defines STH3Xn1 on grove A - x depends on the other devices
@@ -93,6 +100,25 @@
#define _GWSHT3X #define _GWSHT3X
#endif #endif
#GROVE
//example: -DSHT4XG1_A : defines STH4Xn1 on grove A - x depends on the other devices
#ifdef GWSHT4XG1$GS$
#ifndef M5_GROOVEIIC$GS$
#define M5_GROOVEIIC$GS$
#endif
GROOVE_IIC(SHT4X,$Z$,1)
#define _GWSHT4X
#endif
#GROVE
#ifdef GWSHT4XG2$GS$
#ifndef M5_GROOVEIIC$GS$
#define M5_GROOVEIIC$GS$
#endif
GROOVE_IIC(SHT4X,$Z$,2)
#define _GWSHT4X
#endif
#GROVE #GROVE
#ifdef GWQMP6988G1$GS$ #ifdef GWQMP6988G1$GS$
#ifndef M5_GROOVEIIC$GS$ #ifndef M5_GROOVEIIC$GS$

View File

@@ -23,6 +23,7 @@ class BME280Config : public IICSensorBase{
bool prAct=true; bool prAct=true;
bool tmAct=true; bool tmAct=true;
bool huAct=true; bool huAct=true;
bool sEnv=true;
tN2kTempSource tmSrc=tN2kTempSource::N2kts_InsideTemperature; tN2kTempSource tmSrc=tN2kTempSource::N2kts_InsideTemperature;
tN2kHumiditySource huSrc=tN2kHumiditySource::N2khs_InsideHumidity; tN2kHumiditySource huSrc=tN2kHumiditySource::N2khs_InsideHumidity;
tN2kPressureSource prSrc=tN2kPressureSource::N2kps_Atmospheric; tN2kPressureSource prSrc=tN2kPressureSource::N2kps_Atmospheric;
@@ -152,6 +153,7 @@ SensorBase::Creator registerBME280(GwApi *api){
CFG_SGET(s, prNam, prefix); \ CFG_SGET(s, prNam, prefix); \
CFG_SGET(s, tmOff, prefix); \ CFG_SGET(s, tmOff, prefix); \
CFG_SGET(s, prOff, prefix); \ CFG_SGET(s, prOff, prefix); \
CFG_SGET(s, sEnv, prefix); \
s->busId = bus; \ s->busId = bus; \
s->addr = baddr; \ s->addr = baddr; \
s->ok = true; \ s->ok = true; \

View File

@@ -29,6 +29,7 @@ class BMP280Config : public IICSensorBase{
public: public:
bool prAct=true; bool prAct=true;
bool tmAct=true; bool tmAct=true;
bool sEnv=true;
tN2kTempSource tmSrc=tN2kTempSource::N2kts_InsideTemperature; tN2kTempSource tmSrc=tN2kTempSource::N2kts_InsideTemperature;
tN2kPressureSource prSrc=tN2kPressureSource::N2kps_Atmospheric; tN2kPressureSource prSrc=tN2kPressureSource::N2kps_Atmospheric;
tN2kHumiditySource huSrc=tN2kHumiditySource::N2khs_Undef; tN2kHumiditySource huSrc=tN2kHumiditySource::N2khs_Undef;
@@ -150,6 +151,7 @@ SensorBase::Creator registerBMP280(GwApi *api){
CFG_SGET(s, prNam, prefix); \ CFG_SGET(s, prNam, prefix); \
CFG_SGET(s, tmOff, prefix); \ CFG_SGET(s, tmOff, prefix); \
CFG_SGET(s, prOff, prefix); \ CFG_SGET(s, prOff, prefix); \
CFG_SGET(s, sEnv,prefix); \
s->busId = bus; \ s->busId = bus; \
s->addr = baddr; \ s->addr = baddr; \
s->ok = true; \ s->ok = true; \

View File

@@ -104,12 +104,19 @@ void sendN2kTemperature(GwApi *api,CFG &cfg,double value, int counterId){
template <class CFG> template <class CFG>
void sendN2kEnvironmentalParameters(GwApi *api,CFG &cfg,double tmValue, double huValue, double prValue, int counterId){ void sendN2kEnvironmentalParameters(GwApi *api,CFG &cfg,double tmValue, double huValue, double prValue, int counterId){
if (! cfg.sEnv) return;
tN2kMsg msg; tN2kMsg msg;
SetN2kEnvironmentalParameters(msg,1,cfg.tmSrc,tmValue,cfg.huSrc,huValue,prValue); SetN2kEnvironmentalParameters(msg,1,cfg.tmSrc,tmValue,cfg.huSrc,huValue,prValue);
api->sendN2kMessage(msg); api->sendN2kMessage(msg);
api->increment(counterId,cfg.prefix+String("hum")); if (huValue != N2kDoubleNA){
api->increment(counterId,cfg.prefix+String("press")); api->increment(counterId,cfg.prefix+String("ehum"));
api->increment(counterId,cfg.prefix+String("temp")); }
if (prValue != N2kDoubleNA){
api->increment(counterId,cfg.prefix+String("epress"));
}
if (tmValue != N2kDoubleNA){
api->increment(counterId,cfg.prefix+String("etemp"));
}
} }
#ifndef _GWI_IIC1 #ifndef _GWI_IIC1

View File

@@ -23,7 +23,7 @@ static std::vector<IICGrove> iicGroveList;
#include "GwBME280.h" #include "GwBME280.h"
#include "GwBMP280.h" #include "GwBMP280.h"
#include "GwQMP6988.h" #include "GwQMP6988.h"
#include "GwSHT3X.h" #include "GwSHTXX.h"
#include <map> #include <map>
#include "GwTimer.h" #include "GwTimer.h"
@@ -91,6 +91,7 @@ void initIicTask(GwApi *api){
GwConfigHandler *config=api->getConfig(); GwConfigHandler *config=api->getConfig();
std::vector<SensorBase::Creator> creators; std::vector<SensorBase::Creator> creators;
creators.push_back(registerSHT3X(api)); creators.push_back(registerSHT3X(api));
creators.push_back(registerSHT4X(api));
creators.push_back(registerQMP6988(api)); creators.push_back(registerQMP6988(api));
creators.push_back(registerBME280(api)); creators.push_back(registerBME280(api));
creators.push_back(registerBMP280(api)); creators.push_back(registerBMP280(api));
@@ -147,13 +148,13 @@ bool initWire(GwLog *logger, TwoWire &wire, int num){
#ifdef _GWI_IIC1 #ifdef _GWI_IIC1
return initWireDo(logger,wire,num,_GWI_IIC1); return initWireDo(logger,wire,num,_GWI_IIC1);
#endif #endif
return initWireDo(logger,wire,num,"",GWIIC_SDA,GWIIC_SCL); return initWireDo(logger,wire,num,"",GWIIC_SCL,GWIIC_SDA);
} }
if (num == 2){ if (num == 2){
#ifdef _GWI_IIC2 #ifdef _GWI_IIC2
return initWireDo(logger,wire,num,_GWI_IIC2); return initWireDo(logger,wire,num,_GWI_IIC2);
#endif #endif
return initWireDo(logger,wire,num,"",GWIIC_SDA2,GWIIC_SCL2); return initWireDo(logger,wire,num,"",GWIIC_SCL2,GWIIC_SDA2);
} }
return false; return false;
} }

View File

@@ -9,6 +9,9 @@ class QMP6988Config : public IICSensorBase{
public: public:
String prNam="Pressure"; String prNam="Pressure";
bool prAct=true; bool prAct=true;
bool sEnv=true;
tN2kTempSource tmSrc=tN2kTempSource::N2kts_InsideTemperature;
tN2kHumiditySource huSrc=tN2kHumiditySource::N2khs_Undef;
tN2kPressureSource prSrc=tN2kPressureSource::N2kps_Atmospheric; tN2kPressureSource prSrc=tN2kPressureSource::N2kps_Atmospheric;
float prOff=0; float prOff=0;
QMP6988 *device=nullptr; QMP6988 *device=nullptr;
@@ -39,6 +42,7 @@ class QMP6988Config : public IICSensorBase{
float computed=pressure+prOff; float computed=pressure+prOff;
LOG_DEBUG(GwLog::DEBUG,"%s measure %2.0fPa, computed %2.0fPa",prefix.c_str(), pressure,computed); LOG_DEBUG(GwLog::DEBUG,"%s measure %2.0fPa, computed %2.0fPa",prefix.c_str(), pressure,computed);
sendN2kPressure(api,*this,computed,counterId); sendN2kPressure(api,*this,computed,counterId);
sendN2kEnvironmentalParameters(api,*this,N2kDoubleNA,N2kDoubleNA,computed,counterId);
} }
@@ -90,6 +94,7 @@ SensorBase::Creator registerQMP6988(GwApi *api){
CFG_SGET(s,prAct,prefix); \ CFG_SGET(s,prAct,prefix); \
CFG_SGET(s,intv,prefix); \ CFG_SGET(s,intv,prefix); \
CFG_SGET(s,prOff,prefix); \ CFG_SGET(s,prOff,prefix); \
CFG_SGET(s,sEnv,prefix); \
s->busId = bus; \ s->busId = bus; \
s->addr = baddr; \ s->addr = baddr; \
s->ok = true; \ s->ok = true; \

View File

@@ -1,138 +0,0 @@
#include "GwSHT3X.h"
#ifdef _GWSHT3X
class SHT3XConfig;
static GwSensorConfigInitializerList<SHT3XConfig> configs;
class SHT3XConfig : public IICSensorBase{
public:
String tmNam;
String huNam;
bool tmAct=false;
bool huAct=false;
tN2kHumiditySource huSrc;
tN2kTempSource tmSrc;
SHT3X *device=nullptr;
using IICSensorBase::IICSensorBase;
virtual bool isActive(){
return tmAct || huAct;
}
virtual bool initDevice(GwApi * api,TwoWire *wire){
if (! isActive()) return false;
device=new SHT3X();
device->init(addr,wire);
GwLog *logger=api->getLogger();
LOG_DEBUG(GwLog::LOG,"initialized %s at address %d, intv %ld",prefix.c_str(),(int)addr,intv);
return true;
}
virtual bool preinit(GwApi * api){
GwLog *logger=api->getLogger();
LOG_DEBUG(GwLog::LOG,"%s configured",prefix.c_str());
addHumidXdr(api,*this);
addTempXdr(api,*this);
return isActive();
}
virtual void measure(GwApi * api,TwoWire *wire, int counterId)
{
if (!device)
return;
GwLog *logger=api->getLogger();
int rt = 0;
if ((rt = device->get()) == 0)
{
double temp = device->cTemp;
temp = CToKelvin(temp);
double humid = device->humidity;
LOG_DEBUG(GwLog::DEBUG, "%s measure temp=%2.1f, humid=%2.0f",prefix.c_str(), (float)temp, (float)humid);
if (huAct)
{
sendN2kHumidity(api, *this, humid, counterId);
}
if (tmAct)
{
sendN2kTemperature(api, *this, temp, counterId);
}
}
else
{
LOG_DEBUG(GwLog::DEBUG, "unable to query %s: %d",prefix.c_str(), rt);
}
}
virtual void readConfig(GwConfigHandler *cfg){
if (ok) return;
configs.readConfig(this,cfg);
return;
}
};
SensorBase::Creator creator=[](GwApi *api,const String &prfx)-> SensorBase*{
if (! configs.knowsPrefix(prfx)) return nullptr;
return new SHT3XConfig(api,prfx);
};
SensorBase::Creator registerSHT3X(GwApi *api){
GwLog *logger=api->getLogger();
#if defined(GWSHT3X) || defined (GWSHT3X11)
{
api->addSensor(creator(api,"SHT3X11"));
CHECK_IIC1();
#pragma message "GWSHT3X11 defined"
}
#endif
#if defined(GWSHT3X12)
{
api->addSensor(creator(api,"SHT3X12"));
CHECK_IIC1();
#pragma message "GWSHT3X12 defined"
}
#endif
#if defined(GWSHT3X21)
{
api->addSensor(creator(api,"SHT3X21"));
CHECK_IIC2();
#pragma message "GWSHT3X21 defined"
}
#endif
#if defined(GWSHT3X22)
{
api->addSensor(creator(api,"SHT3X22"));
CHECK_IIC2();
#pragma message "GWSHT3X22 defined"
}
#endif
return creator;
};
/**
* we do not dynamically compute the config names
* just to get compile time errors if something does not fit
* correctly
*/
#define CFGSHT3X(s, prefix, bus, baddr) \
CFG_SGET(s, tmNam, prefix); \
CFG_SGET(s, huNam, prefix); \
CFG_SGET(s, iid, prefix); \
CFG_SGET(s, tmAct, prefix); \
CFG_SGET(s, huAct, prefix); \
CFG_SGET(s, intv, prefix); \
CFG_SGET(s, huSrc, prefix); \
CFG_SGET(s, tmSrc, prefix); \
s->busId = bus; \
s->addr = baddr; \
s->ok = true; \
s->intv *= 1000;
#define SCSHT3X(prefix, bus, addr) \
GWSENSORDEF(configs, SHT3XConfig, CFGSHT3X, prefix, bus, addr)
SCSHT3X(SHT3X11, 1, 0x44);
SCSHT3X(SHT3X12, 1, 0x45);
SCSHT3X(SHT3X21, 2, 0x44);
SCSHT3X(SHT3X22, 2, 0x45);
#else
SensorBase::Creator registerSHT3X(GwApi *api){
return SensorBase::Creator();
}
#endif

254
lib/iictask/GwSHTXX.cpp Normal file
View File

@@ -0,0 +1,254 @@
#include "GwSHTXX.h"
#if defined(_GWSHT3X) || defined(_GWSHT4X)
class SHTXXConfig : public IICSensorBase{
public:
String tmNam;
String huNam;
bool tmAct=false;
bool huAct=false;
bool sEnv=true;
tN2kHumiditySource huSrc;
tN2kTempSource tmSrc;
using IICSensorBase::IICSensorBase;
virtual bool isActive(){
return tmAct || huAct;
}
virtual bool preinit(GwApi * api){
GwLog *logger=api->getLogger();
LOG_DEBUG(GwLog::LOG,"%s configured",prefix.c_str());
addHumidXdr(api,*this);
addTempXdr(api,*this);
return isActive();
}
virtual bool doMeasure(GwApi * api,double &temp, double &humid){
return false;
}
virtual void measure(GwApi * api,TwoWire *wire, int counterId) override
{
GwLog *logger=api->getLogger();
double temp = N2kDoubleNA;
double humid = N2kDoubleNA;
if (doMeasure(api,temp,humid)){
temp = CToKelvin(temp);
LOG_DEBUG(GwLog::DEBUG, "%s measure temp=%2.1f, humid=%2.0f",prefix.c_str(), (float)temp, (float)humid);
if (huAct)
{
sendN2kHumidity(api, *this, humid, counterId);
}
if (tmAct)
{
sendN2kTemperature(api, *this, temp, counterId);
}
if (huAct || tmAct){
sendN2kEnvironmentalParameters(api,*this,temp,humid,N2kDoubleNA,counterId);
}
}
}
};
/**
* we do not dynamically compute the config names
* just to get compile time errors if something does not fit
* correctly
*/
#define INITSHTXX(type,prefix,bus,baddr) \
[] (type *s ,GwConfigHandler *cfg) { \
CFG_SGET(s, tmNam, prefix); \
CFG_SGET(s, huNam, prefix); \
CFG_SGET(s, iid, prefix); \
CFG_SGET(s, tmAct, prefix); \
CFG_SGET(s, huAct, prefix); \
CFG_SGET(s, intv, prefix); \
CFG_SGET(s, huSrc, prefix); \
CFG_SGET(s, tmSrc, prefix); \
CFG_SGET(s, sEnv,prefix); \
s->busId = bus; \
s->addr = baddr; \
s->ok = true; \
s->intv *= 1000; \
}
#if defined(_GWSHT3X)
class SHT3XConfig;
static GwSensorConfigInitializerList<SHT3XConfig> configs3;
class SHT3XConfig : public SHTXXConfig{
SHT3X *device=nullptr;
public:
using SHTXXConfig::SHTXXConfig;
virtual bool initDevice(GwApi * api,TwoWire *wire)override{
if (! isActive()) return false;
device=new SHT3X();
device->init(addr,wire);
GwLog *logger=api->getLogger();
LOG_DEBUG(GwLog::LOG,"initialized %s at address %d, intv %ld",prefix.c_str(),(int)addr,intv);
return true;
}
virtual bool doMeasure(GwApi *api,double &temp, double &humid) override{
if (!device)
return false;
int rt=0;
GwLog *logger=api->getLogger();
if ((rt = device->get()) == 0)
{
temp = device->cTemp;
humid = device->humidity;
return true;
}
else{
LOG_DEBUG(GwLog::DEBUG, "unable to query %s: %d",prefix.c_str(), rt);
}
return false;
}
virtual void readConfig(GwConfigHandler *cfg) override{
if (ok) return;
configs3.readConfig(this,cfg);
return;
}
};
SensorBase::Creator creator3=[](GwApi *api,const String &prfx)-> SensorBase*{
if (! configs3.knowsPrefix(prfx)) return nullptr;
return new SHT3XConfig(api,prfx);
};
SensorBase::Creator registerSHT3X(GwApi *api){
GwLog *logger=api->getLogger();
#if defined(GWSHT3X) || defined (GWSHT3X11)
{
api->addSensor(creator3(api,"SHT3X11"));
CHECK_IIC1();
#pragma message "GWSHT3X11 defined"
}
#endif
#if defined(GWSHT3X12)
{
api->addSensor(creator3(api,"SHT3X12"));
CHECK_IIC1();
#pragma message "GWSHT3X12 defined"
}
#endif
#if defined(GWSHT3X21)
{
api->addSensor(creator3(api,"SHT3X21"));
CHECK_IIC2();
#pragma message "GWSHT3X21 defined"
}
#endif
#if defined(GWSHT3X22)
{
api->addSensor(creator3(api,"SHT3X22"));
CHECK_IIC2();
#pragma message "GWSHT3X22 defined"
}
#endif
return creator3;
};
#define SCSHT3X(prefix, bus, addr) \
GwSensorConfigInitializer<SHT3XConfig> __initCFGSHT3X ## prefix \
(configs3,GwSensorConfig<SHT3XConfig>(#prefix,INITSHTXX(SHT3XConfig,prefix,bus,addr)));
SCSHT3X(SHT3X11, 1, 0x44);
SCSHT3X(SHT3X12, 1, 0x45);
SCSHT3X(SHT3X21, 2, 0x44);
SCSHT3X(SHT3X22, 2, 0x45);
#endif
#if defined(_GWSHT4X)
class SHT4XConfig;
static GwSensorConfigInitializerList<SHT4XConfig> configs4;
class SHT4XConfig : public SHTXXConfig{
SHT4X *device=nullptr;
public:
using SHTXXConfig::SHTXXConfig;
virtual bool initDevice(GwApi * api,TwoWire *wire)override{
if (! isActive()) return false;
device=new SHT4X();
device->begin(wire,addr);
GwLog *logger=api->getLogger();
LOG_DEBUG(GwLog::LOG,"initialized %s at address %d, intv %ld",prefix.c_str(),(int)addr,intv);
return true;
}
virtual bool doMeasure(GwApi *api,double &temp, double &humid) override{
if (!device)
return false;
GwLog *logger=api->getLogger();
if (device->update())
{
temp = device->cTemp;
humid = device->humidity;
return true;
}
else{
LOG_DEBUG(GwLog::DEBUG, "unable to query %s",prefix.c_str());
}
return false;
}
virtual void readConfig(GwConfigHandler *cfg) override{
if (ok) return;
configs4.readConfig(this,cfg);
return;
}
};
SensorBase::Creator creator4=[](GwApi *api,const String &prfx)-> SensorBase*{
if (! configs4.knowsPrefix(prfx)) return nullptr;
return new SHT4XConfig(api,prfx);
};
SensorBase::Creator registerSHT4X(GwApi *api){
GwLog *logger=api->getLogger();
#if defined(GWSHT4X) || defined (GWSHT4X11)
{
api->addSensor(creator3(api,"SHT4X11"));
CHECK_IIC1();
#pragma message "GWSHT4X11 defined"
}
#endif
#if defined(GWSHT4X12)
{
api->addSensor(creator3(api,"SHT4X12"));
CHECK_IIC1();
#pragma message "GWSHT4X12 defined"
}
#endif
#if defined(GWSHT4X21)
{
api->addSensor(creator3(api,"SHT4X21"));
CHECK_IIC2();
#pragma message "GWSHT4X21 defined"
}
#endif
#if defined(GWSHT4X22)
{
api->addSensor(creator3(api,"SHT4X22"));
CHECK_IIC2();
#pragma message "GWSHT4X22 defined"
}
#endif
return creator4;
};
#define SCSHT4X(prefix, bus, addr) \
GwSensorConfigInitializer<SHT4XConfig> __initCFGSHT4X ## prefix \
(configs4,GwSensorConfig<SHT4XConfig>(#prefix,INITSHTXX(SHT4XConfig,prefix,bus,addr)));
SCSHT4X(SHT4X11, 1, 0x44);
SCSHT4X(SHT4X12, 1, 0x45);
SCSHT4X(SHT4X21, 2, 0x44);
SCSHT4X(SHT4X22, 2, 0x45);
#endif
#endif
#ifndef _GWSHT3X
SensorBase::Creator registerSHT3X(GwApi *api){
return SensorBase::Creator();
}
#endif
#ifndef _GWSHT4X
SensorBase::Creator registerSHT4X(GwApi *api){
return SensorBase::Creator();
}
#endif

View File

@@ -1,10 +1,13 @@
#ifndef _GWSHT3X_H #ifndef _GWSHTXX_H
#define _GWSHT3X_H #define _GWSHTXX_H
#include "GwIicSensors.h" #include "GwIicSensors.h"
#ifdef _GWIIC #ifdef _GWIIC
#if defined(GWSHT3X) || defined(GWSHT3X11) || defined(GWSHT3X12) || defined(GWSHT3X21) || defined(GWSHT3X22) #if defined(GWSHT3X) || defined(GWSHT3X11) || defined(GWSHT3X12) || defined(GWSHT3X21) || defined(GWSHT3X22)
#define _GWSHT3X #define _GWSHT3X
#endif #endif
#if defined(GWSHT4X) || defined(GWSHT4X11) || defined(GWSHT4X12) || defined(GWSHT4X21) || defined(GWSHT4X22)
#define _GWSHT4X
#endif
#else #else
#undef _GWSHT3X #undef _GWSHT3X
#undef GWSHT3X #undef GWSHT3X
@@ -12,9 +15,19 @@
#undef GWSHT3X12 #undef GWSHT3X12
#undef GWSHT3X21 #undef GWSHT3X21
#undef GWSHT3X22 #undef GWSHT3X22
#undef _GWSHT4X
#undef GWSHT4X
#undef GWSHT4X11
#undef GWSHT4X12
#undef GWSHT4X21
#undef GWSHT4X22
#endif #endif
#ifdef _GWSHT3X #ifdef _GWSHT3X
#include "SHT3X.h" #include "SHT3X.h"
#endif #endif
SensorBase::Creator registerSHT3X(GwApi *api); #ifdef _GWSHT4X
#include "SHT4X.h"
#endif
SensorBase::Creator registerSHT3X(GwApi *api);
SensorBase::Creator registerSHT4X(GwApi *api);
#endif #endif

View File

@@ -1,4 +1,4 @@
#include "GwSHT3X.h" #include "GwSHTXX.h"
#ifdef _GWSHT3X #ifdef _GWSHT3X
bool SHT3X::init(uint8_t slave_addr_in, TwoWire* wire_in) bool SHT3X::init(uint8_t slave_addr_in, TwoWire* wire_in)

131
lib/iictask/SHT4X.cpp Normal file
View File

@@ -0,0 +1,131 @@
#include "GwSHTXX.h"
#ifdef _GWSHT4X
uint8_t crc8(const uint8_t *data, int len) {
/*
*
* CRC-8 formula from page 14 of SHT spec pdf
*
* Test data 0xBE, 0xEF should yield 0x92
*
* Initialization data 0xFF
* Polynomial 0x31 (x8 + x5 +x4 +1)
* Final XOR 0x00
*/
const uint8_t POLYNOMIAL(0x31);
uint8_t crc(0xFF);
for (int j = len; j; --j) {
crc ^= *data++;
for (int i = 8; i; --i) {
crc = (crc & 0x80) ? (crc << 1) ^ POLYNOMIAL : (crc << 1);
}
}
return crc;
}
bool SHT4X::begin(TwoWire* wire, uint8_t addr) {
_addr = addr;
_wire = wire;
int error;
_wire->beginTransmission(addr);
error = _wire->endTransmission();
if (error == 0) {
return true;
}
return false;
}
bool SHT4X::update() {
uint8_t readbuffer[6];
uint8_t cmd = SHT4x_NOHEAT_HIGHPRECISION;
uint16_t duration = 10;
if (_heater == SHT4X_NO_HEATER) {
if (_precision == SHT4X_HIGH_PRECISION) {
cmd = SHT4x_NOHEAT_HIGHPRECISION;
duration = 10;
}
if (_precision == SHT4X_MED_PRECISION) {
cmd = SHT4x_NOHEAT_MEDPRECISION;
duration = 5;
}
if (_precision == SHT4X_LOW_PRECISION) {
cmd = SHT4x_NOHEAT_LOWPRECISION;
duration = 2;
}
}
if (_heater == SHT4X_HIGH_HEATER_1S) {
cmd = SHT4x_HIGHHEAT_1S;
duration = 1100;
}
if (_heater == SHT4X_HIGH_HEATER_100MS) {
cmd = SHT4x_HIGHHEAT_100MS;
duration = 110;
}
if (_heater == SHT4X_MED_HEATER_1S) {
cmd = SHT4x_MEDHEAT_1S;
duration = 1100;
}
if (_heater == SHT4X_MED_HEATER_100MS) {
cmd = SHT4x_MEDHEAT_100MS;
duration = 110;
}
if (_heater == SHT4X_LOW_HEATER_1S) {
cmd = SHT4x_LOWHEAT_1S;
duration = 1100;
}
if (_heater == SHT4X_LOW_HEATER_100MS) {
cmd = SHT4x_LOWHEAT_100MS;
duration = 110;
}
// _i2c.writeByte(_addr, cmd, 1);
_wire->beginTransmission(_addr);
_wire->write(cmd);
_wire->write(1);
_wire->endTransmission();
delay(duration);
_wire->requestFrom(_addr, (uint8_t)6);
for (uint16_t i = 0; i < 6; i++) {
readbuffer[i] = _wire->read();
}
if (readbuffer[2] != crc8(readbuffer, 2) ||
readbuffer[5] != crc8(readbuffer + 3, 2)) {
return false;
}
float t_ticks = (uint16_t)readbuffer[0] * 256 + (uint16_t)readbuffer[1];
float rh_ticks = (uint16_t)readbuffer[3] * 256 + (uint16_t)readbuffer[4];
cTemp = -45 + 175 * t_ticks / 65535;
humidity = -6 + 125 * rh_ticks / 65535;
humidity = min(max(humidity, (float)0.0), (float)100.0);
return true;
}
void SHT4X::setPrecision(sht4x_precision_t prec) {
_precision = prec;
}
sht4x_precision_t SHT4X::getPrecision(void) {
return _precision;
}
void SHT4X::setHeater(sht4x_heater_t heat) {
_heater = heat;
}
sht4x_heater_t SHT4X::getHeater(void) {
return _heater;
}
#endif

76
lib/iictask/SHT4X.h Normal file
View File

@@ -0,0 +1,76 @@
#ifndef __SHT4X_H_
#define __SHT4X_H_
#include "Arduino.h"
#include "Wire.h"
#define SHT40_I2C_ADDR_44 0x44
#define SHT40_I2C_ADDR_45 0x45
#define SHT41_I2C_ADDR_44 0x44
#define SHT41_I2C_ADDR_45 0x45
#define SHT45_I2C_ADDR_44 0x44
#define SHT45_I2C_ADDR_45 0x45
#define SHT4x_DEFAULT_ADDR 0x44 /**< SHT4x I2C Address */
#define SHT4x_NOHEAT_HIGHPRECISION \
0xFD /**< High precision measurement, no heater */
#define SHT4x_NOHEAT_MEDPRECISION \
0xF6 /**< Medium precision measurement, no heater */
#define SHT4x_NOHEAT_LOWPRECISION \
0xE0 /**< Low precision measurement, no heater */
#define SHT4x_HIGHHEAT_1S \
0x39 /**< High precision measurement, high heat for 1 sec */
#define SHT4x_HIGHHEAT_100MS \
0x32 /**< High precision measurement, high heat for 0.1 sec */
#define SHT4x_MEDHEAT_1S \
0x2F /**< High precision measurement, med heat for 1 sec */
#define SHT4x_MEDHEAT_100MS \
0x24 /**< High precision measurement, med heat for 0.1 sec */
#define SHT4x_LOWHEAT_1S \
0x1E /**< High precision measurement, low heat for 1 sec */
#define SHT4x_LOWHEAT_100MS \
0x15 /**< High precision measurement, low heat for 0.1 sec */
#define SHT4x_READSERIAL 0x89 /**< Read Out of Serial Register */
#define SHT4x_SOFTRESET 0x94 /**< Soft Reset */
typedef enum {
SHT4X_HIGH_PRECISION,
SHT4X_MED_PRECISION,
SHT4X_LOW_PRECISION,
} sht4x_precision_t;
/** Optional pre-heater configuration setting */
typedef enum {
SHT4X_NO_HEATER,
SHT4X_HIGH_HEATER_1S,
SHT4X_HIGH_HEATER_100MS,
SHT4X_MED_HEATER_1S,
SHT4X_MED_HEATER_100MS,
SHT4X_LOW_HEATER_1S,
SHT4X_LOW_HEATER_100MS,
} sht4x_heater_t;
class SHT4X {
public:
bool begin(TwoWire* wire = &Wire, uint8_t addr = SHT40_I2C_ADDR_44);
bool update(void);
float cTemp = 0;
float humidity = 0;
void setPrecision(sht4x_precision_t prec);
sht4x_precision_t getPrecision(void);
void setHeater(sht4x_heater_t heat);
sht4x_heater_t getHeater(void);
private:
TwoWire* _wire;
uint8_t _addr;
sht4x_precision_t _precision = SHT4X_HIGH_PRECISION;
sht4x_heater_t _heater = SHT4X_NO_HEATER;
};
#endif

View File

@@ -1,49 +1,77 @@
[ [
{ {
"type": "array", "type": "array",
"name": "SHT3X", "name": "SHTXX",
"replace": [ "replace": [
{ {
"b": "1", "b": "1",
"i": "11", "i": "11",
"n": "99" "n": "99",
"x": "3"
}, },
{ {
"b": "1", "b": "1",
"i": "12", "i": "12",
"n": "98" "n": "98",
"x": "3"
}, },
{ {
"b": "2", "b": "2",
"i": "21", "i": "21",
"n": "109" "n": "109",
"x": "3"
}, },
{ {
"b": "2", "b": "2",
"i": "22", "i": "22",
"n": "108" "n": "108",
"x": "3"
},
{
"b": "1",
"i": "11",
"n": "119",
"x": "4"
},
{
"b": "1",
"i": "12",
"n": "118",
"x": "4"
},
{
"b": "2",
"i": "21",
"n": "129",
"x": "4"
},
{
"b": "2",
"i": "22",
"n": "128",
"x": "4"
} }
], ],
"children": [ "children": [
{ {
"name": "SHT3X$itmAct", "name": "SHT$xX$itmAct",
"label": "SHT3X$i Temp", "label": "SHT$xX$i Temp",
"type": "boolean", "type": "boolean",
"default": "true", "default": "true",
"description": "Enable the $i. I2C SHT3x temp sensor (bus $b)", "description": "Enable the $i. I2C SHT$xX temp sensor (bus $b)",
"category": "iicsensors$b", "category": "iicsensors$b",
"capabilities": { "capabilities": {
"SHT3X$i": "true" "SHT$xX$i": "true"
} }
}, },
{ {
"name": "SHT3X$itmSrc", "name": "SHT$xX$itmSrc",
"label": "SHT3X$i Temp Type", "label": "SHT$xX$i Temp Type",
"type": "list", "type": "list",
"default": "2", "default": "2",
"description": "the NMEA2000 source type for the temperature", "description": "the NMEA2000 source type for the temperature (PGN 130312,130311)",
"list": [ "list": [
{ {
"l": "SeaTemperature", "l": "SeaTemperature",
@@ -112,23 +140,23 @@
], ],
"category": "iicsensors$b", "category": "iicsensors$b",
"capabilities": { "capabilities": {
"SHT3X$i": "true" "SHT$xX$i": "true"
} }
}, },
{ {
"name": "SHT3X$ihuAct", "name": "SHT$xX$ihuAct",
"label": "SHT3X$i Humidity", "label": "SHT$xX$i Humidity",
"type": "boolean", "type": "boolean",
"default": "true", "default": "true",
"description": "Enable the $i. I2C SHT3x humidity sensor (bus $b)", "description": "Enable the $i. I2C SHT$xX humidity sensor (bus $b)",
"category": "iicsensors$b", "category": "iicsensors$b",
"capabilities": { "capabilities": {
"SHT3X$i": "true" "SHT$xX$i": "true"
} }
}, },
{ {
"name": "SHT3X$ihuSrc", "name": "SHT$xX$ihuSrc",
"label": "SHT3X$i Humid Type", "label": "SHT$xX$i Humid Type",
"list": [ "list": [
{ {
"l": "OutsideHumidity", "l": "OutsideHumidity",
@@ -141,57 +169,68 @@
], ],
"category": "iicsensors$b", "category": "iicsensors$b",
"capabilities": { "capabilities": {
"SHT3X": "true" "SHT$xX": "true"
} }
}, },
{ {
"name": "SHT3X$iiid", "name": "SHT$xX$iiid",
"label": "SHT3X$i N2K iid", "label": "SHT$xX$i N2K iid",
"type": "number", "type": "number",
"default": "$n", "default": "$n",
"description": "the N2K instance id for the $i. SHT3X Temperature and Humidity ", "description": "the N2K instance id for the $i. SHT$xX Temperature and Humidity (PGN 130312,130311) ",
"category": "iicsensors$b", "category": "iicsensors$b",
"min": 0, "min": 0,
"max": 253, "max": 253,
"check": "checkMinMax", "check": "checkMinMax",
"capabilities": { "capabilities": {
"SHT3X$i": "true" "SHT$xX$i": "true"
} }
}, },
{ {
"name": "SHT3X$iintv", "name": "SHT$xX$isEnv",
"label": "SHT3X$i Interval", "label": "SHT$xX$i send Env",
"type": "boolean",
"default": "true",
"description": "also send PGN 130311",
"category": "iicsensors$b",
"capabilities": {
"SHT$xX$i": "true"
}
},
{
"name": "SHT$xX$iintv",
"label": "SHT$xX$i Interval",
"type": "number", "type": "number",
"default": 2, "default": 2,
"description": "Interval(s) to query SHT3X Temperature and Humidity (1...300)", "description": "Interval(s) to query SHT$xX Temperature and Humidity (1...300)",
"category": "iicsensors$b", "category": "iicsensors$b",
"min": 1, "min": 1,
"max": 300, "max": 300,
"check": "checkMinMax", "check": "checkMinMax",
"capabilities": { "capabilities": {
"SHT3X$i": "true" "SHT$xX$i": "true"
} }
}, },
{ {
"name": "SHT3X$itmNam", "name": "SHT$xX$itmNam",
"label": "SHT3X$i Temp XDR", "label": "SHT$xX$i Temp XDR",
"type": "String", "type": "String",
"default": "Temp$i", "default": "Temp$i",
"description": "set the XDR transducer name for the $i. SHT3X Temperature, leave empty to disable NMEA0183 XDR ", "description": "set the XDR transducer name for the $i. SHT$xX Temperature, leave empty to disable NMEA0183 XDR ",
"category": "iicsensors$b", "category": "iicsensors$b",
"capabilities": { "capabilities": {
"SHT3X$i": "true" "SHT$xX$i": "true"
} }
}, },
{ {
"name": "SHT3X$ihuNam", "name": "SHT$xX$ihuNam",
"label": "SHT3X$i Humid XDR", "label": "SHT$xX$i Humid XDR",
"type": "String", "type": "String",
"default": "Humidity$i", "default": "Humidity$i",
"description": "set the XDR transducer name for the $i. SHT3X Humidity, leave empty to disable NMEA0183 XDR", "description": "set the XDR transducer name for the $i. SHT$xX Humidity, leave empty to disable NMEA0183 XDR",
"category": "iicsensors$b", "category": "iicsensors$b",
"capabilities": { "capabilities": {
"SHT3X$i": "true" "SHT$xX$i": "true"
} }
} }
] ]
@@ -247,6 +286,17 @@
"QMP6988$i": "true" "QMP6988$i": "true"
} }
}, },
{
"name": "QMP6988$isEnv",
"label": "QMP6988$i send Env",
"type": "boolean",
"default": "true",
"description": "also send PGN 130311",
"category": "iicsensors$b",
"capabilities": {
"QMP6988$i": "true"
}
},
{ {
"name": "QMP6988$iintv", "name": "QMP6988$iintv",
"label": "QMP6988-$i Interval", "label": "QMP6988-$i Interval",
@@ -473,7 +523,7 @@
"label": "BME280-$i N2K iid", "label": "BME280-$i N2K iid",
"type": "number", "type": "number",
"default": "$n", "default": "$n",
"description": "the N2K instance id for the BME280 Temperature and Humidity ", "description": "the N2K instance id for the BME280 Temperature, Humidity, Pressure (PGN 130312,130313, 130314) ",
"category": "iicsensors$b", "category": "iicsensors$b",
"min": 0, "min": 0,
"max": 253, "max": 253,
@@ -482,6 +532,17 @@
"BME280$i": "true" "BME280$i": "true"
} }
}, },
{
"name": "BME280$isEnv",
"label": "BME280$i send Env",
"type": "boolean",
"default": "true",
"description": "also send PGN 130311",
"category": "iicsensors$b",
"capabilities": {
"BME280$i": "true"
}
},
{ {
"name": "BME280$iintv", "name": "BME280$iintv",
"label": "BME280-$i Interval", "label": "BME280-$i Interval",
@@ -683,7 +744,7 @@
"label": "BMP280-$i N2K iid", "label": "BMP280-$i N2K iid",
"type": "number", "type": "number",
"default": "$n", "default": "$n",
"description": "the N2K instance id for the BMP280 Temperature", "description": "the N2K instance id for the BMP280 Temperature/Pressure (PGN 130312,130314)",
"category": "iicsensors$b", "category": "iicsensors$b",
"min": 0, "min": 0,
"max": 253, "max": 253,
@@ -692,6 +753,17 @@
"BMP280$i": "true" "BMP280$i": "true"
} }
}, },
{
"name": "BMP280$isEnv",
"label": "BMP280$i send Env",
"type": "boolean",
"default": "true",
"description": "also send PGN 130311",
"category": "iicsensors$b",
"capabilities": {
"BMP280$i": "true"
}
},
{ {
"name": "BMP280$iintv", "name": "BMP280$iintv",
"label": "BMP280-$i Interval", "label": "BMP280-$i Interval",

View File

@@ -11,6 +11,17 @@ build_flags=
-D M5_CAN_KIT -D M5_CAN_KIT
${env.build_flags} ${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] [env:m5stack-atom-bme280]
extends = sensors extends = sensors

View File

@@ -2,7 +2,7 @@
This code is free software; you can redistribute it and/or This code is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version. version 2 of the License, or (at your option) any later version.
This code is distributed in the hope that it will be useful, This code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
@@ -27,6 +27,8 @@ const double nmTom = 1.852 * 1000;
uint16_t DaysSince1970 = 0; uint16_t DaysSince1970 = 0;
#define boolbit(b) (b?1:0)
class MyAisDecoder : public AIS::AisDecoder class MyAisDecoder : public AIS::AisDecoder
{ {
public: public:
@@ -82,25 +84,24 @@ class MyAisDecoder : public AIS::AisDecoder
tN2kMsg N2kMsg; tN2kMsg N2kMsg;
// PGN129038 SetN2kPGN129038(
N2kMsg,
N2kMsg.SetPGN(129038L); _uMsgType,
N2kMsg.Priority = 4; (tN2kAISRepeat)_Repeat,
N2kMsg.AddByte((_Repeat & 0x03) << 6 | (_uMsgType & 0x3f)); _uMmsi,
N2kMsg.Add4ByteUInt(_uMmsi); _iPosLon/ 600000.0,
N2kMsg.Add4ByteDouble(_iPosLon / 600000.0, 1e-07); _iPosLat / 600000.0,
N2kMsg.Add4ByteDouble(_iPosLat / 600000.0, 1e-07); _bPosAccuracy,
N2kMsg.AddByte((_timestamp & 0x3f) << 2 | (_Raim & 0x01) << 1 | (_bPosAccuracy & 0x01)); _Raim,
N2kMsg.Add2ByteUDouble(decodeCog(_iCog), 1e-04); _timestamp,
N2kMsg.Add2ByteUDouble(_uSog * knToms/10.0, 0.01); decodeCog(_iCog),
N2kMsg.AddByte(0x00); // Communication State (19 bits) _uSog * knToms/10.0,
N2kMsg.AddByte(0x00); tN2kAISTransceiverInformation::N2kaischannel_A_VDL_reception,
N2kMsg.AddByte(0x00); // AIS transceiver information (5 bits) decodeHeading(_iHeading),
N2kMsg.Add2ByteUDouble(decodeHeading(_iHeading), 1e-04); decodeRot(_iRot),
N2kMsg.Add2ByteDouble(decodeRot(_iRot), 3.125E-05); // 1e-3/32.0 (tN2kAISNavStatus)_uNavstatus,
N2kMsg.AddByte(0xF0 | (_uNavstatus & 0x0f)); 0xff
N2kMsg.AddByte(0xff); // Reserved );
N2kMsg.AddByte(0xff); // SID (NA)
send(N2kMsg); send(N2kMsg);
} }
@@ -255,9 +256,40 @@ class MyAisDecoder : public AIS::AisDecoder
send(N2kMsg); send(N2kMsg);
} }
//mmsi, aidType, name + nameExt, posAccuracy, posLon, posLat, toBow, toStern, toPort, toStarboard
virtual void onType21(unsigned int , unsigned int , const std::string &, bool , int , int , unsigned int , unsigned int , unsigned int , unsigned int ) override { virtual void onType21(unsigned int mmsi , unsigned int aidType , const std::string & name, bool accuracy, int posLon, int posLat, unsigned int toBow,
unsigned int toStern, unsigned int toPort, unsigned int toStarboard,
unsigned int repeat,unsigned int timestamp, bool raim, bool virtualAton, bool offPosition) override {
//Serial.println("21"); //Serial.println("21");
//the name can be at most 120bit+88bit (35 byte) + termination -> 36 Byte
//in principle we should use tN2kAISAtoNReportData to directly call the library
//function for 129041. But this makes the conversion really complex.
bool assignedMode=false;
tN2kGNSStype gnssType=tN2kGNSStype::N2kGNSSt_GPS; //canboat considers 0 as undefined...
tN2kAISTransceiverInformation transceiverInfo=tN2kAISTransceiverInformation::N2kaischannel_A_VDL_reception;
tN2kMsg N2kMsg;
N2kMsg.SetPGN(129041);
N2kMsg.Priority=4;
N2kMsg.AddByte((repeat & 0x03) << 6 | (21 & 0x3f));
N2kMsg.Add4ByteUInt(mmsi); //N2kData.UserID
N2kMsg.Add4ByteDouble(posLon / 600000.0, 1e-07);
N2kMsg.Add4ByteDouble(posLat / 600000.0, 1e-07);
N2kMsg.AddByte((timestamp & 0x3f)<<2 | boolbit(raim)<<1 | boolbit(accuracy));
N2kMsg.Add2ByteUDouble(toBow+toStern, 0.1);
N2kMsg.Add2ByteUDouble(toPort+toStarboard, 0.1);
N2kMsg.Add2ByteUDouble(toStarboard, 0.1);
N2kMsg.Add2ByteUDouble(toBow, 0.1);
N2kMsg.AddByte(boolbit(assignedMode) << 7
| boolbit(virtualAton) << 6
| boolbit(offPosition) << 5
| (aidType & 0x1f));
N2kMsg.AddByte((gnssType & 0x0F) << 1 | 0xe0);
N2kMsg.AddByte(N2kUInt8NA); //status
N2kMsg.AddByte((transceiverInfo & 0x1f) | 0xe0);
//bit offset 208 (see canboat/pgns.xml) -> 26 bytes from start
//as MaxDataLen is 223 and the string can be at most 36 bytes + 2 byte heading - no further check here
N2kMsg.AddVarStr(name.c_str());
send(N2kMsg);
} }
virtual void onType24A(unsigned int _uMsgType, unsigned int _repeat, unsigned int _uMmsi, virtual void onType24A(unsigned int _uMsgType, unsigned int _repeat, unsigned int _uMmsi,

View File

@@ -143,7 +143,7 @@ private:
*/ */
GwXDRFoundMapping getOtherFieldMapping(GwXDRFoundMapping &found, int field){ GwXDRFoundMapping getOtherFieldMapping(GwXDRFoundMapping &found, int field){
if (found.empty) return GwXDRFoundMapping(); if (found.empty) return GwXDRFoundMapping();
return xdrMappings->getMapping(found.definition->category, return xdrMappings->getMapping(0,found.definition->category,
found.definition->selector, found.definition->selector,
field, field,
found.instanceId); found.instanceId);

View File

@@ -708,12 +708,37 @@ private:
} }
} }
//helper for converting the AIS transceiver info to talker/channel
void setTalkerChannel(tNMEA0183AISMsg &msg, tN2kAISTransceiverInformation &transceiver){
bool channelA=true;
bool own=false;
switch (transceiver){
case tN2kAISTransceiverInformation::N2kaischannel_A_VDL_reception:
channelA=true;
own=false;
break;
case tN2kAISTransceiverInformation::N2kaischannel_B_VDL_reception:
channelA=false;
own=false;
break;
case tN2kAISTransceiverInformation::N2kaischannel_A_VDL_transmission:
channelA=true;
own=true;
break;
case tN2kAISTransceiverInformation::N2kaischannel_B_VDL_transmission:
channelA=false;
own=true;
break;
}
msg.SetChannelAndTalker(channelA,own);
}
//***************************************************************************** //*****************************************************************************
// 129038 AIS Class A Position Report (Message 1, 2, 3) // 129038 AIS Class A Position Report (Message 1, 2, 3)
void HandleAISClassAPosReport(const tN2kMsg &N2kMsg) void HandleAISClassAPosReport(const tN2kMsg &N2kMsg)
{ {
unsigned char SID;
tN2kAISRepeat _Repeat; tN2kAISRepeat _Repeat;
uint32_t _UserID; // MMSI uint32_t _UserID; // MMSI
double _Latitude =N2kDoubleNA; double _Latitude =N2kDoubleNA;
@@ -732,64 +757,19 @@ private:
uint8_t _MessageType = 1; uint8_t _MessageType = 1;
tNMEA0183AISMsg NMEA0183AISMsg; tNMEA0183AISMsg NMEA0183AISMsg;
if (ParseN2kPGN129038(N2kMsg, SID, _Repeat, _UserID, _Latitude, _Longitude, _Accuracy, _RAIM, _Seconds, if (ParseN2kPGN129038(N2kMsg, _MessageType, _Repeat, _UserID, _Latitude, _Longitude, _Accuracy, _RAIM, _Seconds,
_COG, _SOG, _Heading, _ROT, _NavStatus,_AISTransceiverInformation,_SID)) _COG, _SOG, _Heading, _ROT, _NavStatus,_AISTransceiverInformation,_SID))
{ {
// Debug
#ifdef SERIAL_PRINT_AIS_FIELDS
Serial.println(" Msg 1 ");
const double pi = 3.1415926535897932384626433832795;
const double radToDeg = 180.0 / pi;
const double msTokn = 3600.0 / 1852.0;
const double radsToDegMin = 60 * 360.0 / (2 * pi); // [rad/s -> degree/minute]
Serial.print("Repeat: ");
Serial.println(_Repeat);
Serial.print("UserID: ");
Serial.println(_UserID);
Serial.print("Latitude: ");
Serial.println(_Latitude);
Serial.print("Longitude: ");
Serial.println(_Longitude);
Serial.print("Accuracy: ");
Serial.println(_Accuracy);
Serial.print("RAIM: ");
Serial.println(_RAIM);
Serial.print("Seconds: ");
Serial.println(_Seconds);
Serial.print("COG: ");
Serial.println(_COG * radToDeg);
Serial.print("SOG: ");
Serial.println(_SOG * msTokn);
Serial.print("Heading: ");
Serial.println(_Heading * radToDeg);
Serial.print("ROT: ");
Serial.println(_ROT * radsToDegMin);
Serial.print("NavStatus: ");
Serial.println(_NavStatus);
#endif
setTalkerChannel(NMEA0183AISMsg,_AISTransceiverInformation);
if (_MessageType < 1 || _MessageType > 3) _MessageType=1; //only allow type 1...3 for 129038
if (SetAISClassABMessage1(NMEA0183AISMsg, _MessageType, _Repeat, _UserID, _Latitude, _Longitude, _Accuracy, if (SetAISClassABMessage1(NMEA0183AISMsg, _MessageType, _Repeat, _UserID, _Latitude, _Longitude, _Accuracy,
_RAIM, _Seconds, _COG, _SOG, _Heading, _ROT, _NavStatus)) _RAIM, _Seconds, _COG, _SOG, _Heading, _ROT, _NavStatus))
{ {
SendMessage(NMEA0183AISMsg); SendMessage(NMEA0183AISMsg);
#ifdef SERIAL_PRINT_AIS_NMEA
// Debug Print AIS-NMEA
Serial.print(NMEA0183AISMsg.GetPrefix());
Serial.print(NMEA0183AISMsg.Sender());
Serial.print(NMEA0183AISMsg.MessageCode());
for (int i = 0; i < NMEA0183AISMsg.FieldCount(); i++)
{
Serial.print(",");
Serial.print(NMEA0183AISMsg.Field(i));
}
char buf[7];
sprintf(buf, "*%02X\r\n", NMEA0183AISMsg.GetCheckSum());
Serial.print(buf);
#endif
} }
} }
} // end 129038 AIS Class A Position Report Message 1/3 } // end 129038 AIS Class A Position Report Message 1/3
@@ -825,84 +805,18 @@ private:
_Length, _Beam, _PosRefStbd, _PosRefBow, _ETAdate, _ETAtime, _Draught, _Destination,21, _Length, _Beam, _PosRefStbd, _PosRefBow, _ETAdate, _ETAtime, _Draught, _Destination,21,
_AISversion, _GNSStype, _DTE, _AISinfo,_SID)) _AISversion, _GNSStype, _DTE, _AISinfo,_SID))
{ {
setTalkerChannel(NMEA0183AISMsg,_AISinfo);
#ifdef SERIAL_PRINT_AIS_FIELDS
// Debug Print N2k Values
Serial.println(" Msg 5 ");
Serial.print("MessageID: ");
Serial.println(_MessageID);
Serial.print("Repeat: ");
Serial.println(_Repeat);
Serial.print("UserID: ");
Serial.println(_UserID);
Serial.print("IMONumber: ");
Serial.println(_IMONumber);
Serial.print("Callsign: ");
Serial.println(_Callsign);
Serial.print("VesselType: ");
Serial.println(_VesselType);
Serial.print("Name: ");
Serial.println(_Name);
Serial.print("Length: ");
Serial.println(_Length);
Serial.print("Beam: ");
Serial.println(_Beam);
Serial.print("PosRefStbd: ");
Serial.println(_PosRefStbd);
Serial.print("PosRefBow: ");
Serial.println(_PosRefBow);
Serial.print("ETAdate: ");
Serial.println(_ETAdate);
Serial.print("ETAtime: ");
Serial.println(_ETAtime);
Serial.print("Draught: ");
Serial.println(_Draught);
Serial.print("Destination: ");
Serial.println(_Destination);
Serial.print("GNSStype: ");
Serial.println(_GNSStype);
Serial.print("DTE: ");
Serial.println(_DTE);
Serial.println(" Msg 5 ");
#endif
if (SetAISClassAMessage5(NMEA0183AISMsg, _MessageID, _Repeat, _UserID, _IMONumber, _Callsign, _Name, _VesselType, if (SetAISClassAMessage5(NMEA0183AISMsg, _MessageID, _Repeat, _UserID, _IMONumber, _Callsign, _Name, _VesselType,
_Length, _Beam, _PosRefStbd, _PosRefBow, _ETAdate, _ETAtime, _Draught, _Destination, _Length, _Beam, _PosRefStbd, _PosRefBow, _ETAdate, _ETAtime, _Draught, _Destination,
_GNSStype, _DTE)) _GNSStype, _DTE,_AISversion))
{ {
if (NMEA0183AISMsg.BuildMsg5Part1()){
SendMessage(NMEA0183AISMsg.BuildMsg5Part1(NMEA0183AISMsg)); SendMessage(NMEA0183AISMsg);
#ifdef SERIAL_PRINT_AIS_NMEA
// Debug Print AIS-NMEA Message Type 5, Part 1
char buf[7];
Serial.print(NMEA0183AISMsg.GetPrefix());
Serial.print(NMEA0183AISMsg.Sender());
Serial.print(NMEA0183AISMsg.MessageCode());
for (int i = 0; i < NMEA0183AISMsg.FieldCount(); i++)
{
Serial.print(",");
Serial.print(NMEA0183AISMsg.Field(i));
} }
sprintf(buf, "*%02X\r\n", NMEA0183AISMsg.GetCheckSum()); if (NMEA0183AISMsg.BuildMsg5Part2()){
Serial.print(buf); SendMessage(NMEA0183AISMsg);
#endif
SendMessage(NMEA0183AISMsg.BuildMsg5Part2(NMEA0183AISMsg));
#ifdef SERIAL_PRINT_AIS_NMEA
// Print AIS-NMEA Message Type 5, Part 2
Serial.print(NMEA0183AISMsg.GetPrefix());
Serial.print(NMEA0183AISMsg.Sender());
Serial.print(NMEA0183AISMsg.MessageCode());
for (int i = 0; i < NMEA0183AISMsg.FieldCount(); i++)
{
Serial.print(",");
Serial.print(NMEA0183AISMsg.Field(i));
} }
sprintf(buf, "*%02X\r\n", NMEA0183AISMsg.GetCheckSum());
Serial.print(buf);
#endif
} }
} }
} }
@@ -926,35 +840,21 @@ private:
tN2kAISUnit _Unit; tN2kAISUnit _Unit;
bool _Display, _DSC, _Band, _Msg22, _State; bool _Display, _DSC, _Band, _Msg22, _State;
tN2kAISMode _Mode; tN2kAISMode _Mode;
tN2kAISTransceiverInformation _AISTranceiverInformation; tN2kAISTransceiverInformation _AISTransceiverInformation;
uint8_t _SID; uint8_t _SID;
if (ParseN2kPGN129039(N2kMsg, _MessageID, _Repeat, _UserID, _Latitude, _Longitude, _Accuracy, _RAIM, if (ParseN2kPGN129039(N2kMsg, _MessageID, _Repeat, _UserID, _Latitude, _Longitude, _Accuracy, _RAIM,
_Seconds, _COG, _SOG, _AISTranceiverInformation, _Heading, _Unit, _Display, _DSC, _Band, _Msg22, _Mode, _State,_SID)) _Seconds, _COG, _SOG, _AISTransceiverInformation, _Heading, _Unit, _Display, _DSC, _Band, _Msg22, _Mode, _State,_SID))
{ {
tNMEA0183AISMsg NMEA0183AISMsg; tNMEA0183AISMsg NMEA0183AISMsg;
setTalkerChannel(NMEA0183AISMsg,_AISTransceiverInformation);
if (SetAISClassBMessage18(NMEA0183AISMsg, _MessageID, _Repeat, _UserID, _Latitude, _Longitude, _Accuracy, _RAIM, if (SetAISClassBMessage18(NMEA0183AISMsg, _MessageID, _Repeat, _UserID, _Latitude, _Longitude, _Accuracy, _RAIM,
_Seconds, _COG, _SOG, _Heading, _Unit, _Display, _DSC, _Band, _Msg22, _Mode, _State)) _Seconds, _COG, _SOG, _Heading, _Unit, _Display, _DSC, _Band, _Msg22, _Mode, _State))
{ {
SendMessage(NMEA0183AISMsg); SendMessage(NMEA0183AISMsg);
#ifdef SERIAL_PRINT_AIS_NMEA
// Debug Print AIS-NMEA
Serial.print(NMEA0183AISMsg.GetPrefix());
Serial.print(NMEA0183AISMsg.Sender());
Serial.print(NMEA0183AISMsg.MessageCode());
for (int i = 0; i < NMEA0183AISMsg.FieldCount(); i++)
{
Serial.print(",");
Serial.print(NMEA0183AISMsg.Field(i));
}
char buf[7];
sprintf(buf, "*%02X\r\n", NMEA0183AISMsg.GetCheckSum());
Serial.print(buf);
#endif
} }
} }
return; return;
@@ -976,8 +876,10 @@ private:
{ {
tNMEA0183AISMsg NMEA0183AISMsg; tNMEA0183AISMsg NMEA0183AISMsg;
setTalkerChannel(NMEA0183AISMsg,_AISInfo);
if (SetAISClassBMessage24PartA(NMEA0183AISMsg, _MessageID, _Repeat, _UserID, _Name)) if (SetAISClassBMessage24PartA(NMEA0183AISMsg, _MessageID, _Repeat, _UserID, _Name))
{ {
SendMessage(NMEA0183AISMsg);
} }
} }
return; return;
@@ -1005,77 +907,51 @@ private:
_Length, _Beam, _PosRefStbd, _PosRefBow, _MothershipID,_AISInfo,_SID)) _Length, _Beam, _PosRefStbd, _PosRefBow, _MothershipID,_AISInfo,_SID))
{ {
//
#ifdef SERIAL_PRINT_AIS_FIELDS
// Debug Print N2k Values
Serial.println(" Msg 24 ");
Serial.print("MessageID: ");
Serial.println(_MessageID);
Serial.print("Repeat: ");
Serial.println(_Repeat);
Serial.print("UserID: ");
Serial.println(_UserID);
Serial.print("VesselType: ");
Serial.println(_VesselType);
Serial.print("Vendor: ");
Serial.println(_Vendor);
Serial.print("Callsign: ");
Serial.println(_Callsign);
Serial.print("Length: ");
Serial.println(_Length);
Serial.print("Beam: ");
Serial.println(_Beam);
Serial.print("PosRefStbd: ");
Serial.println(_PosRefStbd);
Serial.print("PosRefBow: ");
Serial.println(_PosRefBow);
Serial.print("MothershipID: ");
Serial.println(_MothershipID);
Serial.println(" Msg 24 ");
#endif
tNMEA0183AISMsg NMEA0183AISMsg; tNMEA0183AISMsg NMEA0183AISMsg;
setTalkerChannel(NMEA0183AISMsg,_AISInfo);
if (SetAISClassBMessage24(NMEA0183AISMsg, _MessageID, _Repeat, _UserID, _VesselType, _Vendor, _Callsign, if (SetAISClassBMessage24PartB(NMEA0183AISMsg, _MessageID, _Repeat, _UserID, _VesselType, _Vendor, _Callsign,
_Length, _Beam, _PosRefStbd, _PosRefBow, _MothershipID)) _Length, _Beam, _PosRefStbd, _PosRefBow, _MothershipID))
{ {
SendMessage(NMEA0183AISMsg);
SendMessage(NMEA0183AISMsg.BuildMsg24PartA(NMEA0183AISMsg));
#ifdef SERIAL_PRINT_AIS_NMEA
// Debug Print AIS-NMEA
char buf[7];
Serial.print(NMEA0183AISMsg.GetPrefix());
Serial.print(NMEA0183AISMsg.Sender());
Serial.print(NMEA0183AISMsg.MessageCode());
for (int i = 0; i < NMEA0183AISMsg.FieldCount(); i++)
{
Serial.print(",");
Serial.print(NMEA0183AISMsg.Field(i));
}
sprintf(buf, "*%02X\r\n", NMEA0183AISMsg.GetCheckSum());
Serial.print(buf);
#endif
SendMessage(NMEA0183AISMsg.BuildMsg24PartB(NMEA0183AISMsg));
#ifdef SERIAL_PRINT_AIS_NMEA
Serial.print(NMEA0183AISMsg.GetPrefix());
Serial.print(NMEA0183AISMsg.Sender());
Serial.print(NMEA0183AISMsg.MessageCode());
for (int i = 0; i < NMEA0183AISMsg.FieldCount(); i++)
{
Serial.print(",");
Serial.print(NMEA0183AISMsg.Field(i));
}
sprintf(buf, "*%02X\r\n", NMEA0183AISMsg.GetCheckSum());
Serial.print(buf);
#endif
} }
} }
return; return;
} }
//*****************************************************************************
// PGN 129041 Aton
void HandleAISMessage21(const tN2kMsg &N2kMsg)
{
tN2kAISAtoNReportData data;
if (ParseN2kPGN129041(N2kMsg,data)){
tNMEA0183AISMsg nmea0183Msg;
setTalkerChannel(nmea0183Msg,data.AISTransceiverInformation);
if (SetAISMessage21(
nmea0183Msg,
data.Repeat,
data.UserID,
data.Latitude,
data.Longitude,
data.Accuracy,
data.RAIM,
data.Seconds,
data.Length,
data.Beam,
data.PositionReferenceStarboard,
data.PositionReferenceTrueNorth,
data.AtoNType,
data.OffPositionIndicator,
data.VirtualAtoNFlag,
data.AssignedModeFlag,
data.GNSSType,
data.AtoNStatus,
data.AtoNName
)){
SendMessage(nmea0183Msg);
}
}
}
void HandleSystemTime(const tN2kMsg &msg){ void HandleSystemTime(const tN2kMsg &msg){
unsigned char sid=-1; unsigned char sid=-1;
uint16_t DaysSince1970=N2kUInt16NA; uint16_t DaysSince1970=N2kUInt16NA;
@@ -1271,12 +1147,12 @@ private:
double Level=N2kDoubleNA; double Level=N2kDoubleNA;
double Capacity=N2kDoubleNA; double Capacity=N2kDoubleNA;
if (ParseN2kPGN127505(N2kMsg,Instance,FluidType,Level,Capacity)) { if (ParseN2kPGN127505(N2kMsg,Instance,FluidType,Level,Capacity)) {
GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRFLUID,FluidType,0,Instance); GwXDRFoundMapping mapping=xdrMappings->getMapping(Level,XDRFLUID,FluidType,0,Instance);
if (updateDouble(&mapping,Level)){ if (updateDouble(&mapping,Level)){
LOG_DEBUG(GwLog::DEBUG+1,"found fluidlevel mapping %s",mapping.definition->toString().c_str()); LOG_DEBUG(GwLog::DEBUG+1,"found fluidlevel mapping %s",mapping.definition->toString().c_str());
addToXdr(mapping.buildXdrEntry(Level)); addToXdr(mapping.buildXdrEntry(Level));
} }
mapping=xdrMappings->getMapping(XDRFLUID,FluidType,1,Instance); mapping=xdrMappings->getMapping(Capacity, XDRFLUID,FluidType,1,Instance);
if (updateDouble(&mapping,Capacity)){ if (updateDouble(&mapping,Capacity)){
LOG_DEBUG(GwLog::DEBUG+1,"found fluid capacity mapping %s",mapping.definition->toString().c_str()); LOG_DEBUG(GwLog::DEBUG+1,"found fluid capacity mapping %s",mapping.definition->toString().c_str());
addToXdr(mapping.buildXdrEntry(Capacity)); addToXdr(mapping.buildXdrEntry(Capacity));
@@ -1294,19 +1170,19 @@ private:
double BatteryTemperature=N2kDoubleNA; double BatteryTemperature=N2kDoubleNA;
if (ParseN2kPGN127508(N2kMsg,BatteryInstance,BatteryVoltage,BatteryCurrent,BatteryTemperature,SID)) { if (ParseN2kPGN127508(N2kMsg,BatteryInstance,BatteryVoltage,BatteryCurrent,BatteryTemperature,SID)) {
int i=0; int i=0;
GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRBAT,0,0,BatteryInstance); GwXDRFoundMapping mapping=xdrMappings->getMapping(BatteryVoltage, XDRBAT,0,0,BatteryInstance);
if (updateDouble(&mapping,BatteryVoltage)){ if (updateDouble(&mapping,BatteryVoltage)){
LOG_DEBUG(GwLog::DEBUG+1,"found BatteryVoltage mapping %s",mapping.definition->toString().c_str()); LOG_DEBUG(GwLog::DEBUG+1,"found BatteryVoltage mapping %s",mapping.definition->toString().c_str());
addToXdr(mapping.buildXdrEntry(BatteryVoltage)); addToXdr(mapping.buildXdrEntry(BatteryVoltage));
i++; i++;
} }
mapping=xdrMappings->getMapping(XDRBAT,0,1,BatteryInstance); mapping=xdrMappings->getMapping(BatteryCurrent,XDRBAT,0,1,BatteryInstance);
if (updateDouble(&mapping,BatteryCurrent)){ if (updateDouble(&mapping,BatteryCurrent)){
LOG_DEBUG(GwLog::DEBUG+1,"found BatteryCurrent mapping %s",mapping.definition->toString().c_str()); LOG_DEBUG(GwLog::DEBUG+1,"found BatteryCurrent mapping %s",mapping.definition->toString().c_str());
addToXdr(mapping.buildXdrEntry(BatteryCurrent)); addToXdr(mapping.buildXdrEntry(BatteryCurrent));
i++; i++;
} }
mapping=xdrMappings->getMapping(XDRBAT,0,2,BatteryInstance); mapping=xdrMappings->getMapping(BatteryTemperature,XDRBAT,0,2,BatteryInstance);
if (updateDouble(&mapping,BatteryTemperature)){ if (updateDouble(&mapping,BatteryTemperature)){
LOG_DEBUG(GwLog::DEBUG+1,"found BatteryTemperature mapping %s",mapping.definition->toString().c_str()); LOG_DEBUG(GwLog::DEBUG+1,"found BatteryTemperature mapping %s",mapping.definition->toString().c_str());
addToXdr(mapping.buildXdrEntry(BatteryTemperature)); addToXdr(mapping.buildXdrEntry(BatteryTemperature));
@@ -1338,13 +1214,13 @@ private:
SendMessage(NMEA0183Msg); SendMessage(NMEA0183Msg);
} }
int i=0; int i=0;
GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRTEMP,N2kts_OutsideTemperature,0,0); GwXDRFoundMapping mapping=xdrMappings->getMapping(OutsideAmbientAirTemperature, XDRTEMP,N2kts_OutsideTemperature,0,0);
if (updateDouble(&mapping,OutsideAmbientAirTemperature)){ if (updateDouble(&mapping,OutsideAmbientAirTemperature)){
LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str()); LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str());
addToXdr(mapping.buildXdrEntry(OutsideAmbientAirTemperature)); addToXdr(mapping.buildXdrEntry(OutsideAmbientAirTemperature));
i++; i++;
} }
mapping=xdrMappings->getMapping(XDRPRESSURE,N2kps_Atmospheric,0,0); mapping=xdrMappings->getMapping(AtmosphericPressure,XDRPRESSURE,N2kps_Atmospheric,0,0);
if (updateDouble(&mapping,AtmosphericPressure)){ if (updateDouble(&mapping,AtmosphericPressure)){
LOG_DEBUG(GwLog::DEBUG+1,"found pressure mapping %s",mapping.definition->toString().c_str()); LOG_DEBUG(GwLog::DEBUG+1,"found pressure mapping %s",mapping.definition->toString().c_str());
addToXdr(mapping.buildXdrEntry(AtmosphericPressure)); addToXdr(mapping.buildXdrEntry(AtmosphericPressure));
@@ -1379,19 +1255,19 @@ private:
SendMessage(NMEA0183Msg); SendMessage(NMEA0183Msg);
} }
GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRTEMP,TempSource,0,0); GwXDRFoundMapping mapping=xdrMappings->getMapping(Temperature, XDRTEMP,TempSource,0,0);
if (updateDouble(&mapping,Temperature)){ if (updateDouble(&mapping,Temperature)){
LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str()); LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str());
addToXdr(mapping.buildXdrEntry(Temperature)); addToXdr(mapping.buildXdrEntry(Temperature));
i++; i++;
} }
mapping=xdrMappings->getMapping(XDRHUMIDITY,HumiditySource,0,0); mapping=xdrMappings->getMapping(Humidity, XDRHUMIDITY,HumiditySource,0,0);
if (updateDouble(&mapping,Humidity)){ if (updateDouble(&mapping,Humidity)){
LOG_DEBUG(GwLog::DEBUG+1,"found humidity mapping %s",mapping.definition->toString().c_str()); LOG_DEBUG(GwLog::DEBUG+1,"found humidity mapping %s",mapping.definition->toString().c_str());
addToXdr(mapping.buildXdrEntry(Humidity)); addToXdr(mapping.buildXdrEntry(Humidity));
i++; i++;
} }
mapping=xdrMappings->getMapping(XDRPRESSURE,N2kps_Atmospheric,0,0); mapping=xdrMappings->getMapping(AtmosphericPressure, XDRPRESSURE,N2kps_Atmospheric,0,0);
if (updateDouble(&mapping,AtmosphericPressure)){ if (updateDouble(&mapping,AtmosphericPressure)){
LOG_DEBUG(GwLog::DEBUG+1,"found pressure mapping %s",mapping.definition->toString().c_str()); LOG_DEBUG(GwLog::DEBUG+1,"found pressure mapping %s",mapping.definition->toString().c_str());
addToXdr(mapping.buildXdrEntry(AtmosphericPressure)); addToXdr(mapping.buildXdrEntry(AtmosphericPressure));
@@ -1426,12 +1302,12 @@ private:
SendMessage(NMEA0183Msg); SendMessage(NMEA0183Msg);
} }
GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRTEMP,(int)TemperatureSource,0,TemperatureInstance); GwXDRFoundMapping mapping=xdrMappings->getMapping(Temperature, XDRTEMP,(int)TemperatureSource,0,TemperatureInstance);
if (updateDouble(&mapping,Temperature)){ if (updateDouble(&mapping,Temperature)){
LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str()); LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str());
addToXdr(mapping.buildXdrEntry(Temperature)); addToXdr(mapping.buildXdrEntry(Temperature));
} }
mapping=xdrMappings->getMapping(XDRTEMP,(int)TemperatureSource,1,TemperatureInstance); mapping=xdrMappings->getMapping(setTemperature, XDRTEMP,(int)TemperatureSource,1,TemperatureInstance);
if (updateDouble(&mapping,setTemperature)){ if (updateDouble(&mapping,setTemperature)){
LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str()); LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str());
addToXdr(mapping.buildXdrEntry(setTemperature)); addToXdr(mapping.buildXdrEntry(setTemperature));
@@ -1449,12 +1325,13 @@ private:
LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN); LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN);
return; return;
} }
GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRHUMIDITY,(int)HumiditySource,0,HumidityInstance); GwXDRFoundMapping mapping;
mapping=xdrMappings->getMapping(ActualHumidity, XDRHUMIDITY,(int)HumiditySource,0,HumidityInstance);
if (updateDouble(&mapping,ActualHumidity)){ if (updateDouble(&mapping,ActualHumidity)){
LOG_DEBUG(GwLog::DEBUG+1,"found humidity mapping %s",mapping.definition->toString().c_str()); LOG_DEBUG(GwLog::DEBUG+1,"found humidity mapping %s",mapping.definition->toString().c_str());
addToXdr(mapping.buildXdrEntry(ActualHumidity)); addToXdr(mapping.buildXdrEntry(ActualHumidity));
} }
mapping=xdrMappings->getMapping(XDRHUMIDITY,(int)HumiditySource,1,HumidityInstance); mapping=xdrMappings->getMapping(SetHumidity, XDRHUMIDITY,(int)HumiditySource,1,HumidityInstance);
if (updateDouble(&mapping,SetHumidity)){ if (updateDouble(&mapping,SetHumidity)){
LOG_DEBUG(GwLog::DEBUG+1,"found humidity mapping %s",mapping.definition->toString().c_str()); LOG_DEBUG(GwLog::DEBUG+1,"found humidity mapping %s",mapping.definition->toString().c_str());
addToXdr(mapping.buildXdrEntry(SetHumidity)); addToXdr(mapping.buildXdrEntry(SetHumidity));
@@ -1472,7 +1349,7 @@ private:
LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN); LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN);
return; return;
} }
GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRPRESSURE,(int)PressureSource,0,PressureInstance); GwXDRFoundMapping mapping=xdrMappings->getMapping(ActualPressure, XDRPRESSURE,(int)PressureSource,0,PressureInstance);
if (! updateDouble(&mapping,ActualPressure)) return; if (! updateDouble(&mapping,ActualPressure)) return;
LOG_DEBUG(GwLog::DEBUG+1,"found pressure mapping %s",mapping.definition->toString().c_str()); LOG_DEBUG(GwLog::DEBUG+1,"found pressure mapping %s",mapping.definition->toString().c_str());
addToXdr(mapping.buildXdrEntry(ActualPressure)); addToXdr(mapping.buildXdrEntry(ActualPressure));
@@ -1490,12 +1367,12 @@ private:
LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN); LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN);
} }
for (int i=0;i<8;i++){ for (int i=0;i<8;i++){
GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRENGINE,0,i,instance); GwXDRFoundMapping mapping=xdrMappings->getMapping(values[i], XDRENGINE,0,i,instance);
if (! updateDouble(&mapping,values[i])) continue; if (! updateDouble(&mapping,values[i])) continue;
addToXdr(mapping.buildXdrEntry(values[i])); addToXdr(mapping.buildXdrEntry(values[i]));
} }
for (int i=0;i< 2;i++){ for (int i=0;i< 2;i++){
GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRENGINE,0,i+8,instance); GwXDRFoundMapping mapping=xdrMappings->getMapping(ivalues[i],XDRENGINE,0,i+8,instance);
if (! updateDouble(&mapping,ivalues[i])) continue; if (! updateDouble(&mapping,ivalues[i])) continue;
addToXdr(mapping.buildXdrEntry((double)ivalues[i])); addToXdr(mapping.buildXdrEntry((double)ivalues[i]));
} }
@@ -1511,7 +1388,7 @@ private:
LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN); LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN);
} }
for (int i=0;i<3;i++){ for (int i=0;i<3;i++){
GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRATTITUDE,0,i,instance); GwXDRFoundMapping mapping=xdrMappings->getMapping(values[i], XDRATTITUDE,0,i,instance);
if (! updateDouble(&mapping,values[i])) continue; if (! updateDouble(&mapping,values[i])) continue;
addToXdr(mapping.buildXdrEntry(values[i])); addToXdr(mapping.buildXdrEntry(values[i]));
} }
@@ -1525,15 +1402,15 @@ private:
speed,pressure,tilt)){ speed,pressure,tilt)){
LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN); LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN);
} }
GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRENGINE,0,10,instance); GwXDRFoundMapping mapping=xdrMappings->getMapping(speed, XDRENGINE,0,10,instance);
if (updateDouble(&mapping,speed)){ if (updateDouble(&mapping,speed)){
addToXdr(mapping.buildXdrEntry(speed)); addToXdr(mapping.buildXdrEntry(speed));
} }
mapping=xdrMappings->getMapping(XDRENGINE,0,11,instance); mapping=xdrMappings->getMapping(pressure, XDRENGINE,0,11,instance);
if (updateDouble(&mapping,pressure)){ if (updateDouble(&mapping,pressure)){
addToXdr(mapping.buildXdrEntry(pressure)); addToXdr(mapping.buildXdrEntry(pressure));
} }
mapping=xdrMappings->getMapping(XDRENGINE,0,12,instance); mapping=xdrMappings->getMapping(tilt, XDRENGINE,0,12,instance);
if (updateDouble(&mapping,tilt)){ if (updateDouble(&mapping,tilt)){
addToXdr(mapping.buildXdrEntry((double)tilt)); addToXdr(mapping.buildXdrEntry((double)tilt));
} }
@@ -1559,12 +1436,12 @@ private:
LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN); LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN);
return; return;
} }
GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRTEMP,(int)TemperatureSource,0,TemperatureInstance); GwXDRFoundMapping mapping=xdrMappings->getMapping(Temperature, XDRTEMP,(int)TemperatureSource,0,TemperatureInstance);
if (updateDouble(&mapping,Temperature)){ if (updateDouble(&mapping,Temperature)){
LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str()); LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str());
addToXdr(mapping.buildXdrEntry(Temperature)); addToXdr(mapping.buildXdrEntry(Temperature));
} }
mapping=xdrMappings->getMapping(XDRTEMP,(int)TemperatureSource,1,TemperatureInstance); mapping=xdrMappings->getMapping(setTemperature, XDRTEMP,(int)TemperatureSource,1,TemperatureInstance);
if (updateDouble(&mapping,setTemperature)){ if (updateDouble(&mapping,setTemperature)){
LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str()); LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str());
addToXdr(mapping.buildXdrEntry(setTemperature)); addToXdr(mapping.buildXdrEntry(setTemperature));
@@ -1614,6 +1491,7 @@ private:
converters.registerConverter(129794UL, &N2kToNMEA0183Functions::HandleAISClassAMessage5); // AIS Class A Ship Static and Voyage related data, Message Type 5 converters.registerConverter(129794UL, &N2kToNMEA0183Functions::HandleAISClassAMessage5); // AIS Class A Ship Static and Voyage related data, Message Type 5
converters.registerConverter(129809UL, &N2kToNMEA0183Functions::HandleAISClassBMessage24A); // AIS Class B "CS" Static Data Report, Part A converters.registerConverter(129809UL, &N2kToNMEA0183Functions::HandleAISClassBMessage24A); // AIS Class B "CS" Static Data Report, Part A
converters.registerConverter(129810UL, &N2kToNMEA0183Functions::HandleAISClassBMessage24B); // AIS Class B "CS" Static Data Report, Part B converters.registerConverter(129810UL, &N2kToNMEA0183Functions::HandleAISClassBMessage24B); // AIS Class B "CS" Static Data Report, Part B
converters.registerConverter(129041UL, &N2kToNMEA0183Functions::HandleAISMessage21); // AIS Aton
#endif #endif
} }

View File

@@ -26,7 +26,7 @@ OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
#include <NMEA0183AISMessages.h> #include "NMEA0183AISMessages.h"
#include <N2kTypes.h> #include <N2kTypes.h>
#include <N2kMsg.h> #include <N2kMsg.h>
#include <string.h> #include <string.h>
@@ -34,7 +34,7 @@ OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//#include <unordered_map> //#include <unordered_map>
#include <sstream> #include <sstream>
#include <math.h> #include <math.h>
#include <NMEA0183AISMsg.h> #include "NMEA0183AISMsg.h"
const double pi=3.1415926535897932384626433832795; const double pi=3.1415926535897932384626433832795;
const double kmhToms=1000.0/3600.0; const double kmhToms=1000.0/3600.0;
@@ -47,17 +47,15 @@ const double nmTom=1.852*1000;
const double mToFathoms=0.546806649; const double mToFathoms=0.546806649;
const double mToFeet=3.2808398950131; const double mToFeet=3.2808398950131;
const double radsToDegMin = 60 * 360.0 / (2 * pi); // [rad/s -> degree/minute] const double radsToDegMin = 60 * 360.0 / (2 * pi); // [rad/s -> degree/minute]
const char Prefix='!';
std::vector<ship *> vships;
int numShips(){return vships.size();}
// ************************ Helper for AIS *********************************** // ************************ Helper for AIS ***********************************
static bool AddMessageType(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageType); static bool AddMessageType(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageType);
static bool AddRepeat(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t Repeat); static bool AddRepeat(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t Repeat);
static bool AddUserID(tNMEA0183AISMsg &NMEA0183AISMsg, uint32_t UserID); static bool AddUserID(tNMEA0183AISMsg &NMEA0183AISMsg, uint32_t UserID);
static bool AddIMONumber(tNMEA0183AISMsg &NMEA0183AISMsg, uint32_t &IMONumber); static bool AddIMONumber(tNMEA0183AISMsg &NMEA0183AISMsg, uint32_t &IMONumber);
static bool AddText(tNMEA0183AISMsg &NMEA0183AISMsg, char *FieldVal, uint8_t length); static bool AddText(tNMEA0183AISMsg &NMEA0183AISMsg, char *FieldVal, uint8_t length);
//static bool AddVesselType(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t VesselType);
static bool AddDimensions(tNMEA0183AISMsg &NMEA0183AISMsg, double Length, double Beam, double PosRefStbd, double PosRefBow); static bool AddDimensions(tNMEA0183AISMsg &NMEA0183AISMsg, double Length, double Beam, double PosRefStbd, double PosRefBow);
static bool AddNavStatus(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t &NavStatus); static bool AddNavStatus(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t &NavStatus);
static bool AddROT(tNMEA0183AISMsg &NMEA0183AISMsg, double &rot); static bool AddROT(tNMEA0183AISMsg &NMEA0183AISMsg, double &rot);
@@ -91,7 +89,7 @@ bool SetAISClassABMessage1( tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageType
if ( !AddNavStatus(NMEA0183AISMsg, NavStatus) ) return false; // 38-41 | 4 Navigational Status e.g.: "Under way sailing" if ( !AddNavStatus(NMEA0183AISMsg, NavStatus) ) return false; // 38-41 | 4 Navigational Status e.g.: "Under way sailing"
if ( !AddROT(NMEA0183AISMsg, ROT) ) return false; // 42-49 | 8 Rate of Turn (ROT) if ( !AddROT(NMEA0183AISMsg, ROT) ) return false; // 42-49 | 8 Rate of Turn (ROT)
if ( !AddSOG(NMEA0183AISMsg, SOG) ) return false; // 50-59 | 10 [m/s -> kts] SOG with one digit x10, 1023 = N/A if ( !AddSOG(NMEA0183AISMsg, SOG) ) return false; // 50-59 | 10 [m/s -> kts] SOG with one digit x10, 1023 = N/A
if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Accuracy, 1) ) return false;// 60 | 1 GPS Accuracy 1 oder 0, Default 0 if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Accuracy) ) return false;// 60 | 1 GPS Accuracy 1 oder 0, Default 0
if ( !AddLongitude(NMEA0183AISMsg, Longitude) ) return false; // 61-88 | 28 Longitude in Minutes / 10000 if ( !AddLongitude(NMEA0183AISMsg, Longitude) ) return false; // 61-88 | 28 Longitude in Minutes / 10000
if ( !AddLatitude(NMEA0183AISMsg, Latitude) ) return false; // 89-115 | 27 Latitude in Minutes / 10000 if ( !AddLatitude(NMEA0183AISMsg, Latitude) ) return false; // 89-115 | 27 Latitude in Minutes / 10000
if ( !AddCOG(NMEA0183AISMsg, COG) ) return false; // 116-127 | 12 Course over ground will be 3600 (0xE10) if that data is not available. if ( !AddCOG(NMEA0183AISMsg, COG) ) return false; // 116-127 | 12 Course over ground will be 3600 (0xE10) if that data is not available.
@@ -99,17 +97,12 @@ bool SetAISClassABMessage1( tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageType
if ( !AddSeconds(NMEA0183AISMsg, Seconds) ) return false; // 137-142 | 6 Seconds in UTC timestamp) if ( !AddSeconds(NMEA0183AISMsg, Seconds) ) return false; // 137-142 | 6 Seconds in UTC timestamp)
if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 2) ) return false; // 143-144 | 2 Maneuver Indicator: 0 (default) 1, 2 (not delivered within this PGN) if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 2) ) return false; // 143-144 | 2 Maneuver Indicator: 0 (default) 1, 2 (not delivered within this PGN)
if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 3) ) return false; // 145-147 | 3 Spare if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 3) ) return false; // 145-147 | 3 Spare
if ( !NMEA0183AISMsg.AddBoolToPayloadBin(RAIM, 1) ) return false; // 148-148 | 1 RAIM flag 0 = RAIM not in use (default), 1 = RAIM in use if ( !NMEA0183AISMsg.AddBoolToPayloadBin(RAIM) ) return false; // 148-148 | 1 RAIM flag 0 = RAIM not in use (default), 1 = RAIM in use
if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 19) ) return false; // 149-167 | 19 Radio Status (-> 0 NOT SENT WITH THIS PGN!!!!!) if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 19) ) return false; // 149-167 | 19 Radio Status (-> 0 NOT SENT WITH THIS PGN!!!!!)
if ( !NMEA0183AISMsg.InitAis()) return false;
if ( !NMEA0183AISMsg.Init("VDM","AI", Prefix) ) return false; int padBits=0;
if ( !NMEA0183AISMsg.AddStrField("1") ) return false; if ( !NMEA0183AISMsg.AddStrField( NMEA0183AISMsg.GetPayloadFix(padBits) ) ) return false;
if ( !NMEA0183AISMsg.AddStrField("1") ) return false; if ( !NMEA0183AISMsg.AddUInt32Field(padBits) ) return false;
if ( !NMEA0183AISMsg.AddEmptyField() ) return false;
if ( !NMEA0183AISMsg.AddStrField("A") ) return false;
if ( !NMEA0183AISMsg.AddStrField( NMEA0183AISMsg.GetPayload() ) ) return false;
if ( !NMEA0183AISMsg.AddStrField("0") ) return false; // Message 1,2,3 has always Zero Padding
return true; return true;
} }
@@ -121,14 +114,16 @@ bool SetAISClassAMessage5(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, u
uint32_t UserID, uint32_t IMONumber, char *Callsign, char *Name, uint32_t UserID, uint32_t IMONumber, char *Callsign, char *Name,
uint8_t VesselType, double Length, double Beam, double PosRefStbd, uint8_t VesselType, double Length, double Beam, double PosRefStbd,
double PosRefBow, uint16_t ETAdate, double ETAtime, double Draught, double PosRefBow, uint16_t ETAdate, double ETAtime, double Draught,
char *Destination, tN2kGNSStype GNSStype, uint8_t DTE ) { char *Destination, tN2kGNSStype GNSStype, uint8_t DTE,
tN2kAISVersion AISversion) {
// AIS Type 5 Message // AIS Type 5 Message
NMEA0183AISMsg.ClearAIS(); NMEA0183AISMsg.ClearAIS();
if ( !AddMessageType(NMEA0183AISMsg, 5) ) return false; // 0 - 5 | 6 Message Type -> Constant: 5 if ( !AddMessageType(NMEA0183AISMsg, 5) ) return false; // 0 - 5 | 6 Message Type -> Constant: 5
if ( !AddRepeat(NMEA0183AISMsg, Repeat) ) return false; // 6 - 7 | 2 Repeat Indicator: 0 = default; 3 = do not repeat any more if ( !AddRepeat(NMEA0183AISMsg, Repeat) ) return false; // 6 - 7 | 2 Repeat Indicator: 0 = default; 3 = do not repeat any more
if ( !AddUserID(NMEA0183AISMsg, UserID) ) return false; // 8 - 37 | 30 MMSI if ( !AddUserID(NMEA0183AISMsg, UserID) ) return false; // 8 - 37 | 30 MMSI
if ( !NMEA0183AISMsg.AddIntToPayloadBin(1, 2) ) return false; // 38 - 39 | 2 AIS Version -> 0 oder 1 NOT DERIVED FROM N2k, Always 1!!!! if ( !NMEA0183AISMsg.AddIntToPayloadBin((uint32_t)AISversion, 2) )
return false; // 38 - 39 | 2 AIS Version -> 0 oder 1 NOT DERIVED FROM N2k, Always 1!!!!
if ( !AddIMONumber(NMEA0183AISMsg, IMONumber) ) return false; // 40 - 69 | 30 IMO Number unisgned if ( !AddIMONumber(NMEA0183AISMsg, IMONumber) ) return false; // 40 - 69 | 30 IMO Number unisgned
if ( !AddText(NMEA0183AISMsg, Callsign, 42) ) return false; // 70 - 111 | 42 Call Sign WDE4178 -> 7 6-bit characters -> Ascii lt. Table) if ( !AddText(NMEA0183AISMsg, Callsign, 42) ) return false; // 70 - 111 | 42 Call Sign WDE4178 -> 7 6-bit characters -> Ascii lt. Table)
if ( !AddText(NMEA0183AISMsg, Name, 120) ) return false; // 112-231 | 120 Vessel Name POINT FERMIN -> 20 6-bit characters -> Ascii lt. Table if ( !AddText(NMEA0183AISMsg, Name, 120) ) return false; // 112-231 | 120 Vessel Name POINT FERMIN -> 20 6-bit characters -> Ascii lt. Table
@@ -146,10 +141,12 @@ bool SetAISClassAMessage5(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, u
// **************************************************************************** // ****************************************************************************
// AIS position report (class B 129039) -> Type 18: Standard Class B CS Position Report // AIS position report (class B 129039) -> Type 18: Standard Class B CS Position Report
// ParseN2kPGN129039(const tN2kMsg &N2kMsg, uint8_t &MessageID, tN2kAISRepeat &Repeat, uint32_t &UserID, // PGN129039
// ParseN2kAISClassBPosition(const tN2kMsg &N2kMsg, uint8_t &MessageID, tN2kAISRepeat &Repeat, uint32_t &UserID,
// double &Latitude, double &Longitude, bool &Accuracy, bool &RAIM, // double &Latitude, double &Longitude, bool &Accuracy, bool &RAIM,
// uint8_t &Seconds, double &COG, double &SOG, double &Heading, tN2kAISUnit &Unit, // uint8_t &Seconds, double &COG, double &SOG, tN2kAISTransceiverInformation &AISTransceiverInformation,
// bool &Display, bool &DSC, bool &Band, bool &Msg22, tN2kAISMode &Mode, bool &State) // double &Heading, tN2kAISUnit &Unit, bool &Display, bool &DSC, bool &Band, bool &Msg22, tN2kAISMode &Mode,
// bool &State)
// VDM, VDO (AIS VHF Data-link message 18) // VDM, VDO (AIS VHF Data-link message 18)
bool SetAISClassBMessage18(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, uint32_t UserID, bool SetAISClassBMessage18(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, uint32_t UserID,
double Latitude, double Longitude, bool Accuracy, bool RAIM, double Latitude, double Longitude, bool Accuracy, bool RAIM,
@@ -162,7 +159,7 @@ bool SetAISClassBMessage18(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, u
if ( !AddUserID(NMEA0183AISMsg, UserID) ) return false; // 8 - 37 | 30 MMSI if ( !AddUserID(NMEA0183AISMsg, UserID) ) return false; // 8 - 37 | 30 MMSI
if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 8) ) return false; // 38-45 | 8 Regional Reserved if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 8) ) return false; // 38-45 | 8 Regional Reserved
if ( !AddSOG(NMEA0183AISMsg, SOG) ) return false; // 46-55 | 10 [m/s -> kts] SOG with one digit x10, 1023 = N/A if ( !AddSOG(NMEA0183AISMsg, SOG) ) return false; // 46-55 | 10 [m/s -> kts] SOG with one digit x10, 1023 = N/A
if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Accuracy, 1)) return false; // 56 | 1 GPS Accuracy 1 oder 0, Default 0 if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Accuracy)) return false; // 56 | 1 GPS Accuracy 1 oder 0, Default 0
if ( !AddLongitude(NMEA0183AISMsg, Longitude) ) return false; // 57-84 | 28 Longitude in Minutes / 10000 if ( !AddLongitude(NMEA0183AISMsg, Longitude) ) return false; // 57-84 | 28 Longitude in Minutes / 10000
if ( !AddLatitude(NMEA0183AISMsg, Latitude) ) return false; // 85-111 | 27 Latitude in Minutes / 10000 if ( !AddLatitude(NMEA0183AISMsg, Latitude) ) return false; // 85-111 | 27 Latitude in Minutes / 10000
if ( !AddCOG(NMEA0183AISMsg, COG) ) return false; // 112-123 | 12 Course over ground will be 3600 (0xE10) if that data is not available. if ( !AddCOG(NMEA0183AISMsg, COG) ) return false; // 112-123 | 12 Course over ground will be 3600 (0xE10) if that data is not available.
@@ -171,20 +168,16 @@ bool SetAISClassBMessage18(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, u
if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 2) ) return false; // 139-140 | 2 Regional Reserved if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 2) ) return false; // 139-140 | 2 Regional Reserved
if ( !NMEA0183AISMsg.AddIntToPayloadBin(Unit, 1) ) return false; // 141 | 1 0=Class B SOTDMA unit 1=Class B CS (Carrier Sense) unit if ( !NMEA0183AISMsg.AddIntToPayloadBin(Unit, 1) ) return false; // 141 | 1 0=Class B SOTDMA unit 1=Class B CS (Carrier Sense) unit
if ( !NMEA0183AISMsg.AddIntToPayloadBin(Display, 1) ) return false; // 142 | 1 0=No visual display, 1=Has display, (Probably not reliable). if ( !NMEA0183AISMsg.AddIntToPayloadBin(Display, 1) ) return false; // 142 | 1 0=No visual display, 1=Has display, (Probably not reliable).
if ( !NMEA0183AISMsg.AddBoolToPayloadBin(DSC, 1) ) return false; // 143 | 1 If 1, unit is attached to a VHF voice radio with DSC capability. if ( !NMEA0183AISMsg.AddBoolToPayloadBin(DSC) ) return false; // 143 | 1 If 1, unit is attached to a VHF voice radio with DSC capability.
if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Band, 1) ) return false; // 144 | 1 If this flag is 1, the unit can use any part of the marine channel. if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Band) ) return false; // 144 | 1 If this flag is 1, the unit can use any part of the marine channel.
if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Msg22, 1) ) return false; // 145 | 1 If 1, unit can accept a channel assignment via Message Type 22. if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Msg22)) return false; // 145 | 1 If 1, unit can accept a channel assignment via Message Type 22.
if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Mode, 1) ) return false; // 146 | 1 Assigned-mode flag: 0 = autonomous mode (default), 1 = assigned mode if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Mode) ) return false; // 146 | 1 Assigned-mode flag: 0 = autonomous mode (default), 1 = assigned mode
if ( !NMEA0183AISMsg.AddBoolToPayloadBin(RAIM, 1) ) return false; // 147 | 1 as for Message Type 1,2,3 if ( !NMEA0183AISMsg.AddBoolToPayloadBin(RAIM) ) return false; // 147 | 1 as for Message Type 1,2,3
if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 20) ) return false; // 148-167 | 20 Radio Status not in PGN 129039 if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 20) ) return false; // 148-167 | 20 Radio Status not in PGN 129039
if ( !NMEA0183AISMsg.InitAis()) return false;
if ( !NMEA0183AISMsg.Init("VDM","AI", Prefix) ) return false; int padBits=0;
if ( !NMEA0183AISMsg.AddStrField("1") ) return false; if ( !NMEA0183AISMsg.AddStrField( NMEA0183AISMsg.GetPayloadFix(padBits) ) ) return false;
if ( !NMEA0183AISMsg.AddStrField("1") ) return false; if ( !NMEA0183AISMsg.AddUInt32Field(padBits) ) return false;
if ( !NMEA0183AISMsg.AddEmptyField() ) return false;
if ( !NMEA0183AISMsg.AddStrField("B") ) return false;
if ( !NMEA0183AISMsg.AddStrField( NMEA0183AISMsg.GetPayload() ) ) return false;
if ( !NMEA0183AISMsg.AddStrField("0") ) return false; // Message 18, has always Zero Padding
return true; return true;
} }
@@ -217,41 +210,28 @@ bool SetAISClassBMessage18(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, u
// Part A: MessageID, Repeat, UserID, ShipName -> store in vector to call on Part B arrivals!!! // Part A: MessageID, Repeat, UserID, ShipName -> store in vector to call on Part B arrivals!!!
// Part B: MessageID, Repeat, UserID, VesselType (5), Callsign (5), Length & Beam, PosRefBow,.. (5) // Part B: MessageID, Repeat, UserID, VesselType (5), Callsign (5), Length & Beam, PosRefBow,.. (5)
bool SetAISClassBMessage24PartA(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, uint32_t UserID, char *Name) { bool SetAISClassBMessage24PartA(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, uint32_t UserID, char *Name) {
// AIS Type 24 Message
bool found = false; NMEA0183AISMsg.ClearAIS();
for (size_t i = 0; i < vships.size(); i++) { // Common for PART A AND Part B Bit 0 - 39 / len 40
if ( vships[i]->_userID == UserID ) { if ( !AddMessageType(NMEA0183AISMsg, 24) ) return false; // 0 - 5 | 6 Message Type -> Constant: 24
found = true; if ( !AddRepeat(NMEA0183AISMsg, Repeat) ) return false; // 6 - 7 | 2 Repeat Indicator: 0 = default; 3 = do not repeat any more
break; if ( !AddUserID(NMEA0183AISMsg, UserID) ) return false; // 8 - 37 | 30 MMSI
} if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 2) ) return false; // 38-39 | 2 Part Number 0-1 ->
} // Part A: 40 + 128 = len 168
if ( ! found ) { if ( !AddText(NMEA0183AISMsg, Name, 120) ) return false; // 40-159 | 120 Vessel Name 20 6-bit characters -> Ascii Table
std::string nm; if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 8) ) return false; // 160-167 | 8 Spare
nm+= Name; if ( !NMEA0183AISMsg.InitAis() ) return false;
vships.push_back(new ship(UserID, nm)); int padBits=0;
} if ( !NMEA0183AISMsg.AddStrField( NMEA0183AISMsg.GetPayloadFix(padBits) ) ) return false;
if ( !NMEA0183AISMsg.AddUInt32Field(padBits) ) return false;
return true; return true;
} }
// *************************************************************************************************************** // ***************************************************************************************************************
bool SetAISClassBMessage24(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, bool SetAISClassBMessage24PartB(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat,
uint32_t UserID, uint8_t VesselType, char *VendorID, char *Callsign, uint32_t UserID, uint8_t VesselType, char *VendorID, char *Callsign,
double Length, double Beam, double PosRefStbd, double PosRefBow, uint32_t MothershipID ) { double Length, double Beam, double PosRefStbd, double PosRefBow, uint32_t MothershipID ) {
uint8_t PartNr = 0; // Identifier for the message part number; always 0 for Part A
char *ShipName = (char*)" "; // get from vector to look up for sent Messages Part A
uint8_t i;
for ( i = 0; i < vships.size(); i++) {
if ( vships[i]->_userID == UserID ) {
ShipName = const_cast<char*>( vships[i]->_shipName.c_str() );
}
}
if ( i > MAX_SHIP_IN_VECTOR ) {
std::vector<ship *>::iterator it=vships.begin();
delete *it;
vships.erase(it);
}
// AIS Type 24 Message // AIS Type 24 Message
NMEA0183AISMsg.ClearAIS(); NMEA0183AISMsg.ClearAIS();
@@ -259,11 +239,7 @@ bool SetAISClassBMessage24(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID,
if ( !AddMessageType(NMEA0183AISMsg, 24) ) return false; // 0 - 5 | 6 Message Type -> Constant: 24 if ( !AddMessageType(NMEA0183AISMsg, 24) ) return false; // 0 - 5 | 6 Message Type -> Constant: 24
if ( !AddRepeat(NMEA0183AISMsg, Repeat) ) return false; // 6 - 7 | 2 Repeat Indicator: 0 = default; 3 = do not repeat any more if ( !AddRepeat(NMEA0183AISMsg, Repeat) ) return false; // 6 - 7 | 2 Repeat Indicator: 0 = default; 3 = do not repeat any more
if ( !AddUserID(NMEA0183AISMsg, UserID) ) return false; // 8 - 37 | 30 MMSI if ( !AddUserID(NMEA0183AISMsg, UserID) ) return false; // 8 - 37 | 30 MMSI
if ( !NMEA0183AISMsg.AddIntToPayloadBin(PartNr, 2) ) return false; // 38-39 | 2 Part Number 0-1 -> if ( !NMEA0183AISMsg.AddIntToPayloadBin(1, 2) ) return false; // 38-39 | 2 Part Number 0-1 ->
// Part A: 40 + 128 = len 168
if ( !AddText(NMEA0183AISMsg, ShipName, 120) ) return false; // 40-159 | 120 Vessel Name 20 6-bit characters -> Ascii Table
if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 8) ) return false; // 160-167 | 8 Spare
// https://www.navcen.uscg.gov/?pageName=AISMessagesB // https://www.navcen.uscg.gov/?pageName=AISMessagesB
// PART B: 40 + 128 = len 168 // PART B: 40 + 128 = len 168
@@ -272,6 +248,59 @@ bool SetAISClassBMessage24(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID,
if ( !AddText(NMEA0183AISMsg, Callsign, 42) ) return false; // 218-259 | 90-131 | 42 Call Sign WDE4178 -> 7 6-bit characters, as in Msg Type 5 if ( !AddText(NMEA0183AISMsg, Callsign, 42) ) return false; // 218-259 | 90-131 | 42 Call Sign WDE4178 -> 7 6-bit characters, as in Msg Type 5
if ( !AddDimensions(NMEA0183AISMsg, Length, Beam, PosRefStbd, PosRefBow) ) return false; // 260-289 | 132-161 | 30 Dimensions if ( !AddDimensions(NMEA0183AISMsg, Length, Beam, PosRefStbd, PosRefBow) ) return false; // 260-289 | 132-161 | 30 Dimensions
if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 6) ) return false; // 290-295 | 162-167 | 6 Spare if ( !NMEA0183AISMsg.AddIntToPayloadBin(0, 6) ) return false; // 290-295 | 162-167 | 6 Spare
if ( !NMEA0183AISMsg.InitAis() ) return false;
int padBits=0;
if ( !NMEA0183AISMsg.AddStrField( NMEA0183AISMsg.GetPayloadFix(padBits) ) ) return false;
if ( !NMEA0183AISMsg.AddUInt32Field(padBits) ) return false;
return true;
}
// ****************************************************************************
// AIS ATON report (129041) -> Type 21: Position and status report for aids-to-navigation
// PGN129041
bool SetAISMessage21(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t Repeat, uint32_t UserID,
double Latitude, double Longitude, bool Accuracy, bool RAIM,
uint8_t Seconds, double Length, double Beam, double PositionReferenceStarboard,
double PositionReferenceTrueNord, tN2kAISAtoNType Type, bool OffPositionIndicator,
bool VirtualAtoNFlag, bool AssignedModeFlag, tN2kGNSStype GNSSType, uint8_t AtoNStatus,
char * atonName ) {
//
NMEA0183AISMsg.ClearAIS();
if ( !AddMessageType(NMEA0183AISMsg, 21) ) return false; // 0 - 5 | 6 Message Type -> Constant: 18
if ( !AddRepeat(NMEA0183AISMsg, Repeat) ) return false; // 6 - 7 | 2 Repeat Indicator: 0 = default; 3 = do not repeat any more
if ( !AddUserID(NMEA0183AISMsg, UserID) ) return false; // 8 - 37 | 30 MMSI
if ( ! NMEA0183AISMsg.AddIntToPayloadBin(Type,5)) return false; // | 5 aid type
//the name must be split:
//if it's > 120 bits the rest goes to the last parameter
if ( !NMEA0183AISMsg.AddEncodedCharToPayloadBin(atonName,120))
return false; // | 120 name
if ( !NMEA0183AISMsg.AddBoolToPayloadBin(Accuracy) ) return false; // | 1 accuracy
if ( !AddLongitude(NMEA0183AISMsg,Longitude)) return false; // | 28 lon
if ( !AddLatitude(NMEA0183AISMsg,Latitude)) return false; // | 27 lat
if ( !AddDimensions(NMEA0183AISMsg, Length, Beam,
PositionReferenceStarboard, PositionReferenceTrueNord)) return false; // | 30 dim
if ( !AddEPFDFixType(NMEA0183AISMsg,GNSSType)) return false; // | 4 fix type
if ( !AddSeconds(NMEA0183AISMsg,Seconds)) return false; // | 6 second
if ( !NMEA0183AISMsg.AddBoolToPayloadBin(OffPositionIndicator))
return false; // | 1 off
if ( !NMEA0183AISMsg.AddIntToPayloadBin(0,8)) return false; // | 8 reserverd
if ( !NMEA0183AISMsg.AddBoolToPayloadBin(RAIM)) return false; // | 1 raim
if ( !NMEA0183AISMsg.AddBoolToPayloadBin(VirtualAtoNFlag))
return false; // | 1 virt
if ( !NMEA0183AISMsg.AddBoolToPayloadBin(AssignedModeFlag))
return false; // | 1 assigned
if ( !NMEA0183AISMsg.AddIntToPayloadBin(0,1)) return false; // | 1 spare
size_t l=strlen(atonName);
if (l >=20){
uint8_t bitlen=(l-20)*6;
if (bitlen > 88) bitlen=88;
if ( !NMEA0183AISMsg.AddEncodedCharToPayloadBin(atonName+20,bitlen)) return false; // | name
}
if ( !NMEA0183AISMsg.InitAis() ) return false;
int padBits=0;
if ( !NMEA0183AISMsg.AddStrField( NMEA0183AISMsg.GetPayload(padBits) ) ) return false;
if ( !NMEA0183AISMsg.AddUInt32Field(padBits) ) return false;
return true; return true;
} }
@@ -325,7 +354,6 @@ bool AddIMONumber(tNMEA0183AISMsg &NMEA0183AISMsg, uint32_t &IMONumber) {
// 120bit Name or Destination // 120bit Name or Destination
bool AddText(tNMEA0183AISMsg &NMEA0183AISMsg, char *FieldVal, uint8_t length) { bool AddText(tNMEA0183AISMsg &NMEA0183AISMsg, char *FieldVal, uint8_t length) {
uint8_t len = length/6; uint8_t len = length/6;
if ( strlen(FieldVal) > len ) FieldVal[len] = 0; if ( strlen(FieldVal) > len ) FieldVal[len] = 0;
if ( !NMEA0183AISMsg.AddEncodedCharToPayloadBin(FieldVal, length) ) return false; if ( !NMEA0183AISMsg.AddEncodedCharToPayloadBin(FieldVal, length) ) return false;
return true; return true;
@@ -347,29 +375,26 @@ bool AddDimensions(tNMEA0183AISMsg &NMEA0183AISMsg, double Length, double Beam,
uint16_t _PosRefStbd = 0; uint16_t _PosRefStbd = 0;
uint16_t _PosRefPort = 0; uint16_t _PosRefPort = 0;
if (PosRefBow < 0) PosRefBow=0; //could be N2kIsNA if ( PosRefBow >= 0.0 && PosRefBow <= 511.0 ) {
if ( PosRefBow <= 511.0 ) { _PosRefBow = ceil(PosRefBow);
_PosRefBow = round(PosRefBow);
} else { } else {
_PosRefBow = 511; _PosRefBow = 511;
} }
if (PosRefStbd < 0 ) PosRefStbd=0; //could be N2kIsNA
if (PosRefStbd <= 63.0 ) { if ( PosRefStbd >= 0.0 && PosRefStbd <= 63.0 ) {
_PosRefStbd = round(PosRefStbd); _PosRefStbd = ceil(PosRefStbd);
} else { } else {
_PosRefStbd = 63; _PosRefStbd = 63;
} }
if ( !N2kIsNA(Length) ) { if ( !N2kIsNA(Length) ) {
if (Length >= PosRefBow){ _PosRefStern = ceil( Length ) - _PosRefBow;
_PosRefStern=round(Length - PosRefBow); if ( _PosRefStern < 0 ) _PosRefStern = 0;
}
if ( _PosRefStern > 511 ) _PosRefStern = 511; if ( _PosRefStern > 511 ) _PosRefStern = 511;
} }
if ( !N2kIsNA(Beam) ) { if ( !N2kIsNA(Beam) ) {
if (Beam >= PosRefStbd){ _PosRefPort = ceil( Beam ) - _PosRefStbd;
_PosRefPort = round( Beam - PosRefStbd); if ( _PosRefPort < 0 ) _PosRefPort = 0;
}
if ( _PosRefPort > 63 ) _PosRefPort = 63; if ( _PosRefPort > 63 ) _PosRefPort = 63;
} }
@@ -572,3 +597,5 @@ bool AddETADateTime(tNMEA0183AISMsg &NMEA0183AISMsg, uint16_t &ETAdate, double &
if ( ! NMEA0183AISMsg.AddIntToPayloadBin(minute, 6) ) return false; if ( ! NMEA0183AISMsg.AddIntToPayloadBin(minute, 6) ) return false;
return true; return true;
} }

View File

@@ -27,24 +27,16 @@ OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#ifndef _tNMEA0183AISMessages_H_ #ifndef _tNMEA0183AISMessages_H_
#define _tNMEA0183AISMessages_H_ #define _tNMEA0183AISMessages_H_
#include <stdio.h> #include <stdio.h>
#include <time.h> #include <time.h>
#include <string.h> #include <string.h>
#include <N2kTypes.h> #include <N2kTypes.h>
#include <NMEA0183AISMsg.h> #include "NMEA0183AISMsg.h"
#include <stddef.h> #include <stddef.h>
#include <vector> #include <vector>
#include <string> #include <string>
#define MAX_SHIP_IN_VECTOR 200
class ship {
public:
uint32_t _userID;
std::string _shipName;
ship(uint32_t UserID, std::string ShipName) : _userID(UserID), _shipName(ShipName) {}
};
// Types 1, 2 and 3: Position Report Class A or B // Types 1, 2 and 3: Position Report Class A or B
bool SetAISClassABMessage1(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageType, uint8_t Repeat, bool SetAISClassABMessage1(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageType, uint8_t Repeat,
@@ -57,7 +49,8 @@ bool SetAISClassAMessage5(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, ui
uint32_t UserID, uint32_t IMONumber, char *Callsign, char *Name, uint32_t UserID, uint32_t IMONumber, char *Callsign, char *Name,
uint8_t VesselType, double Length, double Beam, double PosRefStbd, uint8_t VesselType, double Length, double Beam, double PosRefStbd,
double PosRefBow, uint16_t ETAdate, double ETAtime, double Draught, double PosRefBow, uint16_t ETAdate, double ETAtime, double Draught,
char *Destination, tN2kGNSStype GNSStype, uint8_t DTE ); char *Destination, tN2kGNSStype GNSStype, uint8_t DTE,
tN2kAISVersion AISversion);
//***************************************************************************** //*****************************************************************************
// AIS position report (class B 129039) -> Standard Class B CS Position Report Message Type 18 Part B // AIS position report (class B 129039) -> Standard Class B CS Position Report Message Type 18 Part B
@@ -73,11 +66,19 @@ bool SetAISClassBMessage24PartA(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t Message
//***************************************************************************** //*****************************************************************************
// Static Data Report Class B, Message Type 24 // Static Data Report Class B, Message Type 24
bool SetAISClassBMessage24(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat, bool SetAISClassBMessage24PartB(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t MessageID, uint8_t Repeat,
uint32_t UserID, uint8_t VesselType, char *VendorID, char *Callsign, uint32_t UserID, uint8_t VesselType, char *VendorID, char *Callsign,
double Length, double Beam, double PosRefStbd, double PosRefBow, uint32_t MothershipID ); double Length, double Beam, double PosRefStbd, double PosRefBow, uint32_t MothershipID );
int numShips(); //*****************************************************************************
// Aton class 21
bool SetAISMessage21(tNMEA0183AISMsg &NMEA0183AISMsg, uint8_t Repeat, uint32_t UserID,
double Latitude, double Longitude, bool Accuracy, bool RAIM,
uint8_t Seconds, double Length, double Beam, double PositionReferenceStarboard,
double PositionReferenceTrueNord, tN2kAISAtoNType Type, bool OffPositionIndicator,
bool VirtualAtoNFlag, bool AssignedModeFlag, tN2kGNSStype GNSSType, uint8_t AtoNStatus,
char * atonName );
inline int32_t aRoundToInt(double x) { inline int32_t aRoundToInt(double x) {
return x >= 0 return x >= 0
? (int32_t) floor(x + 0.5) ? (int32_t) floor(x + 0.5)

View File

@@ -25,7 +25,7 @@ OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#include "NMEA0183AISMsg.h" #include "NMEA0183AISMsg.h"
#include <NMEA0183Msg.h> #include <NMEA0183Msg.h>
#include <Arduino.h> //#include <Arduino.h>
#include <math.h> #include <math.h>
#include <stdint.h> #include <stdint.h>
#include <stdlib.h> #include <stdlib.h>
@@ -43,52 +43,37 @@ tNMEA0183AISMsg::tNMEA0183AISMsg() {
//***************************************************************************** //*****************************************************************************
void tNMEA0183AISMsg::ClearAIS() { void tNMEA0183AISMsg::ClearAIS() {
PayloadBin[0]=0;
Payload[0]=0; Payload[0]=0;
PayloadBin.reset();
iAddPldBin=0; iAddPldBin=0;
iAddPld=0; iAddPld=0;
} }
//*****************************************************************************
// Add 6bit with no data.
bool tNMEA0183AISMsg::AddEmptyFieldToPayloadBin(uint8_t iBits) {
if ( (iAddPldBin + iBits * 6) >= AIS_BIN_MAX_LEN ) return false; // Is there room for any data
for (uint8_t i=0;i<iBits;i++) {
strncpy(PayloadBin+iAddPldBin, EmptyAISField, 6);
iAddPldBin+=6;
}
return true;
}
//***************************************************************************** //*****************************************************************************
bool tNMEA0183AISMsg::AddIntToPayloadBin(int32_t ival, uint16_t countBits) { bool tNMEA0183AISMsg::AddIntToPayloadBin(int32_t ival, uint16_t countBits) {
if ( (iAddPldBin + countBits ) >= AIS_BIN_MAX_LEN ) return false; // Is there room for any data if ( (iAddPldBin + countBits ) >= AIS_BIN_MAX_LEN ) return false; // Is there room for any data
AISBitSet bset(ival); bset = ival;
PayloadBin[iAddPldBin]=0;
uint16_t iAdd=iAddPldBin; uint16_t iAdd=iAddPldBin;
for(int i = countBits-1; i >= 0 ; i--) { for(int i = countBits-1; i >= 0 ; i--) {
PayloadBin[iAdd] = bset[i]?'1':'0'; PayloadBin[iAdd]=bset [i];
iAdd++; iAdd++;
} }
iAddPldBin += countBits; iAddPldBin += countBits;
PayloadBin[iAddPldBin]=0;
return true; return true;
} }
// **************************************************************************** //****************************************************************************
bool tNMEA0183AISMsg::AddBoolToPayloadBin(bool &bval, uint8_t size) { bool tNMEA0183AISMsg::AddBoolToPayloadBin(bool &bval) {
int8_t iTemp; if ( (iAddPldBin + 1 ) >= AIS_BIN_MAX_LEN ) return false;
(bval == true)? iTemp = 1 : iTemp = 0; PayloadBin[iAddPldBin]=bval;
if ( ! AddIntToPayloadBin(iTemp, size) ) return false; iAddPldBin++;
return true; return true;
} }
@@ -99,13 +84,11 @@ bool tNMEA0183AISMsg::AddEncodedCharToPayloadBin(char *sval, size_t countBits) {
if ( (iAddPldBin + countBits ) >= AIS_BIN_MAX_LEN ) return false; // Is there room for any data if ( (iAddPldBin + countBits ) >= AIS_BIN_MAX_LEN ) return false; // Is there room for any data
PayloadBin[iAddPldBin]=0; const char * ptr;
std::bitset<6> bs;
char * ptr;
size_t len = strlen(sval); // e.g.: should be 7 for Callsign size_t len = strlen(sval); // e.g.: should be 7 for Callsign
if ( len * 6 > countBits ) len = countBits / 6; if ( len * 6 > countBits ) len = countBits / 6;
for (int i = 0; i<len; i++) { for (size_t i = 0; i<len; i++) {
ptr = strchr(AsciiChar, sval[i]); ptr = strchr(AsciiChar, sval[i]);
if ( ptr ) { if ( ptr ) {
@@ -117,37 +100,44 @@ bool tNMEA0183AISMsg::AddEncodedCharToPayloadBin(char *sval, size_t countBits) {
AddIntToPayloadBin(0, 6); AddIntToPayloadBin(0, 6);
} }
} }
PayloadBin[iAddPldBin+1]=0;
// fill up with "@", also covers empty sval // fill up with "@", also covers empty sval
if ( len * 6 < countBits ) { if ( len * 6 < countBits ) {
for (int i=0;i<(countBits/6-len);i++) { for (size_t i=0;i<(countBits/6-len);i++) {
AddIntToPayloadBin(0, 6); AddIntToPayloadBin(0, 6);
} }
} }
PayloadBin[iAddPldBin]=0;
return true; return true;
} }
// ***************************************************************************** //*****************************************************************************
bool tNMEA0183AISMsg::ConvertBinaryAISPayloadBinToAscii(const char *payloadbin) { template <unsigned int S>
uint16_t len; int tNMEA0183AISMsg::ConvertBinaryAISPayloadBinToAscii(std::bitset<S> &src,uint16_t maxSize,uint16_t bitSize,uint16_t stoffset) {
Payload[0]='\0';
len = strlen( payloadbin ) / 6; // 28 uint16_t slen=maxSize;
if (stoffset >= slen) return 0;
slen-=stoffset;
uint16_t bitLen=bitSize > 0?bitSize:slen;
uint16_t len= bitLen / 6;
if ((len * 6) < bitLen) len+=1;
uint16_t padBits=0;
uint32_t offset; uint32_t offset;
char s[7]; std::bitset<6> s;
uint8_t dec; uint8_t dec;
int i; int i;
for ( i=0; i<len; i++ ) { for ( i=0; i<len; i++ ) {
offset = i * 6; offset = i * 6;
int k = 0; int k = 5;
for (int j=offset; j<offset+6; j++ ) { for (uint32_t j=offset; j<offset+6; j++ ) {
s[k] = payloadbin[j]; if (j < slen){
k++; s[k] = src[stoffset+j];
} }
s[k]=0; else{
dec = strtoull (s, NULL, 2); //binToDec s[k] = 0;
padBits++;
}
k--;
}
dec = s.to_ulong();
if (dec < 40 ) dec += 48; if (dec < 40 ) dec += 48;
else dec += 56; else dec += 56;
@@ -156,142 +146,56 @@ bool tNMEA0183AISMsg::ConvertBinaryAISPayloadBinToAscii(const char *payloadbin)
} }
Payload[i]=0; Payload[i]=0;
return true; return padBits;
}
void tNMEA0183AISMsg::SetChannelAndTalker(bool channelA,bool own){
channel[0]=channelA?'A':'B';
strcpy(talker,own?"VDO":"VDM");
} }
//********************** BUILD 2-parted AIS Sentences ************************ //********************** BUILD 2-parted AIS Sentences ************************
const tNMEA0183AISMsg& tNMEA0183AISMsg::BuildMsg5Part1(tNMEA0183AISMsg &AISMsg) { bool tNMEA0183AISMsg::InitAis(int max,int number,int sequence){
if ( !Init(talker,"AI", '!') ) return false;
Init("VDM", "AI", '!'); if ( !AddUInt32Field(max) ) return false;
AddStrField("2"); if ( !AddUInt32Field(number) ) return false;
AddStrField("1"); if (sequence >= 0){
AddStrField("5"); if ( !AddUInt32Field(sequence) ) return false;
AddStrField("A"); }
AddStrField( GetPayloadType5_Part1() ); else{
AddStrField("0"); if ( !AddEmptyField() ) return false;
}
return AISMsg; if ( !AddStrField(channel) ) return false;
return true;
}
bool tNMEA0183AISMsg::BuildMsg5Part1() {
if ( iAddPldBin != 424 ) return false;
InitAis(2,1,5);
int padBits=0;
AddStrField( GetPayload(padBits,0,336));
AddUInt32Field(padBits);
return true;
} }
const tNMEA0183AISMsg& tNMEA0183AISMsg::BuildMsg5Part2(tNMEA0183AISMsg &AISMsg) { bool tNMEA0183AISMsg::BuildMsg5Part2() {
if ( iAddPldBin != 424 ) return false;
Init("VDM", "AI", '!'); InitAis(2,2,5);
AddStrField("2"); int padBits=0;
AddStrField("2"); AddStrField( GetPayload(padBits,336,88) );
AddStrField("5"); AddUInt32Field(padBits);
AddStrField("A"); return true;
AddStrField( GetPayloadType5_Part2() );
AddStrField("2"); // Message 5, Part 2 has always 2 Padding Zeros
return AISMsg;
} }
const tNMEA0183AISMsg& tNMEA0183AISMsg::BuildMsg24PartA(tNMEA0183AISMsg &AISMsg) {
Init("VDM", "AI", '!');
AddStrField("1");
AddStrField("1");
AddEmptyField();
AddStrField("A");
AddStrField( GetPayloadType24_PartA() );
AddStrField("0");
return AISMsg;
}
const tNMEA0183AISMsg& tNMEA0183AISMsg::BuildMsg24PartB(tNMEA0183AISMsg &AISMsg) {
Init("VDM", "AI", '!');
AddStrField("1");
AddStrField("1");
AddEmptyField();
AddStrField("A");
AddStrField( GetPayloadType24_PartB() );
AddStrField("0"); // Message 24, both parts have always Zero Padding
return AISMsg;
}
//******************************* AIS PAYLOADS ********************************* //******************************* AIS PAYLOADS *********************************
//******************************************************************************
// get converted Payload for Message 1, 2, 3 & 18, always Length 168 // get converted Payload for Message 1, 2, 3 & 18, always Length 168
const char *tNMEA0183AISMsg::GetPayload() { const char *tNMEA0183AISMsg::GetPayloadFix(int &padBits,uint16_t fixLen){
uint16_t lenbin = iAddPldBin;
uint16_t lenbin = strlen( PayloadBin); if ( lenbin != fixLen ) return nullptr;
if ( lenbin != 168 ) return nullptr; return GetPayload(padBits,0,0);
}
if ( !ConvertBinaryAISPayloadBinToAscii( PayloadBin ) ) return nullptr; const char *tNMEA0183AISMsg::GetPayload(int &padBits,uint16_t offset,uint16_t bitLen) {
padBits=ConvertBinaryAISPayloadBinToAscii<AIS_BIN_MAX_LEN>(PayloadBin,iAddPldBin, bitLen,offset );
return Payload; return Payload;
} }
//******************************************************************************
// get converted Part 1 of Payload for Message 5
const char *tNMEA0183AISMsg::GetPayloadType5_Part1() {
uint16_t lenbin = strlen( PayloadBin);
if ( lenbin != 424 ) return nullptr;
char to[337];
strncpy(to, PayloadBin, 336); // First Part is always 336 Length
to[336]=0;
if ( !ConvertBinaryAISPayloadBinToAscii( to ) ) return nullptr;
return Payload;
}
//******************************************************************************
// get converted Part 2 of Payload for Message 5
const char *tNMEA0183AISMsg::GetPayloadType5_Part2() {
uint16_t lenbin = strlen( PayloadBin);
if ( lenbin != 424 ) return nullptr;
lenbin = 88; // Second Part is always 424 - 336 + 2 padding Zeros in Length
char to[91];
strncpy(to, PayloadBin + 336, lenbin);
to[88]='0'; to[89]='0'; to[90]=0;
if ( !ConvertBinaryAISPayloadBinToAscii( to ) ) return nullptr;
return Payload;
}
//******************************************************************************
// get converted Part A of Payload for Message 24
// Bit 0.....167, len 168
// In PayloadBin is Part A and Part B chained together with Length 296
const char *tNMEA0183AISMsg::GetPayloadType24_PartA() {
uint16_t lenbin = strlen( PayloadBin);
if ( lenbin != 296 ) return nullptr; // too short for Part A
char to[169]; // Part A has Length 168
*to = '\0';
for (int i=0; i<168; i++){
to[i] = PayloadBin[i];
}
to[168]=0;
if ( !ConvertBinaryAISPayloadBinToAscii( to ) ) return nullptr;
return Payload;
}
//******************************************************************************
// get converted Part B of Payload for Message 24
// Bit 0.....38 + bit39='1' (part number) + bit 168........295 296='\0' of total PayloadBin
// binary part B: len 40 + 128 = len 168
const char *tNMEA0183AISMsg::GetPayloadType24_PartB() {
uint16_t lenbin = strlen( PayloadBin);
if ( lenbin != 296 ) return nullptr; // too short for Part B
char to[169]; // Part B has Length 168
*to = '\0';
for (int i=0; i<39; i++){
to[i] = PayloadBin[i];
}
to[39] = 49; // part number 1
for (int i=40; i<168; i++) {
to[i] = PayloadBin[i+128];
}
to[168]=0;
if ( !ConvertBinaryAISPayloadBinToAscii( to ) ) return nullptr;
return Payload;
}

View File

@@ -45,43 +45,48 @@ OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#define BITSET_LENGTH 120 #define BITSET_LENGTH 120
typedef std::bitset<BITSET_LENGTH> AISBitSet;
class tNMEA0183AISMsg : public tNMEA0183Msg { class tNMEA0183AISMsg : public tNMEA0183Msg {
protected: // AIS-NMEA protected: // AIS-NMEA
std::bitset<BITSET_LENGTH> bset;
static const char *EmptyAISField; // 6bits 0 not used yet..... static const char *EmptyAISField; // 6bits 0 not used yet.....
static const char *AsciChar; static const char *AsciChar;
uint16_t iAddPldBin; uint16_t iAddPldBin;
char Payload[AIS_MSG_MAX_LEN]; char Payload[AIS_MSG_MAX_LEN];
uint8_t iAddPld; uint8_t iAddPld;
char talker[4]="VDM";
char channel[2]="A";
std::bitset<AIS_BIN_MAX_LEN> PayloadBin;
public: public:
char PayloadBin[AIS_BIN_MAX_LEN];
char PayloadBin2[AIS_BIN_MAX_LEN];
// Clear message // Clear message
void ClearAIS(); void ClearAIS();
public: public:
tNMEA0183AISMsg(); tNMEA0183AISMsg();
const char *GetPayload(); const char *GetPayloadFix(int &padBits,uint16_t fixLen=168);
const char *GetPayloadType5_Part1(); const char *GetPayload(int &padBits,uint16_t offset=0,uint16_t bitLen=0);
const char *GetPayloadType5_Part2();
const char *GetPayloadType24_PartA();
const char *GetPayloadType24_PartB();
const char *GetPayloadBin() const { return PayloadBin; }
const tNMEA0183AISMsg& BuildMsg5Part1(tNMEA0183AISMsg &AISMsg); bool BuildMsg5Part1();
const tNMEA0183AISMsg& BuildMsg5Part2(tNMEA0183AISMsg &AISMsg); bool BuildMsg5Part2();
const tNMEA0183AISMsg& BuildMsg24PartA(tNMEA0183AISMsg &AISMsg); bool InitAis(int max=1,int number=1,int sequence=-1);
const tNMEA0183AISMsg& BuildMsg24PartB(tNMEA0183AISMsg &AISMsg);
// Generally Used // Generally Used
bool AddIntToPayloadBin(int32_t ival, uint16_t countBits); bool AddIntToPayloadBin(int32_t ival, uint16_t countBits);
bool AddBoolToPayloadBin(bool &bval, uint8_t size); bool AddBoolToPayloadBin(bool &bval);
bool AddEncodedCharToPayloadBin(char *sval, size_t Length); bool AddEncodedCharToPayloadBin(char *sval, size_t Length);
bool AddEmptyFieldToPayloadBin(uint8_t iBits); /**
bool ConvertBinaryAISPayloadBinToAscii(const char *payloadbin); * @param channelA - if set A, otherwise B
* @param own - if set VDO, else VDM
*/
void SetChannelAndTalker(bool channelA,bool own=false);
/**
* convert the payload to ascii
* return the number of padding bits
* @param bitSize the number of bits to be used, 0 - use all bits
*/
template <unsigned int SZ>
int ConvertBinaryAISPayloadBinToAscii(std::bitset<SZ> &src,uint16_t maxSize, uint16_t bitSize,uint16_t offset=0);
// AIS Helper functions // AIS Helper functions
protected: protected:

View File

@@ -1,11 +1,11 @@
# NMEA2000 -> NMEA0183 AIS converter v1.0.0 # NMEA2000 to NMEA0183 AIS Converter
Import from https://github.com/ronzeiller/NMEA0183-AIS
NMEA0183 AIS library © Ronnie Zeiller, www.zeiller.eu NMEA0183 AIS library © Ronnie Zeiller, www.zeiller.eu
Addendum for NMEA2000 and NMEA0183 Library from Timo Lappalainen https://github.com/ttlappalainen Addendum for NMEA2000 and NMEA0183 Library from Timo Lappalainen https://github.com/ttlappalainen
to get NMEA0183 AIS data from N2k-bus
## Conversions: ## Conversions:
@@ -15,6 +15,33 @@ Addendum for NMEA2000 and NMEA0183 Library from Timo Lappalainen https://github.
- NMEA2000 PGN 129809 => AIS Class B "CS" Static Data Report, making a list of UserID (MMSI) and Ship Names used for Message 24 Part A - NMEA2000 PGN 129809 => AIS Class B "CS" Static Data Report, making a list of UserID (MMSI) and Ship Names used for Message 24 Part A
- NMEA2000 PGN 129810 => AIS Class B "CS" Static Data Report, Message 24 Part A+B - NMEA2000 PGN 129810 => AIS Class B "CS" Static Data Report, Message 24 Part A+B
### Versions
1.0.6 2024-03-25
- fixed to work with Timo´s NMEA2000 v4.21.3
1.0.5 2023-12-02
- removed VDO remote print statements
1.0.4 2023-12-02
- merged @Isoltero master with fixed memory over run, added VDO remote print statements Thanks to Luis Soltero
- fixed example, thanks to @arduinomnomnom
1.0.3 2022-05-01
- Update Examples: AISTransceiverInformation in ParseN2kPGN129039 for changes in NMEA2000 library: https://github.com/ttlappalainen/NMEA2000
1.0.2 2022-04-30
- bugfix: malloc without free. Thanks to Luis Soltero (Issue https://github.com/ronzeiller/NMEA0183-AIS/issues/3)
1.0.1 2022-03-15
- bugfix: buffer overrun missing space for termination. Thanks to Luis Soltero (Issue https://github.com/ronzeiller/NMEA0183-AIS/issues/2)
2020-12-25
- corrected Navigational Status 0. Thanks to Li-Ren (Issue https://github.com/ronzeiller/NMEA0183-AIS/issues/1)
1.0.0 2019-11-24
- initial upload
### Remarks ### Remarks
1. Message Type could be set to 1 or 3 (identical messages) on demand 1. Message Type could be set to 1 or 3 (identical messages) on demand
2. Maneuver Indicator (not part of NMEA2000 PGN 129038) => will be set to 0 (default) 2. Maneuver Indicator (not part of NMEA2000 PGN 129038) => will be set to 0 (default)
@@ -33,17 +60,14 @@ To use this library you need also:
## License ## License
Permission is hereby granted, free of charge, to any person obtaining a copy of MIT license
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to use, Copyright (c) 2019-2022 Ronnie Zeiller, www.zeiller.eu
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
Software, and to permit persons to whom the Software is furnished to do so, Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -162,8 +162,6 @@ bool Nmea2kTwai::checkRecovery(){
return strt; return strt;
} }
void Nmea2kTwai::loop(){ void Nmea2kTwai::loop(){
if (disabled) return; if (disabled) return;
timers.loop(); timers.loop();

View File

@@ -1,190 +0,0 @@
#if defined BOARD_OBP60S3 || defined BOARD_OBP40S3
#include "BoatDataCalibration.h"
#include <cmath>
#include <math.h>
#include <unordered_map>
CalibrationDataList calibrationData;
std::unordered_map<std::string, TypeCalibData> CalibrationDataList::calibMap; // list of calibration data instances
void CalibrationDataList::readConfig(GwConfigHandler* config, GwLog* logger)
// Initial load of calibration data into internal list
// This method is called once at init phase of <obp60task> to read the configuration values
{
std::string instance;
double offset;
double slope;
double smooth;
String calInstance = "";
String calOffset = "";
String calSlope = "";
String calSmooth = "";
// Load user format configuration values
String lengthFormat = config->getString(config->lengthFormat); // [m|ft]
String distanceFormat = config->getString(config->distanceFormat); // [m|km|nm]
String speedFormat = config->getString(config->speedFormat); // [m/s|km/h|kn]
String windspeedFormat = config->getString(config->windspeedFormat); // [m/s|km/h|kn|bft]
String tempFormat = config->getString(config->tempFormat); // [K|C|F]
// Read calibration settings for data instances
for (int i = 0; i < MAX_CALIBRATION_DATA; i++) {
calInstance = "calInstance" + String(i + 1);
calOffset = "calOffset" + String(i + 1);
calSlope = "calSlope" + String(i + 1);
calSmooth = "calSmooth" + String(i + 1);
instance = std::string(config->getString(calInstance, "---").c_str());
if (instance == "---") {
logger->logDebug(GwLog::LOG, "no calibration data for instance no. %d", i + 1);
continue;
}
calibMap[instance] = { 0.0f, 1.0f, 1.0f, 0.0f, false };
offset = (config->getString(calOffset, "")).toFloat();
slope = (config->getString(calSlope, "")).toFloat();
smooth = (config->getString(calSmooth, "")).toInt(); // user input is int; further math is done with double
// Convert calibration values to internal standard formats
if (instance == "AWS" || instance == "TWS") {
if (windspeedFormat == "m/s") {
// No conversion needed
} else if (windspeedFormat == "km/h") {
offset /= 3.6; // Convert km/h to m/s
} else if (windspeedFormat == "kn") {
offset /= 1.94384; // Convert kn to m/s
} else if (windspeedFormat == "bft") {
offset *= 2 + (offset / 2); // Convert Bft to m/s (approx) -> to be improved
}
} else if (instance == "AWA" || instance == "COG" || instance == "TWA" || instance == "TWD" || instance == "HDM" || instance == "PRPOS" || instance == "RPOS") {
offset *= M_PI / 180; // Convert deg to rad
} else if (instance == "DBT") {
if (lengthFormat == "m") {
// No conversion needed
} else if (lengthFormat == "ft") {
offset /= 3.28084; // Convert ft to m
}
} else if (instance == "SOG" || instance == "STW") {
if (speedFormat == "m/s") {
// No conversion needed
} else if (speedFormat == "km/h") {
offset /= 3.6; // Convert km/h to m/s
} else if (speedFormat == "kn") {
offset /= 1.94384; // Convert kn to m/s
}
} else if (instance == "WTemp") {
if (tempFormat == "K" || tempFormat == "C") {
// No conversion needed
} else if (tempFormat == "F") {
offset *= 9.0 / 5.0; // Convert °F to K
slope *= 9.0 / 5.0; // Convert °F to K
}
}
// transform smoothing factor from {0.01..10} to {0.3..0.95} and invert for exponential smoothing formula
if (smooth <= 0) {
smooth = 0;
} else {
if (smooth > 10) {
smooth = 10;
}
smooth = 0.3 + ((smooth - 0.01) * (0.95 - 0.3) / (10 - 0.01));
}
smooth = 1 - smooth;
calibMap[instance].offset = offset;
calibMap[instance].slope = slope;
calibMap[instance].smooth = smooth;
calibMap[instance].isCalibrated = false;
logger->logDebug(GwLog::LOG, "calibration data: %s, offset: %f, slope: %f, smoothing: %f", instance.c_str(),
calibMap[instance].offset, calibMap[instance].slope, calibMap[instance].smooth);
}
logger->logDebug(GwLog::LOG, "all calibration data read");
}
void CalibrationDataList::calibrateInstance(GwApi::BoatValue* boatDataValue, GwLog* logger)
// Method to calibrate the boat data value
{
std::string instance = boatDataValue->getName().c_str();
double offset = 0;
double slope = 1.0;
double dataValue = 0;
std::string format = "";
if (calibMap.find(instance) == calibMap.end()) {
logger->logDebug(GwLog::DEBUG, "BoatDataCalibration: %s not in calibration list", instance.c_str());
return;
} else if (!boatDataValue->valid) { // no valid boat data value, so we don't want to apply calibration data
calibMap[instance].isCalibrated = false;
return;
} else {
offset = calibMap[instance].offset;
slope = calibMap[instance].slope;
dataValue = boatDataValue->value;
format = boatDataValue->getFormat().c_str();
logger->logDebug(GwLog::DEBUG, "BoatDataCalibration: %s: value: %f, format: %s", instance.c_str(), dataValue, format.c_str());
if (format == "formatWind") { // instance is of type angle
dataValue = (dataValue * slope) + offset;
dataValue = fmod(dataValue, 2 * M_PI);
if (dataValue > (M_PI)) {
dataValue -= (2 * M_PI);
} else if (dataValue < (M_PI * -1)) {
dataValue += (2 * M_PI);
}
} else if (format == "formatCourse") { // instance is of type direction
dataValue = (dataValue * slope) + offset;
dataValue = fmod(dataValue, 2 * M_PI);
if (dataValue < 0) {
dataValue += (2 * M_PI);
}
} else if (format == "kelvinToC") { // instance is of type temperature
dataValue = ((dataValue - 273.15) * slope) + offset + 273.15;
} else {
dataValue = (dataValue * slope) + offset;
}
calibMap[instance].isCalibrated = true;
boatDataValue->value = dataValue;
calibrationData.smoothInstance(boatDataValue, logger); // smooth the boat data value
calibMap[instance].value = boatDataValue->value; // store the calibrated + smoothed value in the list
logger->logDebug(GwLog::DEBUG, "BoatDataCalibration: %s: Offset: %f, Slope: %f, Result: %f", instance.c_str(), offset, slope, calibMap[instance].value);
}
}
void CalibrationDataList::smoothInstance(GwApi::BoatValue* boatDataValue, GwLog* logger)
// Method to smoothen the boat data value
{
static std::unordered_map<std::string, double> lastValue; // array for last values of smoothed boat data values
std::string instance = boatDataValue->getName().c_str();
double oldValue = 0;
double dataValue = boatDataValue->value;
double smoothFactor = 0;
if (!boatDataValue->valid) { // no valid boat data value, so we don't want to smoothen value
return;
} else if (calibMap.find(instance) == calibMap.end()) {
logger->logDebug(GwLog::DEBUG, "BoatDataCalibration: smooth factor for %s not found in calibration list", instance.c_str());
return;
} else {
smoothFactor = calibMap[instance].smooth;
if (lastValue.find(instance) != lastValue.end()) {
oldValue = lastValue[instance];
dataValue = oldValue + (smoothFactor * (dataValue - oldValue)); // exponential smoothing algorithm
}
lastValue[instance] = dataValue; // store the new value for next cycle; first time, store only the current value and return
boatDataValue->value = dataValue; // set the smoothed value to the boat data value
}
}
#endif

View File

@@ -1,33 +0,0 @@
// Functions lib for data instance calibration
#ifndef _BOATDATACALIBRATION_H
#define _BOATDATACALIBRATION_H
#include "GwApi.h"
#include <string>
#include <unordered_map>
#define MAX_CALIBRATION_DATA 3 // maximum number of calibration data instances
typedef struct {
double offset; // calibration offset
double slope; // calibration slope
double smooth; // smoothing factor
double value; // calibrated data value
bool isCalibrated; // is data instance value calibrated?
} TypeCalibData;
class CalibrationDataList {
public:
static std::unordered_map<std::string, TypeCalibData> calibMap; // list of calibration data instances
void readConfig(GwConfigHandler* config, GwLog* logger);
void calibrateInstance(GwApi::BoatValue* boatDataValue, GwLog* logger);
void smoothInstance(GwApi::BoatValue* boatDataValue, GwLog* logger);
private:
};
extern CalibrationDataList calibrationData; // this list holds all calibration data
#endif

View File

@@ -0,0 +1,23 @@
#include "ImageDecoder.h"
#include <mbedtls/base64.h>
// Decoder for Base64 content
bool ImageDecoder::decodeBase64(const char* base64, size_t base64Len, uint8_t* outBuffer, size_t outSize, size_t& decodedSize) {
if (base64 == nullptr) {
decodedSize = 0;
return false;
}
int ret = mbedtls_base64_decode(
outBuffer,
outSize,
&decodedSize,
(const unsigned char*)base64,
base64Len
);
return (ret == 0);
}
// Decoder for Base64 content
bool ImageDecoder::decodeBase64(const String& base64, uint8_t* outBuffer, size_t outSize, size_t& decodedSize) {
return decodeBase64(base64.c_str(), base64.length(), outBuffer, outSize, decodedSize);
}

View File

@@ -0,0 +1,10 @@
#pragma once
#include <Arduino.h>
#include <vector>
class ImageDecoder {
public:
bool decodeBase64(const char* base64, size_t base64Len, uint8_t* outBuffer, size_t outSize, size_t& decodedSize);
bool decodeBase64(const String& base64, uint8_t* outBuffer, size_t outSize, size_t& decodedSize);
};

View File

@@ -47,6 +47,8 @@ static uint8_t mulcolor(uint8_t f1, uint8_t f2){
} }
Color setBrightness(const Color &color,uint8_t brightness){ Color setBrightness(const Color &color,uint8_t brightness){
if (brightness > 100) brightness = 100;
uint16_t br255=brightness*255; uint16_t br255=brightness*255;
br255=br255/100; br255=br255/100;
//very simple for now //very simple for now

View File

@@ -0,0 +1,593 @@
#include "NetworkClient.h"
#include "GWWifi.h" // WiFi management (thread-safe)
extern GwWifi gwWifi; // Extern declaration of global WiFi instance
extern "C" {
#include "puff.h"
}
static uint32_t crc32_update(uint32_t crc, const uint8_t* data, size_t len) {
crc = ~crc;
for (size_t i = 0; i < len; ++i) {
crc ^= data[i];
for (int bit = 0; bit < 8; ++bit) {
uint32_t mask = -(int32_t)(crc & 1U);
crc = (crc >> 1) ^ (0xEDB88320U & mask);
}
}
return ~crc;
}
// Constructor
NetworkClient::NetworkClient(size_t reserveSize)
: _doc(reserveSize),
_valid(false),
_jsonRaw(nullptr),
_jsonRawLen(0),
_imageWidth(0),
_imageHeight(0),
_numberPixels(0),
_pictureBase64(nullptr),
_pictureBase64Len(0)
{
}
NetworkClient::~NetworkClient() {
if (_jsonRaw != nullptr) {
free(_jsonRaw);
_jsonRaw = nullptr;
_jsonRawLen = 0;
}
}
bool NetworkClient::findJsonIntField(const char* json, size_t len, const char* key, int& outValue) {
if (json == nullptr || key == nullptr || len == 0) {
return false;
}
char pattern[64];
int plen = snprintf(pattern, sizeof(pattern), "\"%s\"", key);
if (plen <= 0 || (size_t)plen >= sizeof(pattern)) {
return false;
}
const char* keyPos = strstr(json, pattern);
if (keyPos == nullptr) {
return false;
}
const char* end = json + len;
const char* colon = strchr(keyPos + plen, ':');
if (colon == nullptr || colon >= end) {
return false;
}
const char* p = colon + 1;
while (p < end && (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n')) {
++p;
}
if (p >= end) {
return false;
}
char* parseEnd = nullptr;
long value = strtol(p, &parseEnd, 10);
if (parseEnd == p) {
return false;
}
outValue = (int)value;
return true;
}
bool NetworkClient::extractJsonStringInPlace(char* json, size_t len, const char* key, char*& outValue, size_t& outLen) {
outValue = nullptr;
outLen = 0;
if (json == nullptr || key == nullptr || len == 0) {
return false;
}
char pattern[64];
int plen = snprintf(pattern, sizeof(pattern), "\"%s\"", key);
if (plen <= 0 || (size_t)plen >= sizeof(pattern)) {
return false;
}
char* keyPos = strstr(json, pattern);
if (keyPos == nullptr) {
return false;
}
char* end = json + len;
char* colon = strchr(keyPos + plen, ':');
if (colon == nullptr || colon >= end) {
return false;
}
char* p = colon + 1;
while (p < end && (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n')) {
++p;
}
if (p >= end || *p != '"') {
return false;
}
char* valueStart = p + 1;
char* cur = valueStart;
while (cur < end) {
if (*cur == '\\') {
++cur;
if (cur < end) {
++cur;
}
continue;
}
if (*cur == '"') {
*cur = '\0';
outValue = valueStart;
outLen = (size_t)(cur - valueStart);
return true;
}
++cur;
}
return false;
}
// Skip GZIP Header an goto DEFLATE content
int NetworkClient::skipGzipHeader(const uint8_t* data, size_t len) {
if (len < 10) return -1;
if (data[0] != 0x1F || data[1] != 0x8B || data[2] != 8) {
return -1;
}
size_t pos = 10;
uint8_t flags = data[3];
if (flags & 4) {
if (pos + 2 > len) return -1;
uint16_t xlen = data[pos] | (data[pos+1] << 8);
pos += 2 + xlen;
}
if (flags & 8) {
while (pos < len && data[pos] != 0) pos++;
pos++;
}
if (flags & 16) {
while (pos < len && data[pos] != 0) pos++;
pos++;
}
if (flags & 2) pos += 2;
if (pos >= len) return -1;
return pos;
}
// HTTP GET + GZIP Decompression (reading in chunks)
bool NetworkClient::httpGetGzip(const String& url, uint8_t*& outData, size_t& outLen) {
const size_t capacity = READLIMIT; // Read limit for data (can be adjusted in NetworkClient.h)
uint8_t* buffer = (uint8_t*)malloc(capacity);
// If not with WiFi connectetd then return without any activities
if (!gwWifi.clientConnected()) {
if (DEBUGING) {Serial.println("No WiFi connection");}
return false;
}
// If frame buffer not correct allocated then return without any activities
if (!buffer) {
if (DEBUGING) {Serial.println("Malloc failed buffer");}
return false;
}
HTTPClient http;
// Timeouts to prevent hanging connections
http.setConnectTimeout(CONNECTIONTIMEOUT); // Connect timeout in ms (can be adjusted in NetworkClient.h)
http.setTimeout(TCPREADTIMEOUT); // Read timeout in ms (can be adjusted in NetworkClient.h)
http.begin(url);
// NEW: force server to close the connection after the response (prevents "stuck" keep-alive reads)
http.addHeader("Connection", "close");
// NEW: request gzip, but we will only decompress if the server actually answers with gzip
http.addHeader("Accept-Encoding", "gzip");
// NEW: register headers BEFORE GET() (more reliable with Arduino HTTPClient)
if (DEBUGING) {
// We need follow key words
const char* keys[] = {
"Content-Encoding",
"Transfer-Encoding",
"Content-Length"
};
// Read header
http.collectHeaders(keys, 3);
}
int code = http.GET();
if (code != HTTP_CODE_OK) {
Serial.printf("HTTP Client ERROR: %d (%s)\n", code, http.errorToString(code).c_str());
// Hard reset HTTP + socket
WiFiClient* tmp = http.getStreamPtr();
if (tmp) tmp->stop(); // Force close TCP socket
http.end();
free(buffer);
return false;
}
else{
if (DEBUGING) {
String ce = http.header("Content-Encoding");
String te = http.header("Transfer-Encoding");
String cl = http.header("Content-Length");
// Print header informations
Serial.printf("Content-Encoding=%s Transfer-Encoding=%s Content-Length=%s\n",
ce.c_str(),
te.c_str(),
cl.c_str());
}
}
WiFiClient* stream = http.getStreamPtr();
size_t len = 0;
uint32_t lastData = millis();
const uint32_t READ_TIMEOUT = READDATATIMEOUT; // Timeout for reading data (can be adjusted in NetworkClient.h)
bool complete = false;
bool aborting = false; // NEW: remember if we must force-close socket
// NEW: detect if server really sent gzip
String ce = http.header("Content-Encoding");
bool isGzip = ce.equalsIgnoreCase("gzip");
// NEW: read expected body size if provided by server (prevents waiting forever for missing bytes)
int total = http.getSize(); // returns Content-Length, or -1 if unknown/chunked
// NEW: fail fast if server claims something larger than our buffer
if (total > 0 && (size_t)total > capacity) {
Serial.println("Response exceeds READLIMIT.");
aborting = true;
}
// NEW: if not gzip, we will not try to decompress (prevents false "Decompress OK" / random success)
// You can either handle plain JSON here or just fail-fast.
if (!isGzip && !aborting) {
if (DEBUGING) {
Serial.println("Server response is NOT gzip (Content-Encoding != gzip).");
Serial.println("Either disable Accept-Encoding: gzip or add plain-body handling here.");
}
// --- Plain-body handling (recommended): read full body into outData as-is ---
// NEW: try to read Content-Length bytes if available (more robust)
if (total > 0 && (size_t)total > capacity) {
Serial.println("Plain response exceeds READLIMIT.");
aborting = true;
} else {
// Read until we have all bytes (Content-Length) or until connection closes + buffer drains
while ((http.connected() || (stream && stream->available())) && !aborting) {
size_t avail = stream ? stream->available() : 0;
if (avail == 0) {
if (millis() - lastData > READ_TIMEOUT) {
Serial.println("TIMEOUT waiting for data (plain)!");
aborting = true;
break;
}
delay(1);
continue;
}
if (len >= capacity) {
Serial.println("READLIMIT reached, aborting (plain).");
aborting = true;
break;
}
if (len + avail > capacity)
avail = capacity - len;
int read = stream->readBytes(buffer + len, avail);
if (read > 0) {
len += (size_t)read;
lastData = millis();
}
// NEW: stop reading as soon as we have the full response
if (total > 0 && (int)len >= total) {
break; // we got full body
}
}
}
if (aborting) {
// --- Added: Force-close connection only if aborted to avoid TCP RST storms ---
if (stream) stream->stop(); // Force close TCP socket
http.end();
free(buffer);
return false;
}
if (total > 0 && (int)len != total) {
Serial.printf("Plain response incomplete: got=%d expected=%d\n", (int)len, total);
if (stream) stream->stop();
http.end();
free(buffer);
return false;
}
// Return plain body to caller
outData = (uint8_t*)malloc(len + 1);
if (!outData) {
Serial.println("Malloc failed outData (plain).");
// --- Added: Force-close connection only if aborted to avoid TCP RST storms ---
if (stream) stream->stop(); // Force close TCP socket
http.end();
free(buffer);
return false;
}
memcpy(outData, buffer, len);
outData[len] = 0;
outLen = len;
http.end();
free(buffer);
return true;
}
// --- GZIP path (only if Content-Encoding is gzip) ---
if (!aborting) {
// NEW: read exactly Content-Length bytes when available (prevents partial-body timeout loops)
while ((http.connected() || (stream && stream->available())) && !complete && !aborting) {
size_t avail = stream ? stream->available() : 0;
if (avail == 0) {
// NEW: if Content-Length is known and we already read it all, stop immediately
if (total > 0 && (int)len >= total) {
break;
}
if (millis() - lastData > READ_TIMEOUT) {
Serial.println("TIMEOUT waiting for data!");
aborting = true; // NEW: mark abnormal exit
break;
}
delay(1);
continue;
}
// NEW: safety check if buffer limit is reached
if (len >= capacity) {
Serial.println("READLIMIT reached, aborting.");
aborting = true;
break;
}
// NEW: if Content-Length is known, do not read beyond it
if (total > 0) {
size_t remaining = (size_t)total - len;
if (avail > remaining) avail = remaining;
}
if (len + avail > capacity)
avail = capacity - len;
int read = stream->readBytes(buffer + len, avail);
if (read <= 0) {
// NEW: avoid tight loop if read returns zero
delay(1);
continue;
}
len += (size_t)read;
lastData = millis();
if (DEBUGING) {Serial.printf("Read chunk: %d (total: %d)\n", read, (int)len);}
// NEW: if Content-Length is known and fully received, we can stop reading
if (total > 0 && (int)len >= total) {
break;
}
}
// NEW: only attempt gzip parse/decompress after we have a complete body (when Content-Length is known)
// This avoids wasting heap with repeated malloc/free and reduces fragmentation over long runtimes.
if (!aborting) {
if (total > 0 && (int)len != total) {
Serial.printf("GZIP response incomplete: got=%d expected=%d\n", (int)len, total);
aborting = true;
}
}
if (!aborting) {
if (len < 20) {
aborting = true;
} else {
int headerOffset = skipGzipHeader(buffer, len);
if (headerOffset < 0) {
aborting = true;
} else {
size_t deflateLen = len - (size_t)headerOffset;
// GZIP trailer (CRC32 + ISIZE) is 8 bytes and not part of deflate stream.
if (deflateLen >= 8) {
deflateLen -= 8;
}
unsigned long srcLenForSize = (unsigned long)deflateLen;
unsigned long outNeeded = 0;
int sizeRes = puff(NIL, &outNeeded, buffer + headerOffset, &srcLenForSize);
if (sizeRes != 0) {
if (DEBUGING) {
Serial.printf("Decompress size probe failed: res=%d src=%lu\n", sizeRes, srcLenForSize);
}
aborting = true;
} else {
uint8_t* test = (uint8_t*)malloc((size_t)outNeeded + 1);
if (!test) {
Serial.println("Malloc failed test buffer, aborting.");
aborting = true;
} else {
unsigned long srcLen = (unsigned long)deflateLen;
unsigned long testLen = outNeeded;
int res = puff(test, &testLen, buffer + headerOffset, &srcLen);
if (res == 0) {
uint32_t trailerCrc =
(uint32_t)buffer[len - 8] |
((uint32_t)buffer[len - 7] << 8) |
((uint32_t)buffer[len - 6] << 16) |
((uint32_t)buffer[len - 5] << 24);
uint32_t trailerIsize =
(uint32_t)buffer[len - 4] |
((uint32_t)buffer[len - 3] << 8) |
((uint32_t)buffer[len - 2] << 16) |
((uint32_t)buffer[len - 1] << 24);
uint32_t calcCrc = crc32_update(0, test, (size_t)testLen);
uint32_t calcIsize = (uint32_t)testLen;
if (calcCrc != trailerCrc || calcIsize != trailerIsize) {
Serial.printf(
"GZIP CRC/ISIZE mismatch crc=%08lx/%08lx isize=%lu/%lu\n",
(unsigned long)calcCrc,
(unsigned long)trailerCrc,
(unsigned long)calcIsize,
(unsigned long)trailerIsize
);
free(test);
aborting = true;
} else {
test[testLen] = 0;
if (DEBUGING) {Serial.printf("Decompress OK! Size: %lu bytes\n", testLen);}
outData = test;
outLen = (size_t)testLen;
complete = true;
}
} else {
if (DEBUGING) {
Serial.printf("Decompress failed: res=%d out=%lu src=%lu\n", res, testLen, srcLen);
}
free(test);
aborting = true;
}
}
}
}
}
}
}
// --- Added: Force-close connection only if aborted to avoid TCP RST storms ---
if (aborting && stream) stream->stop(); // NEW: stop() only on abnormal termination
http.end();
free(buffer);
if (!complete) {
Serial.println("Failed to complete decompress.");
return false;
}
return true;
}
// Decompress JSON
bool NetworkClient::fetchAndDecompressJson(const String& url) {
_valid = false;
_doc.clear();
_imageWidth = 0;
_imageHeight = 0;
_numberPixels = 0;
_pictureBase64 = nullptr;
_pictureBase64Len = 0;
if (_jsonRaw != nullptr) {
free(_jsonRaw);
_jsonRaw = nullptr;
_jsonRawLen = 0;
}
uint8_t* raw = nullptr;
size_t rawLen = 0;
if (!httpGetGzip(url, raw, rawLen)) {
Serial.println("GZIP download/decompress failed.");
return false;
}
char* json = reinterpret_cast<char*>(raw);
bool ok = true;
ok = findJsonIntField(json, rawLen, "number_pixels", _numberPixels) && ok;
ok = findJsonIntField(json, rawLen, "width", _imageWidth) && ok;
ok = findJsonIntField(json, rawLen, "height", _imageHeight) && ok;
ok = extractJsonStringInPlace(json, rawLen, "picture_base64", _pictureBase64, _pictureBase64Len) && ok;
if (!ok) {
Serial.println("JSON field extraction failed.");
free(raw);
return false;
}
if (_imageWidth <= 0 || _imageHeight <= 0 || _pictureBase64Len == 0) {
Serial.printf("JSON invalid geometry/data w=%d h=%d b64=%u\n",
_imageWidth,
_imageHeight,
(unsigned int)_pictureBase64Len);
free(raw);
return false;
}
_jsonRaw = raw;
_jsonRawLen = rawLen;
if (DEBUGING) {
Serial.printf("JSON fields OK: num=%d w=%d h=%d b64=%u\n",
_numberPixels,
_imageWidth,
_imageHeight,
(unsigned int)_pictureBase64Len);
}
_valid = true;
return true;
}
JsonDocument& NetworkClient::json() {
return _doc;
}
int NetworkClient::imageWidth() const {
return _imageWidth;
}
int NetworkClient::imageHeight() const {
return _imageHeight;
}
int NetworkClient::numberPixels() const {
return _numberPixels;
}
const char* NetworkClient::pictureBase64() const {
return _pictureBase64;
}
size_t NetworkClient::pictureBase64Len() const {
return _pictureBase64Len;
}
bool NetworkClient::isValid() const {
return _valid;
}

View File

@@ -0,0 +1,42 @@
#pragma once
#include <ArduinoJson.h>
#include <WiFi.h>
#include <HTTPClient.h>
#define DEBUGING true // Debug flag for NetworkClient for more live information
#define READLIMIT 200000 // HTTP read limit in byte for gzip content (can be adjusted)
#define CONNECTIONTIMEOUT 3000 // Timeout in ms for HTTP connection
#define TCPREADTIMEOUT 2000 // Timeout in ms for read HTTP client stack
#define READDATATIMEOUT 2000 // Timeout in ms for read data
class NetworkClient {
public:
NetworkClient(size_t reserveSize = 0);
~NetworkClient();
bool fetchAndDecompressJson(const String& url);
JsonDocument& json();
int imageWidth() const;
int imageHeight() const;
int numberPixels() const;
const char* pictureBase64() const;
size_t pictureBase64Len() const;
bool isValid() const;
private:
DynamicJsonDocument _doc;
bool _valid;
uint8_t* _jsonRaw;
size_t _jsonRawLen;
int _imageWidth;
int _imageHeight;
int _numberPixels;
char* _pictureBase64;
size_t _pictureBase64Len;
int skipGzipHeader(const uint8_t* data, size_t len);
bool httpGetGzip(const String& url, uint8_t*& outData, size_t& outLen);
static bool findJsonIntField(const char* json, size_t len, const char* key, int& outValue);
static bool extractJsonStringInPlace(char* json, size_t len, const char* key, char*& outValue, size_t& outLen);
};

View File

@@ -2,9 +2,9 @@
#if defined BOARD_OBP60S3 || defined BOARD_OBP40S3 #if defined BOARD_OBP60S3 || defined BOARD_OBP40S3
#include <Arduino.h> #include <Arduino.h>
#include <PCF8574.h> // Driver for PCF8574 output modul from Horter
#include <Wire.h> // I2C #include <Wire.h> // I2C
#include <RTClib.h> // Driver for DS1388 RTC #include <RTClib.h> // Driver for DS1388 RTC
#include <PCF8574.h> // PCF8574 modules from Horter
#include "SunRise.h" // Lib for sunrise and sunset calculation #include "SunRise.h" // Lib for sunrise and sunset calculation
#include "Pagedata.h" #include "Pagedata.h"
#include "OBP60Hardware.h" #include "OBP60Hardware.h"
@@ -26,6 +26,7 @@
#include "fonts/Ubuntu_Bold32pt8b.h" #include "fonts/Ubuntu_Bold32pt8b.h"
#include "fonts/Atari16px8b.h" // Key label font #include "fonts/Atari16px8b.h" // Key label font
#include "fonts/Atari6px8b.h" // Very small (6x6) font #include "fonts/Atari6px8b.h" // Very small (6x6) font
#include "fonts/IBM8x8px.h"
// E-Ink Display // E-Ink Display
#define GxEPD_WIDTH 400 // Display width #define GxEPD_WIDTH 400 // Display width
@@ -50,7 +51,7 @@ GxEPD2_BW<GxEPD2_420_SE0420NQ04, GxEPD2_420_SE0420NQ04::HEIGHT> display(GxEPD2_4
gxepd2display *epd = &display; gxepd2display *epd = &display;
// Horter I2C moduls // Horter I2C moduls
PCF8574 pcf8574_Out(PCF8574_I2C_ADDR1); // First digital output modul PCF8574 from Horter PCF8574 pcf8574_Modul1(PCF8574_I2C_ADDR1); // First digital IO modul PCF8574 from Horter
// FRAM // FRAM
Adafruit_FRAM_I2C fram; Adafruit_FRAM_I2C fram;
@@ -82,10 +83,11 @@ void hardwareInit(GwApi *api)
Wire.begin(); Wire.begin();
// Init PCF8574 digital outputs // Init PCF8574 digital outputs
Wire.setClock(I2C_SPEED); // Set I2C clock as defined Wire.setClock(I2C_SPEED_LOW); // Set I2C clock to low for external devices
if(pcf8574_Out.begin()){ // Initialize PCF8574 if (pcf8574_Modul1.begin()) { // Initialize PCF8574
pcf8574_Out.write8(255); // Clear all outputs pcf8574_Modul1.write8(255); // Clear all outputs (low activ)
} }
Wire.setClock(I2C_SPEED); // Set I2C clock to normal
fram = Adafruit_FRAM_I2C(); fram = Adafruit_FRAM_I2C();
if (esp_reset_reason() == ESP_RST_POWERON) { if (esp_reset_reason() == ESP_RST_POWERON) {
// help initialize FRAM // help initialize FRAM
@@ -186,6 +188,28 @@ void powerInit(String powermode) {
} }
} }
void setPCF8574PortPinModul1(uint8_t pin, uint8_t value)
{
static bool firstRunFinished;
static uint8_t port1; // Retained data for port bits
// If fisrt run then set all outputs to low
if(firstRunFinished == false){
port1 = 255; // Low active
firstRunFinished = true;
}
if (pin > 7) return;
Wire.setClock(I2C_SPEED_LOW); // Set I2C clock on 10 kHz for longer wires
// Set bit
if (pcf8574_Modul1.begin(port1)) // Check module availability and start it
{
if (value == LOW) port1 &= ~(1 << pin); // Set bit
else port1 |= (1 << pin);
pcf8574_Modul1.write8(port1); // Write byte
}
Wire.setClock(I2C_SPEED); // Set I2C clock on 100 kHz
}
void setPortPin(uint pin, bool value){ void setPortPin(uint pin, bool value){
pinMode(pin, OUTPUT); pinMode(pin, OUTPUT);
digitalWrite(pin, value); digitalWrite(pin, value);
@@ -302,8 +326,46 @@ void toggleBacklightLED(uint brightness, const Color &color) {
ledTaskData->setLedData(current); ledTaskData->setLedData(current);
} }
void stepsBacklightLED(uint brightness, const Color &color) {
static uint step = 0;
uint actBrightness = 0;
// Different brightness steps
if (step == 0){
actBrightness = brightness; // 100% from brightness
statusBacklightLED = true;
}
else if (step == 1) {
actBrightness = brightness * 0.5; // 50% from brighntess
statusBacklightLED = true;
}
else if (step == 2) {
actBrightness = brightness * 0.2; // 20% from brightness
statusBacklightLED = true;
}
else if (step == 3) {
actBrightness = 0; // 0%
statusBacklightLED = false;
}
if (actBrightness < 5) { // Limiter if values too low
actBrightness = 5;
}
step = step + 1; // Increment step counter
if (step == 4) { // Reset counter
step = 0;
}
if (ledTaskData == nullptr) {
return;
}
Color nv = setBrightness(statusBacklightLED ? color:COLOR_BLACK, actBrightness);
LedInterface current = ledTaskData->getLedData();
current.setBacklight(nv);
ledTaskData->setLedData(current);
}
void setFlashLED(bool status) { void setFlashLED(bool status) {
if (ledTaskData == nullptr) return; if (ledTaskData == nullptr) {
return;
}
Color c = status ? COLOR_RED : COLOR_BLACK; Color c = status ? COLOR_RED : COLOR_BLACK;
LedInterface current = ledTaskData->getLedData(); LedInterface current = ledTaskData->getLedData();
current.setFlash(c); current.setFlash(c);
@@ -424,6 +486,27 @@ void drawTextCenter(int16_t cx, int16_t cy, String text) {
epd->print(text); epd->print(text);
} }
// Draw centered botton with centered text
void drawButtonCenter(int16_t cx, int16_t cy, int8_t sx, int8_t sy, String text, uint16_t fg, uint16_t bg, bool inverted) {
int16_t x1, y1;
uint16_t w, h;
epd->getTextBounds(text, 0, 150, &x1, &y1, &w, &h);
int16_t cursorX = cx - (x1 + static_cast<int16_t>(w / 2));
int16_t cursorY = cy - (y1 + static_cast<int16_t>(h / 2));
//epd->drawPixel(cx, cy, fg); // Debug pixel for center position
if (inverted) {
epd->fillRoundRect(cx - sx / 2, cy - sy / 2, sx, sy, 5, fg);
epd->setTextColor(bg);
epd->setCursor(cursorX, cursorY);
epd->print(text);
} else {
epd->drawRoundRect(cx - sx / 2, cy - sy / 2, sx, sy, 5, fg); // Draw button
epd->setTextColor(fg);
epd->setCursor(cursorX, cursorY);
epd->print(text);
}
}
// Draw right aligned text // Draw right aligned text
void drawTextRalign(int16_t x, int16_t y, String text) { void drawTextRalign(int16_t x, int16_t y, String text) {
int16_t x1, y1; int16_t x1, y1;
@@ -892,6 +975,79 @@ void generatorGraphic(uint x, uint y, int pcolor, int bcolor){
epd->print("G"); epd->print("G");
} }
// Display rudder position as horizontal bargraph with configurable +/- range (degrees)
void displayRudderPosition(int rudderPosition, uint8_t rangeDeg, uint16_t cx, uint16_t cy, uint16_t fg, uint16_t bg) {
const int w = 360;
const int h = 20;
const int t = 3; // Line thickness
const int halfw = w/2;
const int halfh = h/2;
// Calculate top-left of bar (cx,cy are center of 0°)
int left = int(cx) - halfw;
int top = int(cy) - halfh;
// clamp provided range to allowed bounds [10,45]
if (rangeDeg < 10) {
rangeDeg = 10;
} else if (rangeDeg > 45) {
rangeDeg = 45;
}
// Pixels per degree for +/-rangeDeg -> total span = 2*rangeDeg
const float pxPerDeg = float(w) / (2.0f * float(rangeDeg));
// Draw outer border (thickness t)
for (int i = 0; i < t; i++) {
epd->drawRect(left + i, top + i, w - 2 * i, h - 2 * i, fg);
}
// Fill inner area with background
epd->fillRect(left + t, top + t, w - 2 * t, h - 2 * t, bg);
// Draw center line
epd->drawRect(cx - 1, top + 1, 3 , h - 2, fg);
// Clamp rudder position to -rangeDeg..rangeDeg
if (rudderPosition > (int)rangeDeg) {
rudderPosition = (int)rangeDeg;
} else if (rudderPosition < -((int)rangeDeg)) {
rudderPosition = -((int)rangeDeg);
}
// Compute fill width in pixels
int fillPx = int(round(rudderPosition * pxPerDeg)); // positive -> right
// Fill area from center to position (if non-zero)
int centerx = cx;
int innerTop = top + t;
int innerH = h - 2 * t;
if (fillPx > 0) {
// Right side
epd->fillRect(centerx, innerTop, fillPx, innerH, fg);
} else if (fillPx < 0) {
// Left side
epd->fillRect(centerx + fillPx, innerTop, -fillPx, innerH, fg);
}
// Draw tick marks every 5° and labels outside the bar
epd->setTextColor(fg);
epd->setFont(&Ubuntu_Bold8pt8b);
for (int angle = -((int)rangeDeg); angle <= (int)rangeDeg; angle += 5) {
int xpos = int(round(centerx + angle * pxPerDeg));
// Vertical tick inside bar
epd->drawLine(xpos, top, xpos, top + h + 2, fg);
// Label outside: below the bar
String lbl = String(angle);
int16_t bx, by;
uint16_t bw, bh;
epd->getTextBounds(lbl, 0, 0, &bx, &by, &bw, &bh);
int16_t tx = xpos - bw/2;
int16_t ty = top + h + bh + 5; // A little spacing
epd->setCursor(tx, ty);
epd->print(lbl);
}
}
// Function to handle HTTP image request // Function to handle HTTP image request
// http://192.168.15.1/api/user/OBP60Task/screenshot // http://192.168.15.1/api/user/OBP60Task/screenshot
void doImageRequest(GwApi *api, int *pageno, const PageStruct pages[MAX_PAGE_NUMBER], AsyncWebServerRequest *request) { void doImageRequest(GwApi *api, int *pageno, const PageStruct pages[MAX_PAGE_NUMBER], AsyncWebServerRequest *request) {

View File

@@ -87,13 +87,14 @@ uint8_t getLastPage();
void hardwareInit(GwApi *api); void hardwareInit(GwApi *api);
void powerInit(String powermode); void powerInit(String powermode);
void setPCF8574PortPinModul1(uint8_t pin, uint8_t value);// Set PCF8574 port pin
void setPortPin(uint pin, bool value); // Set port pin for extension port void setPortPin(uint pin, bool value); // Set port pin for extension port
void togglePortPin(uint pin); // Toggle extension port pin void togglePortPin(uint pin); // Toggle extension port pin
Color colorMapping(const String &colorString); // Color mapping string to CHSV colors Color colorMapping(const String &colorString); // Color mapping string to CHSV colors
void setBacklightLED(uint brightness, const Color &color);// Set backlight LEDs void setBacklightLED(uint brightness, const Color &color);// Set backlight LEDs
void toggleBacklightLED(uint brightness,const Color &color);// Toggle backlight LEDs void toggleBacklightLED(uint brightness,const Color &color);// Toggle backlight LEDs
void stepsBacklightLED(uint brightness, const Color &color);// Set backlight LEDs in 4 steps (100%, 50%, 10%, 0%)
BacklightMode backlightMapping(const String &backlightString);// Configuration string to value BacklightMode backlightMapping(const String &backlightString);// Configuration string to value
void setFlashLED(bool status); // Set flash LED void setFlashLED(bool status); // Set flash LED
@@ -106,6 +107,7 @@ void setBuzzerPower(uint power); // Set buzzer power
String xdrDelete(String input, uint8_t maxlen = 0); // Delete xdr prefix from string and optional limit length String xdrDelete(String input, uint8_t maxlen = 0); // Delete xdr prefix from string and optional limit length
void drawTextCenter(int16_t cx, int16_t cy, String text); void drawTextCenter(int16_t cx, int16_t cy, String text);
void drawButtonCenter(int16_t cx, int16_t cy, int8_t sx, int8_t sy, String text, uint16_t fg, uint16_t bg, bool inverted);
void drawTextRalign(int16_t x, int16_t y, String text); void drawTextRalign(int16_t x, int16_t y, String text);
void drawTextBoxed(Rect box, String text, uint16_t fg, uint16_t bg, bool inverted, bool border); void drawTextBoxed(Rect box, String text, uint16_t fg, uint16_t bg, bool inverted, bool border);
@@ -124,6 +126,10 @@ void solarGraphic(uint x, uint y, int pcolor, int bcolor); // S
void generatorGraphic(uint x, uint y, int pcolor, int bcolor); // Generator graphic void generatorGraphic(uint x, uint y, int pcolor, int bcolor); // Generator graphic
void startLedTask(GwApi *api); void startLedTask(GwApi *api);
// Display rudder position as horizontal bargraph with configurable +/- range (degrees)
// 'rangeDeg' is unsigned and will be clamped to [10,45]
void displayRudderPosition(int rudderPosition, uint8_t rangeDeg, uint16_t cx, uint16_t cy, uint16_t fg, uint16_t bg);
void doImageRequest(GwApi *api, int *pageno, const PageStruct pages[MAX_PAGE_NUMBER], AsyncWebServerRequest *request); void doImageRequest(GwApi *api, int *pageno, const PageStruct pages[MAX_PAGE_NUMBER], AsyncWebServerRequest *request);
// Icons // Icons

View File

@@ -67,14 +67,23 @@ fmtTime Formatter::getTimeFormat(String sformat) {
return fmtTime::MMHH; // default return fmtTime::MMHH; // default
} }
FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &commondata){ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &commondata, bool ignoreSimuDataSetting) {
GwLog *logger = commondata.logger; GwLog *logger = commondata.logger;
FormattedData result; FormattedData result;
static int dayoffset = 0; static int dayoffset = 0;
double rawvalue = 0; double rawvalue = 0;
bool simulation;
if (ignoreSimuDataSetting) {
simulation = false; // ignore user setting for simulation data; we want to format the boat value passed to this function
} else {
simulation = usesimudata; // use setting from configuration
}
result.cvalue = value->value;
// If boat value not valid // If boat value not valid
if (! value->valid && !usesimudata){ if (! value->valid && !simulation){
result.svalue = placeholder; result.svalue = placeholder;
return result; return result;
} }
@@ -98,7 +107,7 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
tmElements_t parts; tmElements_t parts;
time_t tv=tNMEA0183Msg::daysToTime_t(value->value + dayoffset); time_t tv=tNMEA0183Msg::daysToTime_t(value->value + dayoffset);
tNMEA0183Msg::breakTime(tv,parts); tNMEA0183Msg::breakTime(tv,parts);
if (usesimudata == false) { if (simulation == false) {
if (String(dateFormat) == "DE") { if (String(dateFormat) == "DE") {
snprintf(buffer,bsize, "%02d.%02d.%04d", parts.tm_mday, parts.tm_mon+1, parts.tm_year+1900); snprintf(buffer,bsize, "%02d.%02d.%04d", parts.tm_mday, parts.tm_mon+1, parts.tm_year+1900);
} }
@@ -132,11 +141,12 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
if (timeInSeconds > 86400) {timeInSeconds = timeInSeconds - 86400;} if (timeInSeconds > 86400) {timeInSeconds = timeInSeconds - 86400;}
if (timeInSeconds < 0) {timeInSeconds = timeInSeconds + 86400;} if (timeInSeconds < 0) {timeInSeconds = timeInSeconds + 86400;}
// LOG_DEBUG(GwLog::DEBUG,"... formatTime value: %f tz: %f corrected timeInSeconds: %f ", value->value, timeZone, timeInSeconds); // LOG_DEBUG(GwLog::DEBUG,"... formatTime value: %f tz: %f corrected timeInSeconds: %f ", value->value, timeZone, timeInSeconds);
if (usesimudata == false) { if (simulation == false) {
val = modf(timeInSeconds/3600.0, &inthr); val = modf(timeInSeconds/3600.0, &inthr);
val = modf(val*3600.0/60.0, &intmin); val = modf(val*3600.0/60.0, &intmin);
modf(val*60.0,&intsec); modf(val*60.0,&intsec);
snprintf(buffer, bsize, "%02.0f:%02.0f:%02.0f", inthr, intmin, intsec); snprintf(buffer, bsize, "%02.0f:%02.0f:%02.0f", inthr, intmin, intsec);
result.cvalue = timeInSeconds;
} }
else{ else{
static long sec; static long sec;
@@ -146,13 +156,14 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
} }
sec = sec % 60; sec = sec % 60;
snprintf(buffer, bsize, "11:36:%02i", int(sec)); snprintf(buffer, bsize, "11:36:%02i", int(sec));
result.cvalue = sec;
lasttime = millis(); lasttime = millis();
} }
result.unit = ((timeZone == 0) ? "UTC" : "LOT"); result.unit = ((timeZone == 0) ? "UTC" : "LOT");
} }
//######################################################## //########################################################
else if (value->getFormat() == "formatFixed0"){ else if (value->getFormat() == "formatFixed0"){
if(usesimudata == false) { if(simulation == false) {
snprintf(buffer, bsize, "%3.0f", value->value); snprintf(buffer, bsize, "%3.0f", value->value);
rawvalue = value->value; rawvalue = value->value;
} }
@@ -161,16 +172,17 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
snprintf(buffer, bsize, "%3.0f", rawvalue); snprintf(buffer, bsize, "%3.0f", rawvalue);
} }
result.unit = ""; result.unit = "";
result.cvalue = rawvalue;
} }
//######################################################## //########################################################
else if (value->getFormat() == "formatCourse" || value->getFormat() == "formatWind"){ else if (value->getFormat() == "formatCourse" || value->getFormat() == "formatWind"){
double course = 0; double course = 0;
if (usesimudata == false) { if (simulation == false) {
course = value->value; course = value->value;
rawvalue = value->value; rawvalue = value->value;
} }
else { else {
course = 2.53 + float(random(0, 10) / 100.0); course = M_PI_2 + float(random(-17, 17) / 100.0); // create random course/wind values with 90° +/- 10°
rawvalue = course; rawvalue = course;
} }
course = course * 57.2958; // Unit conversion form rad to deg course = course * 57.2958; // Unit conversion form rad to deg
@@ -178,16 +190,17 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
// Format 3 numbers with prefix zero // Format 3 numbers with prefix zero
snprintf(buffer,bsize,"%03.0f",course); snprintf(buffer,bsize,"%03.0f",course);
result.unit = "Deg"; result.unit = "Deg";
result.cvalue = course;
} }
//######################################################## //########################################################
else if (value->getFormat() == "formatKnots" && (value->getName() == "SOG" || value->getName() == "STW")){ else if (value->getFormat() == "formatKnots" && (value->getName() == "SOG" || value->getName() == "STW")){
double speed = 0; double speed = 0;
if (usesimudata == false) { if (simulation == false) {
speed = value->value; speed = value->value;
rawvalue = value->value; rawvalue = value->value;
} }
else{ else{
rawvalue = 4.0 + float(random(0, 40)); rawvalue = 4.0 + float(random(-30, 40) / 10.0); // create random speed values from [1..8] m/s
speed = rawvalue; speed = rawvalue;
} }
if (String(speedFormat) == "km/h"){ if (String(speedFormat) == "km/h"){
@@ -211,16 +224,17 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
else { else {
snprintf(buffer, bsize, fmt_dec_100, speed); snprintf(buffer, bsize, fmt_dec_100, speed);
} }
result.cvalue = speed;
} }
//######################################################## //########################################################
else if (value->getFormat() == "formatKnots" && (value->getName() == "AWS" || value->getName() == "TWS" || value->getName() == "MaxAws" || value->getName() == "MaxTws")){ else if (value->getFormat() == "formatKnots" && (value->getName() == "AWS" || value->getName() == "TWS" || value->getName() == "MaxAws" || value->getName() == "MaxTws")){
double speed = 0; double speed = 0;
if (usesimudata == false) { if (simulation == false) {
speed = value->value; speed = value->value;
rawvalue = value->value; rawvalue = value->value;
} }
else { else {
rawvalue = 4.0 + float(random(0, 40)); rawvalue = 4.0 + float(random(0, 40) / 10.0); // create random wind speed values from [4..8] m/s
speed = rawvalue; speed = rawvalue;
} }
if (String(windspeedFormat) == "km/h"){ if (String(windspeedFormat) == "km/h"){
@@ -291,11 +305,12 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
snprintf(buffer, bsize, fmt_dec_100, speed); snprintf(buffer, bsize, fmt_dec_100, speed);
} }
} }
result.cvalue = speed;
} }
//######################################################## //########################################################
else if (value->getFormat() == "formatRot"){ else if (value->getFormat() == "formatRot"){
double rotation = 0; double rotation = 0;
if (usesimudata == false) { if (simulation == false) {
rotation = value->value; rotation = value->value;
rawvalue = value->value; rawvalue = value->value;
} }
@@ -317,11 +332,12 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
else { else {
snprintf(buffer, bsize, "%3.0f", rotation); snprintf(buffer, bsize, "%3.0f", rotation);
} }
result.cvalue = rotation;
} }
//######################################################## //########################################################
else if (value->getFormat() == "formatDop"){ else if (value->getFormat() == "formatDop"){
double dop = 0; double dop = 0;
if (usesimudata == false) { if (simulation == false) {
dop = value->value; dop = value->value;
rawvalue = value->value; rawvalue = value->value;
} }
@@ -342,10 +358,11 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
else { else {
snprintf(buffer, bsize, fmt_dec_100, dop); snprintf(buffer, bsize, fmt_dec_100, dop);
} }
result.cvalue = dop;
} }
//######################################################## //########################################################
else if (value->getFormat() == "formatLatitude"){ else if (value->getFormat() == "formatLatitude"){
if (usesimudata == false) { if (simulation == false) {
double lat = value->value; double lat = value->value;
rawvalue = value->value; rawvalue = value->value;
String latitude = ""; String latitude = "";
@@ -361,10 +378,11 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
rawvalue = 35.0 + float(random(0, 10)) / 10000.0; rawvalue = 35.0 + float(random(0, 10)) / 10000.0;
snprintf(buffer, bsize, " 51\" %2.4f' N", rawvalue); snprintf(buffer, bsize, " 51\" %2.4f' N", rawvalue);
} }
result.cvalue = rawvalue;
} }
//######################################################## //########################################################
else if (value->getFormat() == "formatLongitude"){ else if (value->getFormat() == "formatLongitude"){
if (usesimudata == false) { if (simulation == false) {
double lon = value->value; double lon = value->value;
rawvalue = value->value; rawvalue = value->value;
String longitude = ""; String longitude = "";
@@ -380,16 +398,17 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
rawvalue = 6.0 + float(random(0, 10)) / 100000.0; rawvalue = 6.0 + float(random(0, 10)) / 100000.0;
snprintf(buffer, bsize, " 15\" %2.4f'", rawvalue); snprintf(buffer, bsize, " 15\" %2.4f'", rawvalue);
} }
result.cvalue = rawvalue;
} }
//######################################################## //########################################################
else if (value->getFormat() == "formatDepth"){ else if (value->getFormat() == "formatDepth"){
double depth = 0; double depth = 0;
if (usesimudata == false) { if (simulation == false) {
depth = value->value; depth = value->value;
rawvalue = value->value; rawvalue = value->value;
} }
else { else {
rawvalue = 18.0 + float(random(0, 100)) / 10.0; rawvalue = 18.0 + float(random(0, 100)) / 10.0; // create random depth values from [18..28] metres
depth = rawvalue; depth = rawvalue;
} }
if(String(lengthFormat) == "ft"){ if(String(lengthFormat) == "ft"){
@@ -408,11 +427,12 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
else { else {
snprintf(buffer, bsize, fmt_dec_100, depth); snprintf(buffer, bsize, fmt_dec_100, depth);
} }
result.cvalue = depth;
} }
//######################################################## //########################################################
else if (value->getFormat() == "formatXte"){ else if (value->getFormat() == "formatXte"){
double xte = 0; double xte = 0;
if (usesimudata == false) { if (simulation == false) {
xte = value->value; xte = value->value;
rawvalue = value->value; rawvalue = value->value;
} else { } else {
@@ -436,11 +456,12 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
else { else {
snprintf(buffer, bsize, "%3.0f", xte); snprintf(buffer, bsize, "%3.0f", xte);
} }
result.cvalue = xte;
} }
//######################################################## //########################################################
else if (value->getFormat() == "kelvinToC"){ else if (value->getFormat() == "kelvinToC"){
double temp = 0; double temp = 0;
if (usesimudata == false) { if (simulation == false) {
temp = value->value; temp = value->value;
rawvalue = value->value; rawvalue = value->value;
} }
@@ -468,11 +489,12 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
else { else {
snprintf(buffer, bsize, fmt_dec_100, temp); snprintf(buffer, bsize, fmt_dec_100, temp);
} }
result.cvalue = temp;
} }
//######################################################## //########################################################
else if (value->getFormat() == "mtr2nm"){ else if (value->getFormat() == "mtr2nm"){
double distance = 0; double distance = 0;
if (usesimudata == false) { if (simulation == false) {
distance = value->value; distance = value->value;
rawvalue = value->value; rawvalue = value->value;
} }
@@ -500,6 +522,7 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
else { else {
snprintf(buffer, bsize, fmt_dec_100, distance); snprintf(buffer, bsize, fmt_dec_100, distance);
} }
result.cvalue = distance;
} }
//######################################################## //########################################################
// Special XDR formats // Special XDR formats
@@ -507,7 +530,7 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
//######################################################## //########################################################
else if (value->getFormat() == "formatXdr:P:P"){ else if (value->getFormat() == "formatXdr:P:P"){
double pressure = 0; double pressure = 0;
if (usesimudata == false) { if (simulation == false) {
pressure = value->value; pressure = value->value;
rawvalue = value->value; rawvalue = value->value;
pressure = pressure / 100.0; // Unit conversion form Pa to hPa pressure = pressure / 100.0; // Unit conversion form Pa to hPa
@@ -518,11 +541,12 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
} }
snprintf(buffer, bsize, "%4.0f", pressure); snprintf(buffer, bsize, "%4.0f", pressure);
result.unit = "hPa"; result.unit = "hPa";
result.cvalue = pressure;
} }
//######################################################## //########################################################
else if (value->getFormat() == "formatXdr:P:B"){ else if (value->getFormat() == "formatXdr:P:B"){
double pressure = 0; double pressure = 0;
if (usesimudata == false) { if (simulation == false) {
pressure = value->value; pressure = value->value;
rawvalue = value->value; rawvalue = value->value;
pressure = pressure / 100.0; // Unit conversion form Pa to mBar pressure = pressure / 100.0; // Unit conversion form Pa to mBar
@@ -533,11 +557,12 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
} }
snprintf(buffer, bsize, "%4.0f", pressure); snprintf(buffer, bsize, "%4.0f", pressure);
result.unit = "mBar"; result.unit = "mBar";
result.cvalue = pressure;
} }
//######################################################## //########################################################
else if (value->getFormat() == "formatXdr:U:V"){ else if (value->getFormat() == "formatXdr:U:V"){
double voltage = 0; double voltage = 0;
if (usesimudata == false) { if (simulation == false) {
voltage = value->value; voltage = value->value;
rawvalue = value->value; rawvalue = value->value;
} }
@@ -552,11 +577,12 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
snprintf(buffer, bsize, fmt_dec_10, voltage); snprintf(buffer, bsize, fmt_dec_10, voltage);
} }
result.unit = "V"; result.unit = "V";
result.cvalue = voltage;
} }
//######################################################## //########################################################
else if (value->getFormat() == "formatXdr:I:A"){ else if (value->getFormat() == "formatXdr:I:A"){
double current = 0; double current = 0;
if (usesimudata == false) { if (simulation == false) {
current = value->value; current = value->value;
rawvalue = value->value; rawvalue = value->value;
} }
@@ -574,11 +600,12 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
snprintf(buffer, bsize, fmt_dec_100, current); snprintf(buffer, bsize, fmt_dec_100, current);
} }
result.unit = "A"; result.unit = "A";
result.cvalue = current;
} }
//######################################################## //########################################################
else if (value->getFormat() == "formatXdr:C:K"){ else if (value->getFormat() == "formatXdr:C:K"){
double temperature = 0; double temperature = 0;
if (usesimudata == false) { if (simulation == false) {
temperature = value->value - 273.15; // Convert K to C temperature = value->value - 273.15; // Convert K to C
rawvalue = value->value - 273.15; rawvalue = value->value - 273.15;
} }
@@ -596,11 +623,12 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
snprintf(buffer, bsize, fmt_dec_100, temperature); snprintf(buffer, bsize, fmt_dec_100, temperature);
} }
result.unit = "Deg C"; result.unit = "Deg C";
result.cvalue = temperature;
} }
//######################################################## //########################################################
else if (value->getFormat() == "formatXdr:C:C"){ else if (value->getFormat() == "formatXdr:C:C"){
double temperature = 0; double temperature = 0;
if (usesimudata == false) { if (simulation == false) {
temperature = value->value; // Value in C temperature = value->value; // Value in C
rawvalue = value->value; rawvalue = value->value;
} }
@@ -618,11 +646,12 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
snprintf(buffer, bsize, fmt_dec_100, temperature); snprintf(buffer, bsize, fmt_dec_100, temperature);
} }
result.unit = "Deg C"; result.unit = "Deg C";
result.cvalue = temperature;
} }
//######################################################## //########################################################
else if (value->getFormat() == "formatXdr:H:P"){ else if (value->getFormat() == "formatXdr:H:P"){
double humidity = 0; double humidity = 0;
if (usesimudata == false) { if (simulation == false) {
humidity = value->value; // Value in % humidity = value->value; // Value in %
rawvalue = value->value; rawvalue = value->value;
} }
@@ -640,11 +669,12 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
snprintf(buffer, bsize, fmt_dec_100, humidity); snprintf(buffer, bsize, fmt_dec_100, humidity);
} }
result.unit = "%"; result.unit = "%";
result.cvalue = humidity;
} }
//######################################################## //########################################################
else if (value->getFormat() == "formatXdr:V:P"){ else if (value->getFormat() == "formatXdr:V:P"){
double volume = 0; double volume = 0;
if (usesimudata == false) { if (simulation == false) {
volume = value->value; // Value in % volume = value->value; // Value in %
rawvalue = value->value; rawvalue = value->value;
} }
@@ -662,11 +692,12 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
snprintf(buffer, bsize, fmt_dec_100, volume); snprintf(buffer, bsize, fmt_dec_100, volume);
} }
result.unit = "%"; result.unit = "%";
result.cvalue = volume;
} }
//######################################################## //########################################################
else if (value->getFormat() == "formatXdr:V:M"){ else if (value->getFormat() == "formatXdr:V:M"){
double volume = 0; double volume = 0;
if (usesimudata == false) { if (simulation == false) {
volume = value->value; // Value in l volume = value->value; // Value in l
rawvalue = value->value; rawvalue = value->value;
} }
@@ -684,11 +715,12 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
snprintf(buffer, bsize, fmt_dec_100, volume); snprintf(buffer, bsize, fmt_dec_100, volume);
} }
result.unit = "l"; result.unit = "l";
result.cvalue = volume;
} }
//######################################################## //########################################################
else if (value->getFormat() == "formatXdr:R:I"){ else if (value->getFormat() == "formatXdr:R:I"){
double flow = 0; double flow = 0;
if (usesimudata == false) { if (simulation == false) {
flow = value->value; // Value in l/min flow = value->value; // Value in l/min
rawvalue = value->value; rawvalue = value->value;
} }
@@ -706,11 +738,12 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
snprintf(buffer, bsize, fmt_dec_100, flow); snprintf(buffer, bsize, fmt_dec_100, flow);
} }
result.unit = "l/min"; result.unit = "l/min";
result.cvalue = flow;
} }
//######################################################## //########################################################
else if (value->getFormat() == "formatXdr:G:"){ else if (value->getFormat() == "formatXdr:G:"){
double generic = 0; double generic = 0;
if (usesimudata == false) { if (simulation == false) {
generic = value->value; generic = value->value;
rawvalue = value->value; rawvalue = value->value;
} }
@@ -728,11 +761,12 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
snprintf(buffer, bsize, fmt_dec_100, generic); snprintf(buffer, bsize, fmt_dec_100, generic);
} }
result.unit = ""; result.unit = "";
result.cvalue = generic;
} }
//######################################################## //########################################################
else if (value->getFormat() == "formatXdr:A:P"){ else if (value->getFormat() == "formatXdr:A:P"){
double dplace = 0; double dplace = 0;
if (usesimudata == false) { if (simulation == false) {
dplace = value->value; // Value in % dplace = value->value; // Value in %
rawvalue = value->value; rawvalue = value->value;
} }
@@ -750,11 +784,12 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
snprintf(buffer, bsize, fmt_dec_100, dplace); snprintf(buffer, bsize, fmt_dec_100, dplace);
} }
result.unit = "%"; result.unit = "%";
result.cvalue = dplace;
} }
//######################################################## //########################################################
else if (value->getFormat() == "formatXdr:A:D"){ else if ((value->getFormat() == "formatXdr:A:D") || ((value->getFormat() == "formatXdr:A:rd"))){
double angle = 0; double angle = 0;
if (usesimudata == false) { if (simulation == false) {
angle = value->value; angle = value->value;
angle = angle * 57.2958; // Unit conversion form rad to deg angle = angle * 57.2958; // Unit conversion form rad to deg
rawvalue = value->value; rawvalue = value->value;
@@ -770,11 +805,12 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
snprintf(buffer,bsize,"%3.0f",angle); snprintf(buffer,bsize,"%3.0f",angle);
} }
result.unit = "Deg"; result.unit = "Deg";
result.cvalue = angle;
} }
//######################################################## //########################################################
else if (value->getFormat() == "formatXdr:T:R"){ else if (value->getFormat() == "formatXdr:T:R"){
double rpm = 0; double rpm = 0;
if (usesimudata == false) { if (simulation == false) {
rpm = value->value; // Value in rpm rpm = value->value; // Value in rpm
rawvalue = value->value; rawvalue = value->value;
} }
@@ -792,6 +828,7 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
snprintf(buffer, bsize, fmt_dec_100, rpm); snprintf(buffer, bsize, fmt_dec_100, rpm);
} }
result.unit = "rpm"; result.unit = "rpm";
result.cvalue = rpm;
} }
//######################################################## //########################################################
// Default format // Default format
@@ -807,6 +844,7 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
snprintf(buffer, bsize, fmt_dec_100, value->value); snprintf(buffer, bsize, fmt_dec_100, value->value);
} }
result.unit = ""; result.unit = "";
result.cvalue = value->value;
} }
buffer[bsize] = 0; buffer[bsize] = 0;
result.value = rawvalue; // Return value is only necessary in case of simulation of graphic pointer result.value = rawvalue; // Return value is only necessary in case of simulation of graphic pointer
@@ -814,6 +852,37 @@ FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &common
return result; return result;
} }
// Convert and format boat value from SI to user defined format (definition for compatibility purposes)
FormattedData Formatter::formatValue(GwApi::BoatValue *value, CommonData &commondata) {
return formatValue(value, commondata, false); // call <formatValue> with standard handling of user setting for simulation data
}
// Helper method for conversion of any data value from SI to user defined format
double Formatter::convertValue(const double &value, const String &name, const String &format, CommonData &commondata)
{
std::unique_ptr<GwApi::BoatValue> tmpBValue; // Temp variable to get converted data value from <OBP60Formatter::formatValue>
double result; // data value converted to user defined target data format
constexpr bool NO_SIMUDATA = true; // switch off simulation feature of <formatValue> function
// prepare temporary BoatValue structure for use in <formatValue>
tmpBValue = std::unique_ptr<GwApi::BoatValue>(new GwApi::BoatValue(name)); // we don't need boat value name for pure value conversion
tmpBValue->setFormat(format);
tmpBValue->valid = true;
tmpBValue->value = value;
result = formatValue(tmpBValue.get(), commondata, NO_SIMUDATA).cvalue; // get value (converted); ignore any simulation data setting
return result;
}
// Helper method for conversion of any data value from SI to user defined format
double Formatter::convertValue(const double &value, const String &format, CommonData &commondata)
{
double result; // data value converted to user defined target data format
result = convertValue(value, "dummy", format, commondata);
return result;
}
String formatDate(fmtDate fmttype, uint16_t year, uint8_t month, uint8_t day) { String formatDate(fmtDate fmttype, uint16_t year, uint8_t month, uint8_t day) {
char buffer[12]; char buffer[12];
if (fmttype == fmtDate::GB) { if (fmttype == fmtDate::GB) {

View File

@@ -130,9 +130,10 @@ enum class fmtTemp {KELVIN, CELSUIS, FAHRENHEIT};
// Structure for formatted boat values // Structure for formatted boat values
typedef struct { typedef struct {
double value; double value; // SI value of boat data value
String svalue; double cvalue; // value converted to target unit
String unit; String svalue; // value converted to target unit and formatted
String unit; // target value unit
} FormattedData; } FormattedData;
// Formatter for boat values // Formatter for boat values
@@ -159,7 +160,10 @@ public:
fmtType stringToFormat(const char* formatStr); fmtType stringToFormat(const char* formatStr);
fmtDate getDateFormat(String sformat); fmtDate getDateFormat(String sformat);
fmtTime getTimeFormat(String sformat); fmtTime getTimeFormat(String sformat);
FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool ignoreSimuDataSetting);
FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata); FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata);
double convertValue(const double &value, const String &name, const String &format, CommonData &commondata);
double convertValue(const double &value, const String &format, CommonData &commondata);
String placeholder = "---"; String placeholder = "---";
}; };

View File

@@ -1,11 +1,12 @@
// General hardware definitions // General hardware definitions
// CAN and RS485 bus pin definitions see obp60task.h // CAN and RS485 bus pin definitions see obp60task.h
#ifdef HARDWARE_V21 #if defined HARDWARE_V20 || HARDWARE_V21
// Direction pin for RS485 NMEA0183 // Direction pin for RS485 NMEA0183
#define OBP_DIRECTION_PIN 18 #define OBP_DIRECTION_PIN 18
// I2C // I2C
#define I2C_SPEED 10000UL // 10kHz clock speed on I2C bus #define I2C_SPEED 10000UL // 10kHz clock speed on I2C bus
#define I2C_SPEED_LOW 1000UL // 10kHz clock speed on I2C bus for external bus
#define OBP_I2C_SDA 47 #define OBP_I2C_SDA 47
#define OBP_I2C_SCL 21 #define OBP_I2C_SCL 21
// DS1388 RTC // DS1388 RTC
@@ -22,8 +23,8 @@
#define AS5600_I2C_ADDR 0x36 // Addr. 0x36 (fix) #define AS5600_I2C_ADDR 0x36 // Addr. 0x36 (fix)
// INA219 // INA219
#define SHUNT_VOLTAGE 0.075 // Shunt voltage in V by max. current (75mV) #define SHUNT_VOLTAGE 0.075 // Shunt voltage in V by max. current (75mV)
#define INA219_I2C_ADDR1 0x40 // Addr. 0x41 (fix A0 = 5V, A1 = GND) for battery #define INA219_I2C_ADDR1 0x41 // Addr. 0x41 (fix A0 = 5V, A1 = GND) for battery
#define INA219_I2C_ADDR2 0x41 // Addr. 0x44 (fix A0 = GND, A1 = 5V) for solar panels #define INA219_I2C_ADDR2 0x44 // Addr. 0x44 (fix A0 = GND, A1 = 5V) for solar panels
#define INA219_I2C_ADDR3 0x45 // Addr. 0x45 (fix A0 = 5V, A1 = 5V) for generator #define INA219_I2C_ADDR3 0x45 // Addr. 0x45 (fix A0 = 5V, A1 = 5V) for generator
// INA226 // INA226
#define INA226_I2C_ADDR1 0x41 // Addr. 0x41 (fix A0 = 5V, A1 = GND) for battery #define INA226_I2C_ADDR1 0x41 // Addr. 0x41 (fix A0 = 5V, A1 = GND) for battery
@@ -83,6 +84,7 @@
#define OBP_DIRECTION_PIN 8 #define OBP_DIRECTION_PIN 8
// I2C // I2C
#define I2C_SPEED 100000UL // 100kHz clock speed on I2C bus #define I2C_SPEED 100000UL // 100kHz clock speed on I2C bus
#define I2C_SPEED_LOW 1000UL // 10kHz clock speed on I2C bus for external bus
#define OBP_I2C_SDA 21 #define OBP_I2C_SDA 21
#define OBP_I2C_SCL 38 #define OBP_I2C_SCL 38
// DS1388 RTC // DS1388 RTC
@@ -99,8 +101,8 @@
#define AS5600_I2C_ADDR 0x36 // Addr. 0x36 (fix) #define AS5600_I2C_ADDR 0x36 // Addr. 0x36 (fix)
// INA219 // INA219
#define SHUNT_VOLTAGE 0.075 // Shunt voltage in V by max. current (75mV) #define SHUNT_VOLTAGE 0.075 // Shunt voltage in V by max. current (75mV)
#define INA219_I2C_ADDR1 0x40 // Addr. 0x41 (fix A0 = 5V, A1 = GND) for battery #define INA219_I2C_ADDR1 0x41 // Addr. 0x41 (fix A0 = 5V, A1 = GND) for battery
#define INA219_I2C_ADDR2 0x41 // Addr. 0x44 (fix A0 = GND, A1 = 5V) for solar panels #define INA219_I2C_ADDR2 0x44 // Addr. 0x44 (fix A0 = GND, A1 = 5V) for solar panels
#define INA219_I2C_ADDR3 0x45 // Addr. 0x45 (fix A0 = 5V, A1 = 5V) for generator #define INA219_I2C_ADDR3 0x45 // Addr. 0x45 (fix A0 = 5V, A1 = 5V) for generator
// INA226 // INA226
#define INA226_I2C_ADDR1 0x41 // Addr. 0x41 (fix A0 = 5V, A1 = GND) for battery #define INA226_I2C_ADDR1 0x41 // Addr. 0x41 (fix A0 = 5V, A1 = GND) for battery

View File

@@ -1,152 +1,346 @@
#include "OBP60Formatter.h"
#include "OBPDataOperations.h" #include "OBPDataOperations.h"
//#include "BoatDataCalibration.h" // Functions lib for data instance calibration
// --- Class CalibrationData ---------------
CalibrationData::CalibrationData(GwLog* log)
{
logger = log;
}
void CalibrationData::readConfig(GwConfigHandler* config)
// Initial load of calibration data into internal list
// This method is called once at init phase of <obp60task> to read the configuration values
{
std::string instance;
double offset;
double slope;
double smooth;
String calInstance = "";
String calOffset = "";
String calSlope = "";
String calSmooth = "";
// Load user format configuration values
String lengthFormat = config->getString(config->lengthFormat); // [m|ft]
String distanceFormat = config->getString(config->distanceFormat); // [m|km|nm]
String speedFormat = config->getString(config->speedFormat); // [m/s|km/h|kn]
String windspeedFormat = config->getString(config->windspeedFormat); // [m/s|km/h|kn|bft]
String tempFormat = config->getString(config->tempFormat); // [K|C|F]
// Read calibration settings for data instances
for (int i = 0; i < MAX_CALIBRATION_DATA; i++) {
calInstance = "calInstance" + String(i + 1);
calOffset = "calOffset" + String(i + 1);
calSlope = "calSlope" + String(i + 1);
calSmooth = "calSmooth" + String(i + 1);
instance = std::string(config->getString(calInstance, "---").c_str());
if (instance == "---") {
LOG_DEBUG(GwLog::LOG, "No calibration data for instance no. %d", i + 1);
continue;
}
calibrationMap[instance] = { 0.0f, 1.0f, 1.0f, 0.0f, false };
offset = (config->getString(calOffset, "")).toDouble();
slope = (config->getString(calSlope, "")).toDouble();
smooth = (config->getString(calSmooth, "")).toInt(); // user input is int; further math is done with double
if (slope == 0.0) {
slope = 1.0; // eliminate adjustment if user selected "0" -> that would set the calibrated value to "0"
}
// Convert calibration values from user input format to internal standard SI format
if (instance == "AWS" || instance == "TWS") {
if (windspeedFormat == "m/s") {
// No conversion needed
} else if (windspeedFormat == "km/h") {
offset /= 3.6; // Convert km/h to m/s
} else if (windspeedFormat == "kn") {
offset /= 1.94384; // Convert kn to m/s
} else if (windspeedFormat == "bft") {
offset *= 2 + (offset / 2); // Convert Bft to m/s (approx) -> to be improved
}
} else if (instance == "AWA" || instance == "COG" || instance == "HDM" || instance == "HDT" || instance == "PRPOS" || instance == "RPOS" || instance == "TWA" || instance == "TWD") {
offset *= DEG_TO_RAD; // Convert deg to rad
} else if (instance == "DBS" || instance == "DBT") {
if (lengthFormat == "m") {
// No conversion needed
} else if (lengthFormat == "ft") {
offset /= 3.28084; // Convert ft to m
}
} else if (instance == "SOG" || instance == "STW") {
if (speedFormat == "m/s") {
// No conversion needed
} else if (speedFormat == "km/h") {
offset /= 3.6; // Convert km/h to m/s
} else if (speedFormat == "kn") {
offset /= 1.94384; // Convert kn to m/s
}
} else if (instance == "WTemp") {
if (tempFormat == "K" || tempFormat == "C") {
// No conversion needed
} else if (tempFormat == "F") {
offset *= 9.0 / 5.0; // Convert °F to K
slope *= 9.0 / 5.0; // Convert °F to K
}
}
// transform smoothing factor from [0.01..10] to [0.3..0.95] and invert for exponential smoothing formula
if (smooth <= 0) {
smooth = 0;
} else {
if (smooth > 10) {
smooth = 10;
}
smooth = 0.3 + ((smooth - 0.01) * (0.95 - 0.3) / (10 - 0.01));
}
smooth = 1 - smooth;
calibrationMap[instance].offset = offset;
calibrationMap[instance].slope = slope;
calibrationMap[instance].smooth = smooth;
calibrationMap[instance].isCalibrated = false;
LOG_DEBUG(GwLog::LOG, "Calibration data type added: %s, offset: %f, slope: %f, smoothing: %f", instance.c_str(),
calibrationMap[instance].offset, calibrationMap[instance].slope, calibrationMap[instance].smooth);
}
// LOG_DEBUG(GwLog::LOG, "All calibration data read");
}
// Handle calibrationMap and calibrate all boat data values
void CalibrationData::handleCalibration(BoatValueList* boatValueList)
{
GwApi::BoatValue* bValue;
for (const auto& cMap : calibrationMap) {
std::string instance = cMap.first.c_str();
bValue = boatValueList->findValueOrCreate(String(instance.c_str()));
calibrateInstance(bValue);
smoothInstance(bValue);
}
}
// Calibrate single boat data value
// Return calibrated boat value or DBL_MAX, if no calibration was performed
bool CalibrationData::calibrateInstance(GwApi::BoatValue* boatDataValue)
{
std::string instance = boatDataValue->getName().c_str();
double offset = 0;
double slope = 1.0;
double dataValue = 0;
std::string format = "";
// we test this earlier, but for safety reasons ...
if (calibrationMap.find(instance) == calibrationMap.end()) {
LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s not in calibration list", instance.c_str());
return false;
}
calibrationMap[instance].isCalibrated = false; // reset calibration flag until properly calibrated
if (!boatDataValue->valid) { // no valid boat data value, so we don't want to apply calibration data
return false;
}
offset = calibrationMap[instance].offset;
slope = calibrationMap[instance].slope;
dataValue = boatDataValue->value;
format = boatDataValue->getFormat().c_str();
// LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s: value: %f, format: %s", instance.c_str(), dataValue, format.c_str());
if (format == "formatWind") { // instance is of type angle
dataValue = (dataValue * slope) + offset;
// dataValue = WindUtils::toPI(dataValue);
dataValue = WindUtils::to2PI(dataValue); // we should call <toPI> for format of [-180..180], but pages cannot display negative values properly yet
} else if (format == "formatCourse") { // instance is of type direction
dataValue = (dataValue * slope) + offset;
dataValue = WindUtils::to2PI(dataValue);
} else if (format == "kelvinToC") { // instance is of type temperature
dataValue = ((dataValue - 273.15) * slope) + offset + 273.15;
} else {
dataValue = (dataValue * slope) + offset;
}
boatDataValue->value = dataValue; // update boat data value with calibrated value
calibrationMap[instance].value = dataValue; // store the calibrated value in the list
calibrationMap[instance].isCalibrated = true;
// LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s: Offset: %f, Slope: %f, Result: %f", instance.c_str(), offset, slope, calibrationMap[instance].value);
return true;
}
// Smooth single boat data value
// Return smoothed boat value or DBL_MAX, if no smoothing was performed
bool CalibrationData::smoothInstance(GwApi::BoatValue* boatDataValue)
{
std::string instance = boatDataValue->getName().c_str();
double oldValue = 0;
double dataValue = boatDataValue->value;
double smoothFactor = 0;
// we test this earlier, but for safety reason ...
if (calibrationMap.find(instance) == calibrationMap.end()) {
// LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s not in calibration list", instance.c_str());
return false;
}
calibrationMap[instance].isCalibrated = false; // reset calibration flag until properly calibrated
if (!boatDataValue->valid) { // no valid boat data value, so we don't need to do anything
return false;
}
smoothFactor = calibrationMap[instance].smooth;
if (lastValue.find(instance) != lastValue.end()) {
oldValue = lastValue[instance];
dataValue = oldValue + (smoothFactor * (dataValue - oldValue)); // exponential smoothing algorithm
}
lastValue[instance] = dataValue; // store the new value for next cycle; first time, store only the current value and return
boatDataValue->value = dataValue; // update boat data value with smoothed value
calibrationMap[instance].value = dataValue; // store the smoothed value in the list
calibrationMap[instance].isCalibrated = true;
// LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s: smooth: %f, oldValue: %f, result: %f", instance.c_str(), smoothFactor, oldValue, calibrationMap[instance].value);
return true;
}
// --- End Class CalibrationData ---------------
// --- Class HstryBuf --------------- // --- Class HstryBuf ---------------
// Init history buffers for selected boat data HstryBuf::HstryBuf(const String& name, int size, BoatValueList* boatValues, GwLog* log)
void HstryBuf::init(BoatValueList* boatValues, GwLog *log) { : logger(log)
, boatDataName(name)
{
hstryBuf.resize(size);
boatValue = boatValues->findValueOrCreate(name);
}
logger = log; void HstryBuf::init(const String& format, int updFreq, int mltplr, double minVal, double maxVal)
{
int hstryUpdFreq = 1000; // Update frequency for history buffers in ms hstryBuf.setMetaData(boatDataName, format, updFreq, mltplr, minVal, maxVal);
int hstryMinVal = 0; // Minimum value for these history buffers hstryMin = minVal;
twdHstryMax = 6283; // Max value for wind direction (TWD, AWD) in rad [0...2*PI], shifted by 1000 for 3 decimals hstryMax = maxVal;
twsHstryMax = 65000; // Max value for wind speed (TWS, AWS) in m/s [0..65], shifted by 1000 for 3 decimals if (!boatValue->valid) {
awdHstryMax = twdHstryMax; boatValue->setFormat(format);
awsHstryMax = twsHstryMax; boatValue->value = std::numeric_limits<double>::max(); // mark current value invalid
twdHstryMin = hstryMinVal;
twsHstryMin = hstryMinVal;
awdHstryMin = hstryMinVal;
awsHstryMin = hstryMinVal;
const double DBL_MAX = std::numeric_limits<double>::max();
// Initialize history buffers with meta data
hstryBufList.twdHstry->setMetaData("TWD", "formatCourse", hstryUpdFreq, hstryMinVal, twdHstryMax);
hstryBufList.twsHstry->setMetaData("TWS", "formatKnots", hstryUpdFreq, hstryMinVal, twsHstryMax);
hstryBufList.awdHstry->setMetaData("AWD", "formatCourse", hstryUpdFreq, hstryMinVal, twdHstryMax);
hstryBufList.awsHstry->setMetaData("AWS", "formatKnots", hstryUpdFreq, hstryMinVal, twsHstryMax);
// create boat values for history data types, if they don't exist yet
twdBVal = boatValues->findValueOrCreate(hstryBufList.twdHstry->getName());
twsBVal = boatValues->findValueOrCreate(hstryBufList.twsHstry->getName());
twaBVal = boatValues->findValueOrCreate("TWA");
awdBVal = boatValues->findValueOrCreate(hstryBufList.awdHstry->getName());
awsBVal = boatValues->findValueOrCreate(hstryBufList.awsHstry->getName());
if (!awdBVal->valid) { // AWD usually does not exist
awdBVal->setFormat(hstryBufList.awdHstry->getFormat());
awdBVal->value = DBL_MAX;
} }
}
void HstryBuf::add(double value)
{
if (value >= hstryMin && value <= hstryMax) {
hstryBuf.add(value);
// LOG_DEBUG(GwLog::DEBUG, "HstryBuf::add: name: %s, value: %.3f", hstryBuf.getName(), value);
}
}
void HstryBuf::handle(bool useSimuData, CommonData& common)
{
// GwApi::BoatValue* tmpBVal;
std::unique_ptr<GwApi::BoatValue> tmpBVal; // Temp variable to get formatted and converted data value from OBP60Formatter
// create temporary boat value for calibration purposes and retrieval of simulation value
// tmpBVal = new GwApi::BoatValue(boatDataName.c_str());
tmpBVal = std::unique_ptr<GwApi::BoatValue>(new GwApi::BoatValue(boatDataName));
tmpBVal->setFormat(boatValue->getFormat());
tmpBVal->value = boatValue->value;
tmpBVal->valid = boatValue->valid;
if (boatValue->valid) {
// Calibrate boat value before adding it to history buffer
//calibrationData.calibrateInstance(tmpBVal.get(), logger);
//add(tmpBVal->value);
add(boatValue->value);
} else if (useSimuData) { // add simulated value to history buffer
double simSIValue = common.fmt->formatValue(tmpBVal.get(), common).value; // simulated value is generated at <formatValue>; here: retreive SI value
add(simSIValue);
} else {
// here we will add invalid (DBL_MAX) value; this will mark periods of missing data in buffer together with a timestamp
}
}
// --- End Class HstryBuf ---------------
// --- Class HstryBuffers ---------------
HstryBuffers::HstryBuffers(int size, BoatValueList* boatValues, GwLog* log)
: size(size)
, boatValueList(boatValues)
, logger(log)
{
// collect boat values for true wind calculation // collect boat values for true wind calculation
awaBVal = boatValues->findValueOrCreate("AWA"); // should all have been already created at true wind object initialization
hdtBVal = boatValues->findValueOrCreate("HDT"); // potentially to be moved to history buffer handling
hdmBVal = boatValues->findValueOrCreate("HDM"); awaBVal = boatValueList->findValueOrCreate("AWA");
varBVal = boatValues->findValueOrCreate("VAR"); hdtBVal = boatValueList->findValueOrCreate("HDT");
cogBVal = boatValues->findValueOrCreate("COG"); hdmBVal = boatValueList->findValueOrCreate("HDM");
sogBVal = boatValues->findValueOrCreate("SOG"); varBVal = boatValueList->findValueOrCreate("VAR");
cogBVal = boatValueList->findValueOrCreate("COG");
sogBVal = boatValueList->findValueOrCreate("SOG");
awdBVal = boatValueList->findValueOrCreate("AWD");
} }
// Handle history buffers for TWD, TWS, AWD, AWS // Create history buffer for boat data type
//void HstryBuf::handleHstryBuf(GwApi* api, BoatValueList* boatValues, bool useSimuData) { void HstryBuffers::addBuffer(const String& name)
void HstryBuf::handleHstryBuf(bool useSimuData) { {
if (HstryBuffers::getBuffer(name) != nullptr) { // buffer for this data type already exists
static int16_t twd = 20; //initial value only relevant if we use simulation data return;
static uint16_t tws = 20; //initial value only relevant if we use simulation data
static double awd, aws, hdt = 20; //initial value only relevant if we use simulation data
GwApi::BoatValue *calBVal; // temp variable just for data calibration -> we don't want to calibrate the original data here
LOG_DEBUG(GwLog::DEBUG,"obp60task handleHstryBuf: TWD_isValid? %d, twdBVal: %.1f, twaBVal: %.1f, twsBVal: %.1f", twdBVal->valid, twdBVal->value * RAD_TO_DEG,
twaBVal->value * RAD_TO_DEG, twsBVal->value * 3.6 / 1.852);
if (twdBVal->valid) {
calBVal = new GwApi::BoatValue("TWD"); // temporary solution for calibration of history buffer values
calBVal->setFormat(twdBVal->getFormat());
calBVal->value = twdBVal->value;
calBVal->valid = twdBVal->valid;
calibrationData.calibrateInstance(calBVal, logger); // Check if boat data value is to be calibrated
twd = static_cast<int16_t>(std::round(calBVal->value * 1000.0));
if (twd >= twdHstryMin && twd <= twdHstryMax) {
hstryBufList.twdHstry->add(twd);
} }
delete calBVal; if (bufferParams.find(name) == bufferParams.end()) { // requested boat data type is not supported in list of <bufferParams>
calBVal = nullptr; return;
} else if (useSimuData) {
twd += random(-20, 20);
twd = WindUtils::to360(twd);
hstryBufList.twdHstry->add(static_cast<int16_t>(DegToRad(twd) * 1000.0));
} }
if (twsBVal->valid) { hstryBuffers[name] = std::unique_ptr<HstryBuf>(new HstryBuf(name, size, boatValueList, logger));
calBVal = new GwApi::BoatValue("TWS"); // temporary solution for calibration of history buffer values
calBVal->setFormat(twsBVal->getFormat());
calBVal->value = twsBVal->value;
calBVal->valid = twsBVal->valid;
calibrationData.calibrateInstance(calBVal, logger); // Check if boat data value is to be calibrated
tws = static_cast<uint16_t>(std::round(calBVal->value * 1000));
if (tws >= twsHstryMin && tws <= twsHstryMax) {
hstryBufList.twsHstry->add(tws);
}
delete calBVal;
calBVal = nullptr;
} else if (useSimuData) {
tws += random(-5000, 5000); // TWS value in m/s; expands to 3 decimals
tws = constrain(tws, 0, 25000); // Limit TWS to [0..25] m/s
hstryBufList.twsHstry->add(tws);
}
if (awaBVal->valid) { // Initialize metadata for buffer
if (hdtBVal->valid) { String valueFormat = bufferParams[name].format; // Data format of boat data type
hdt = hdtBVal->value; // Use HDT if available // String valueFormat = boatValueList->findValueOrCreate(name)->getFormat().c_str(); // Unfortunately, format is not yet available during system initialization
} else { int hstryUpdFreq = bufferParams[name].hstryUpdFreq; // Update frequency for history buffers in ms
hdt = WindUtils::calcHDT(&hdmBVal->value, &varBVal->value, &cogBVal->value, &sogBVal->value); int mltplr = bufferParams[name].mltplr; // default multiplier which transforms original <double> value into buffer type format
} double bufferMinVal = bufferParams[name].bufferMinVal; // Min value for this history buffer
double bufferMaxVal = bufferParams[name].bufferMaxVal; // Max value for this history buffer
awd = awaBVal->value + hdt; hstryBuffers[name]->init(valueFormat, hstryUpdFreq, mltplr, bufferMinVal, bufferMaxVal);
awd = WindUtils::to2PI(awd); LOG_DEBUG(GwLog::DEBUG, "HstryBuffers: new buffer added: name: %s, format: %s, multiplier: %d, min value: %.2f, max value: %.2f", name, valueFormat, mltplr, bufferMinVal, bufferMaxVal);
calBVal = new GwApi::BoatValue("AWD"); // temporary solution for calibration of history buffer values }
calBVal->value = awd;
calBVal->setFormat(awdBVal->getFormat());
calBVal->valid = true;
calibrationData.calibrateInstance(calBVal, logger); // Check if boat data value is to be calibrated
awdBVal->value = calBVal->value;
awdBVal->valid = true;
awd = std::round(calBVal->value * 1000.0);
if (awd >= awdHstryMin && awd <= awdHstryMax) {
hstryBufList.awdHstry->add(static_cast<int16_t>(awd));
}
delete calBVal;
calBVal = nullptr;
} else if (useSimuData) {
awd += random(-20, 20);
awd = WindUtils::to360(awd);
hstryBufList.awdHstry->add(static_cast<int16_t>(DegToRad(awd) * 1000.0));
}
if (awsBVal->valid) { // Handle all registered history buffers
calBVal = new GwApi::BoatValue("AWS"); // temporary solution for calibration of history buffer values void HstryBuffers::handleHstryBufs(bool useSimuData, CommonData& common)
calBVal->setFormat(awsBVal->getFormat()); {
calBVal->value = awsBVal->value; for (auto& bufMap : hstryBuffers) {
calBVal->valid = awsBVal->valid; auto& buf = bufMap.second;
calibrationData.calibrateInstance(calBVal, logger); // Check if boat data value is to be calibrated buf->handle(useSimuData, common);
aws = std::round(calBVal->value * 1000);
if (aws >= awsHstryMin && aws <= awsHstryMax) {
hstryBufList.awsHstry->add(static_cast<uint16_t>(aws));
}
delete calBVal;
calBVal = nullptr;
} else if (useSimuData) {
aws += random(-5000, 5000); // TWS value in m/s; expands to 1 decimal
aws = constrain(aws, 0, 25000); // Limit TWS to [0..25] m/s
hstryBufList.awsHstry->add(aws);
} }
} }
// --- Class HstryBuf ---------------
RingBuffer<uint16_t>* HstryBuffers::getBuffer(const String& name)
{
auto it = hstryBuffers.find(name);
if (it != hstryBuffers.end()) {
return &it->second->hstryBuf;
}
return nullptr;
}
// --- End Class HstryBuffers ---------------
// --- Class WindUtils -------------- // --- Class WindUtils --------------
double WindUtils::to2PI(double a) double WindUtils::to2PI(double a)
{ {
a = fmod(a, 2 * M_PI); a = fmod(a, M_TWOPI);
if (a < 0.0) { if (a < 0.0) {
a += 2 * M_PI; a += M_TWOPI;
} }
return a; return a;
} }
@@ -162,18 +356,18 @@ double WindUtils::toPI(double a)
double WindUtils::to360(double a) double WindUtils::to360(double a)
{ {
a = fmod(a, 360); a = fmod(a, 360.0);
if (a < 0.0) { if (a < 0.0) {
a += 360; a += 360.0;
} }
return a; return a;
} }
double WindUtils::to180(double a) double WindUtils::to180(double a)
{ {
a += 180; a += 180.0;
a = to360(a); a = to360(a);
a -= 180; a -= 180.0;
return a; return a;
} }
@@ -205,14 +399,14 @@ void WindUtils::addPolar(const double* phi1, const double* r1,
void WindUtils::calcTwdSA(const double* AWA, const double* AWS, void WindUtils::calcTwdSA(const double* AWA, const double* AWS,
const double* CTW, const double* STW, const double* HDT, const double* CTW, const double* STW, const double* HDT,
double* TWD, double* TWS, double* TWA) double* TWD, double* TWS, double* TWA, double* AWD)
{ {
double awd = *AWA + *HDT; *AWD = *AWA + *HDT;
awd = to2PI(awd); *AWD = to2PI(*AWD);
double stw = -*STW; double stw = -*STW;
addPolar(&awd, AWS, CTW, &stw, TWD, TWS); addPolar(AWD, AWS, CTW, &stw, TWD, TWS);
// Normalize TWD and TWA to 0-360° // Normalize TWD to [0..360°] (2PI) and TWA to [-180..180] (PI)
*TWD = to2PI(*TWD); *TWD = to2PI(*TWD);
*TWA = toPI(*TWD - *HDT); *TWA = toPI(*TWD - *HDT);
} }
@@ -234,12 +428,12 @@ double WindUtils::calcHDT(const double* hdmVal, const double* varVal, const doub
return hdt; return hdt;
} }
bool WindUtils::calcTrueWind(const double* awaVal, const double* awsVal, bool WindUtils::calcWinds(const double* awaVal, const double* awsVal,
const double* cogVal, const double* stwVal, const double* sogVal, const double* hdtVal, const double* cogVal, const double* stwVal, const double* sogVal, const double* hdtVal,
const double* hdmVal, const double* varVal, double* twdVal, double* twsVal, double* twaVal) const double* hdmVal, const double* varVal, double* twdVal, double* twsVal, double* twaVal, double* awdVal)
{ {
double stw, hdt, ctw; double stw, hdt, ctw;
double twd, tws, twa; double twd, tws, twa, awd;
double minSogVal = 0.1; // SOG below this value (m/s) is assumed to be data noise from GPS sensor double minSogVal = 0.1; // SOG below this value (m/s) is assumed to be data noise from GPS sensor
if (*hdtVal != DBL_MAX) { if (*hdtVal != DBL_MAX) {
@@ -263,44 +457,59 @@ bool WindUtils::calcTrueWind(const double* awaVal, const double* awsVal,
// If STW and SOG are not available, we cannot calculate true wind // If STW and SOG are not available, we cannot calculate true wind
return false; return false;
} }
// Serial.println("\ncalcTrueWind: HDT: " + String(hdt) + ", CTW: " + String(ctw) + ", STW: " + String(stw)); // LOG_DEBUG(GwLog::DEBUG, "WindUtils:calcWinds: HDT: %.1f, CTW %.1f, STW %.1f", hdt, ctw, stw);
if ((*awaVal == DBL_MAX) || (*awsVal == DBL_MAX)) { if ((*awaVal == DBL_MAX) || (*awsVal == DBL_MAX)) {
// Cannot calculate true wind without valid AWA, AWS; other checks are done earlier // Cannot calculate true wind without valid AWA, AWS; other checks are done earlier
return false; return false;
} else { } else {
calcTwdSA(awaVal, awsVal, &ctw, &stw, &hdt, &twd, &tws, &twa); calcTwdSA(awaVal, awsVal, &ctw, &stw, &hdt, &twd, &tws, &twa, &awd);
*twdVal = twd; *twdVal = twd;
*twsVal = tws; *twsVal = tws;
*twaVal = twa; *twaVal = twa;
*awdVal = awd;
return true; return true;
} }
} }
// Calculate true wind data and add to obp60task boat data list // Calculate true wind data and add to obp60task boat data list
bool WindUtils::addTrueWind(GwApi* api, BoatValueList* boatValues, GwLog* log) { bool WindUtils::addWinds()
{
double twd, tws, twa, awd, hdt;
bool twCalculated = false;
bool awdCalculated = false;
GwLog* logger = log; double awaVal = awaBVal->valid ? awaBVal->value : DBL_MAX;
double awsVal = awsBVal->valid ? awsBVal->value : DBL_MAX;
double cogVal = cogBVal->valid ? cogBVal->value : DBL_MAX;
double stwVal = stwBVal->valid ? stwBVal->value : DBL_MAX;
double sogVal = sogBVal->valid ? sogBVal->value : DBL_MAX;
double hdtVal = hdtBVal->valid ? hdtBVal->value : DBL_MAX;
double hdmVal = hdmBVal->valid ? hdmBVal->value : DBL_MAX;
double varVal = varBVal->valid ? varBVal->value : DBL_MAX;
//LOG_DEBUG(GwLog::DEBUG, "WindUtils:addWinds: AWA %.1f, AWS %.1f, COG %.1f, STW %.1f, SOG %.2f, HDT %.1f, HDM %.1f, VAR %.1f", awaBVal->value * RAD_TO_DEG, awsBVal->value * 3.6 / 1.852,
// cogBVal->value * RAD_TO_DEG, stwBVal->value * 3.6 / 1.852, sogBVal->value * 3.6 / 1.852, hdtBVal->value * RAD_TO_DEG, hdmBVal->value * RAD_TO_DEG, varBVal->value * RAD_TO_DEG);
double awaVal, awsVal, cogVal, stwVal, sogVal, hdtVal, hdmVal, varVal; // Check if TWD can be calculated from TWA and HDT/HDM
double twd, tws, twa; if (twaBVal->valid) {
bool isCalculated = false; if (!twdBVal->valid) {
if (hdtVal != DBL_MAX) {
hdt = hdtVal; // Use HDT if available
} else {
hdt = calcHDT(&hdmVal, &varVal, &cogVal, &sogVal);
}
twd = twaBVal->value + hdt;
twd = to2PI(twd);
twdBVal->value = twd;
twdBVal->valid = true;
}
awaVal = awaBVal->valid ? awaBVal->value : DBL_MAX; } else {
awsVal = awsBVal->valid ? awsBVal->value : DBL_MAX; // Calculate true winds and AWD; if true winds exist, use at least AWD calculation
cogVal = cogBVal->valid ? cogBVal->value : DBL_MAX; twCalculated = calcWinds(&awaVal, &awsVal, &cogVal, &stwVal, &sogVal, &hdtVal, &hdmVal, &varVal, &twd, &tws, &twa, &awd);
stwVal = stwBVal->valid ? stwBVal->value : DBL_MAX;
sogVal = sogBVal->valid ? sogBVal->value : DBL_MAX;
hdtVal = hdtBVal->valid ? hdtBVal->value : DBL_MAX;
hdmVal = hdmBVal->valid ? hdmBVal->value : DBL_MAX;
varVal = varBVal->valid ? varBVal->value : DBL_MAX;
LOG_DEBUG(GwLog::DEBUG,"obp60task addTrueWind: AWA %.1f, AWS %.1f, COG %.1f, STW %.1f, SOG %.2f, HDT %.1f, HDM %.1f, VAR %.1f", awaBVal->value * RAD_TO_DEG, awsBVal->value * 3.6 / 1.852,
cogBVal->value * RAD_TO_DEG, stwBVal->value * 3.6 / 1.852, sogBVal->value * 3.6 / 1.852, hdtBVal->value * RAD_TO_DEG, hdmBVal->value * RAD_TO_DEG, varBVal->value * RAD_TO_DEG);
isCalculated = calcTrueWind(&awaVal, &awsVal, &cogVal, &stwVal, &sogVal, &hdtVal, &hdmVal, &varVal, &twd, &tws, &twa); if (twCalculated) { // Replace values only, if successfully calculated and not already available
if (isCalculated) { // Replace values only, if successfully calculated and not already available
if (!twdBVal->valid) { if (!twdBVal->valid) {
twdBVal->value = twd; twdBVal->value = twd;
twdBVal->valid = true; twdBVal->valid = true;
@@ -310,13 +519,19 @@ bool WindUtils::addTrueWind(GwApi* api, BoatValueList* boatValues, GwLog* log) {
twsBVal->valid = true; twsBVal->valid = true;
} }
if (!twaBVal->valid) { if (!twaBVal->valid) {
twaBVal->value = twa; //twaBVal->value = twa;
twaBVal->value = to2PI(twa); // convert to [0..360], because pages cannot display negative values properly yet
twaBVal->valid = true; twaBVal->valid = true;
} }
if (!awdBVal->valid) {
awdBVal->value = awd;
awdBVal->valid = true;
} }
LOG_DEBUG(GwLog::DEBUG,"obp60task addTrueWind: isCalculated %d, TWD %.1f, TWA %.1f, TWS %.1f", isCalculated, twdBVal->value * RAD_TO_DEG, }
twaBVal->value * RAD_TO_DEG, twsBVal->value * 3.6 / 1.852); }
// LOG_DEBUG(GwLog::DEBUG, "WindUtils:addWinds: twCalculated %d, TWD %.1f, TWA %.1f, TWS %.2f kn, AWD: %.1f", twCalculated, twdBVal->value * RAD_TO_DEG,
// twaBVal->value * RAD_TO_DEG, twsBVal->value * 3.6 / 1.852, awdBVal->value * RAD_TO_DEG);
return isCalculated; return twCalculated;
} }
// --- Class WindUtils -------------- // --- End Class WindUtils --------------

View File

@@ -1,68 +1,116 @@
// Function lib for boat data calibration, history buffer handling, true wind calculation, and other operations on boat data
#pragma once #pragma once
#include <N2kMessages.h>
#include "OBPRingBuffer.h" #include "OBPRingBuffer.h"
#include "BoatDataCalibration.h" // Functions lib for data instance calibration #include "Pagedata.h"
#include "obp60task.h" #include "obp60task.h"
#include <math.h> #include <map>
#include <unordered_map>
typedef struct { // Calibration of boat data values, when user setting available
RingBuffer<int16_t>* twdHstry; // supported boat data types are: AWA, AWS, COG, DBS, DBT, HDM, HDT, PRPOS, RPOS, SOG, STW, TWA, TWS, TWD, WTemp
RingBuffer<uint16_t>* twsHstry; class CalibrationData {
RingBuffer<int16_t>* awdHstry; private:
RingBuffer<uint16_t>* awsHstry; typedef struct {
} tBoatHstryData; // Holds pointers to all history buffers for boat data double offset; // calibration offset
double slope; // calibration slope
double smooth; // smoothing factor
double value; // calibrated data value (for future use)
bool isCalibrated; // is data instance value calibrated? (for future use)
} tCalibrationData;
std::unordered_map<std::string, tCalibrationData> calibrationMap; // list of calibration data instances
std::unordered_map<std::string, double> lastValue; // array for last smoothed values of boat data values
GwLog* logger;
static constexpr int8_t MAX_CALIBRATION_DATA = 4; // maximum number of calibration data instances
public:
CalibrationData(GwLog* log);
void readConfig(GwConfigHandler* config);
void handleCalibration(BoatValueList* boatValues); // Handle calibrationMap and calibrate all boat data values
bool calibrateInstance(GwApi::BoatValue* boatDataValue); // Calibrate single boat data value
bool smoothInstance(GwApi::BoatValue* boatDataValue); // Smooth single boat data value
};
class HstryBuf { class HstryBuf {
private: private:
GwLog *logger; RingBuffer<uint16_t> hstryBuf; // Circular buffer to store history values
String boatDataName;
double hstryMin;
double hstryMax;
GwApi::BoatValue* boatValue;
GwLog* logger;
RingBuffer<int16_t> twdHstry; // Circular buffer to store true wind direction values friend class HstryBuffers;
RingBuffer<uint16_t> twsHstry; // Circular buffer to store true wind speed values (TWS)
RingBuffer<int16_t> awdHstry; // Circular buffer to store apparant wind direction values
RingBuffer<uint16_t> awsHstry; // Circular buffer to store apparant xwind speed values (AWS)
int16_t twdHstryMin; // Min value for wind direction (TWD) in history buffer
int16_t twdHstryMax; // Max value for wind direction (TWD) in history buffer
uint16_t twsHstryMin;
uint16_t twsHstryMax;
int16_t awdHstryMin;
int16_t awdHstryMax;
uint16_t awsHstryMin;
uint16_t awsHstryMax;
// boat values for buffers and for true wind calculation
GwApi::BoatValue *twdBVal, *twsBVal, *twaBVal, *awdBVal, *awsBVal;
GwApi::BoatValue *awaBVal, *hdtBVal, *hdmBVal, *varBVal, *cogBVal, *sogBVal;
public: public:
tBoatHstryData hstryBufList; HstryBuf(const String& name, int size, BoatValueList* boatValues, GwLog* log);
void init(const String& format, int updFreq, int mltplr, double minVal, double maxVal);
void add(double value);
void handle(bool useSimuData, CommonData& common);
};
HstryBuf(){ class HstryBuffers {
hstryBufList = {&twdHstry, &twsHstry, &awdHstry, &awsHstry}; // Generate history buffers of zero size private:
std::map<String, std::unique_ptr<HstryBuf>> hstryBuffers;
int size; // size of all history buffers
BoatValueList* boatValueList;
GwLog* logger;
GwApi::BoatValue *awaBVal, *hdtBVal, *hdmBVal, *varBVal, *cogBVal, *sogBVal, *awdBVal; // boat values for true wind calculation
struct HistoryParams {
int hstryUpdFreq; // update frequency of history buffer (documentation only)
int mltplr; // specifies actual value precision being storable:
// [10000: 0 - 6.5535 | 1000: 0 - 65.535 | 100: 0 - 650.35 | 10: 0 - 6503.5
double bufferMinVal; // minimum valid data value
double bufferMaxVal; // maximum valid data value
String format; // format of data type
}; };
HstryBuf(int size) {
hstryBufList = {&twdHstry, &twsHstry, &awdHstry, &awsHstry}; // Define buffer parameters for supported boat data type
hstryBufList.twdHstry->resize(960); // store 960 TWD values for 16 minutes history std::map<String, HistoryParams> bufferParams = {
hstryBufList.twsHstry->resize(960); { "AWA", { 1000, 10000, 0.0, M_TWOPI, "formatWind" } },
hstryBufList.awdHstry->resize(960); { "AWD", { 1000, 10000, 0.0, M_TWOPI, "formatCourse" } },
hstryBufList.awsHstry->resize(960); { "AWS", { 1000, 1000, 0.0, 65.0, "formatKnots" } },
{ "COG", { 1000, 10000, 0.0, M_TWOPI, "formatCourse" } },
{ "DBS", { 1000, 100, 0.0, 650.0, "formatDepth" } },
{ "DBT", { 1000, 100, 0.0, 650.0, "formatDepth" } },
{ "DPT", { 1000, 100, 0.0, 650.0, "formatDepth" } },
{ "HDM", { 1000, 10000, 0.0, M_TWOPI, "formatCourse" } },
{ "HDT", { 1000, 10000, 0.0, M_TWOPI, "formatCourse" } },
{ "ROT", { 1000, 10000, -M_PI / 180.0 * 99.0, M_PI / 180.0 * 99.0, "formatRot" } }, // min/max is -/+ 99 degrees for "rate of turn"
{ "SOG", { 1000, 1000, 0.0, 65.0, "formatKnots" } },
{ "STW", { 1000, 1000, 0.0, 65.0, "formatKnots" } },
{ "TWA", { 1000, 10000, 0.0, M_TWOPI, "formatWind" } },
{ "TWD", { 1000, 10000, 0.0, M_TWOPI, "formatCourse" } },
{ "TWS", { 1000, 1000, 0.0, 65.0, "formatKnots" } },
{ "WTemp", { 1000, 100, 233.0, 650.0, "kelvinToC" } } // [-50..376] °C
}; };
void init(BoatValueList* boatValues, GwLog *log);
void handleHstryBuf(bool useSimuData); public:
HstryBuffers(int size, BoatValueList* boatValues, GwLog* log);
void addBuffer(const String& name);
void handleHstryBufs(bool useSimuData, CommonData& common);
RingBuffer<uint16_t>* getBuffer(const String& name);
}; };
class WindUtils { class WindUtils {
private: private:
GwApi::BoatValue *twdBVal, *twsBVal, *twaBVal; GwApi::BoatValue *twaBVal, *twsBVal, *twdBVal;
GwApi::BoatValue *awaBVal, *awsBVal, *cogBVal, *stwBVal, *sogBVal, *hdtBVal, *hdmBVal, *varBVal; GwApi::BoatValue *awaBVal, *awsBVal, *awdBVal, *cogBVal, *stwBVal, *sogBVal, *hdtBVal, *hdmBVal, *varBVal;
static constexpr double DBL_MAX = std::numeric_limits<double>::max(); static constexpr double DBL_MAX = std::numeric_limits<double>::max();
GwLog* logger;
public: public:
WindUtils(BoatValueList* boatValues){ WindUtils(BoatValueList* boatValues, GwLog* log)
twdBVal = boatValues->findValueOrCreate("TWD"); : logger(log)
twsBVal = boatValues->findValueOrCreate("TWS"); {
twaBVal = boatValues->findValueOrCreate("TWA"); twaBVal = boatValues->findValueOrCreate("TWA");
twsBVal = boatValues->findValueOrCreate("TWS");
twdBVal = boatValues->findValueOrCreate("TWD");
awaBVal = boatValues->findValueOrCreate("AWA"); awaBVal = boatValues->findValueOrCreate("AWA");
awsBVal = boatValues->findValueOrCreate("AWS"); awsBVal = boatValues->findValueOrCreate("AWS");
awdBVal = boatValues->findValueOrCreate("AWD");
cogBVal = boatValues->findValueOrCreate("COG"); cogBVal = boatValues->findValueOrCreate("COG");
stwBVal = boatValues->findValueOrCreate("STW"); stwBVal = boatValues->findValueOrCreate("STW");
sogBVal = boatValues->findValueOrCreate("SOG"); sogBVal = boatValues->findValueOrCreate("SOG");
@@ -70,6 +118,7 @@ public:
hdmBVal = boatValues->findValueOrCreate("HDM"); hdmBVal = boatValues->findValueOrCreate("HDM");
varBVal = boatValues->findValueOrCreate("VAR"); varBVal = boatValues->findValueOrCreate("VAR");
}; };
static double to2PI(double a); static double to2PI(double a);
static double toPI(double a); static double toPI(double a);
static double to360(double a); static double to360(double a);
@@ -81,10 +130,10 @@ public:
double* phi, double* r); double* phi, double* r);
void calcTwdSA(const double* AWA, const double* AWS, void calcTwdSA(const double* AWA, const double* AWS,
const double* CTW, const double* STW, const double* HDT, const double* CTW, const double* STW, const double* HDT,
double* TWD, double* TWS, double* TWA); double* TWD, double* TWS, double* TWA, double* AWD);
static double calcHDT(const double* hdmVal, const double* varVal, const double* cogVal, const double* sogVal); static double calcHDT(const double* hdmVal, const double* varVal, const double* cogVal, const double* sogVal);
bool calcTrueWind(const double* awaVal, const double* awsVal, bool calcWinds(const double* awaVal, const double* awsVal,
const double* cogVal, const double* stwVal, const double* sogVal, const double* hdtVal, const double* cogVal, const double* stwVal, const double* sogVal, const double* hdtVal,
const double* hdmVal, const double* varVal, double* twdVal, double* twsVal, double* twaVal); const double* hdmVal, const double* varVal, double* twdVal, double* twsVal, double* twaVal, double* awdVal);
bool addTrueWind(GwApi* api, BoatValueList* boatValues, GwLog *log); bool addWinds();
}; };

View File

@@ -1,15 +1,44 @@
#pragma once #pragma once
#include "FreeRTOS.h"
#include "GwSynchronized.h" #include "GwSynchronized.h"
#include <algorithm>
#include <limits>
#include <stdexcept>
#include <vector> #include <vector>
#include "WString.h" #include <WString.h>
template <typename T>
struct PSRAMAllocator {
using value_type = T;
PSRAMAllocator() = default;
template <class U>
constexpr PSRAMAllocator(const PSRAMAllocator<U>&) noexcept { }
T* allocate(std::size_t n)
{
void* ptr = heap_caps_malloc(n * sizeof(T), MALLOC_CAP_SPIRAM);
if (!ptr) {
return nullptr;
} else {
return static_cast<T*>(ptr);
}
}
void deallocate(T* p, std::size_t) noexcept
{
heap_caps_free(p);
}
};
template <class T, class U>
bool operator==(const PSRAMAllocator<T>&, const PSRAMAllocator<U>&) { return true; }
template <class T, class U>
bool operator!=(const PSRAMAllocator<T>&, const PSRAMAllocator<U>&) { return false; }
template <typename T> template <typename T>
class RingBuffer { class RingBuffer {
private: private:
std::vector<T> buffer; // THE buffer vector std::vector<T, PSRAMAllocator<T>> buffer; // THE buffer vector, allocated in PSRAM
size_t capacity; size_t capacity;
size_t head; // Points to the next insertion position size_t head; // Points to the next insertion position
size_t first; // Points to the first (oldest) valid element size_t first; // Points to the first (oldest) valid element
@@ -18,49 +47,52 @@ private:
bool is_Full; // Indicates that all buffer elements are used and ringing is in use bool is_Full; // Indicates that all buffer elements are used and ringing is in use
T MIN_VAL; // lowest possible value of buffer of type <T> T MIN_VAL; // lowest possible value of buffer of type <T>
T MAX_VAL; // highest possible value of buffer of type <T> -> indicates invalid value in buffer T MAX_VAL; // highest possible value of buffer of type <T> -> indicates invalid value in buffer
double dblMIN_VAL, dblMAX_VAL; // MIN_VAL, MAX_VAL in double format
mutable SemaphoreHandle_t bufLocker; mutable SemaphoreHandle_t bufLocker;
// metadata for buffer // metadata for buffer
String dataName; // Name of boat data in buffer String dataName; // Name of boat data in buffer
String dataFmt; // Format of boat data in buffer String dataFmt; // Format of boat data in buffer
int updFreq; // Update frequency in milliseconds int updFreq; // Update frequency in milliseconds
T smallest; // Value range of buffer: smallest value; needs to be => MIN_VAL double mltplr; // Multiplier which transforms original <double> value into buffer type format
T largest; // Value range of buffer: biggest value; needs to be < MAX_VAL, since MAX_VAL indicates invalid entries double smallest; // Value range of buffer: smallest value; needs to be => MIN_VAL
double largest; // Value range of buffer: biggest value; needs to be < MAX_VAL, since MAX_VAL indicates invalid entries
void initCommon(); void initCommon();
public: public:
RingBuffer(); RingBuffer();
RingBuffer(size_t size); RingBuffer(size_t size);
void setMetaData(String name, String format, int updateFrequency, T minValue, T maxValue); // Set meta data for buffer void setMetaData(String name, String format, int updateFrequency, double multiplier, double minValue, double maxValue); // Set meta data for buffer
bool getMetaData(String& name, String& format, int& updateFrequency, T& minValue, T& maxValue); // Get meta data of buffer bool getMetaData(String& name, String& format, int& updateFrequency, double& multiplier, double& minValue, double& maxValue); // Get meta data of buffer
bool getMetaData(String& name, String& format); bool getMetaData(String& name, String& format);
String getName() const; // Get buffer name String getName() const; // Get buffer name
String getFormat() const; // Get buffer data format String getFormat() const; // Get buffer data format
void add(const T& value); // Add a new value to buffer void add(const double& value); // Add a new value to buffer
T get(size_t index) const; // Get value at specific position (0-based index from oldest to newest) double get(size_t index) const; // Get value at specific position (0-based index from oldest to newest)
T getFirst() const; // Get the first (oldest) value in buffer double getFirst() const; // Get the first (oldest) value in buffer
T getLast() const; // Get the last (newest) value in buffer double getLast() const; // Get the last (newest) value in buffer
T getMin() const; // Get the lowest value in buffer double getMin() const; // Get the lowest value in buffer
T getMin(size_t amount) const; // Get minimum value of the last <amount> values of buffer double getMin(size_t amount) const; // Get minimum value of the last <amount> values of buffer
T getMax() const; // Get the highest value in buffer double getMax() const; // Get the highest value in buffer
T getMax(size_t amount) const; // Get maximum value of the last <amount> values of buffer double getMax(size_t amount) const; // Get maximum value of the last <amount> values of buffer
T getMid() const; // Get mid value between <min> and <max> value in buffer double getMid() const; // Get mid value between <min> and <max> value in buffer
T getMid(size_t amount) const; // Get mid value between <min> and <max> value of the last <amount> values of buffer double getMid(size_t amount) const; // Get mid value between <min> and <max> value of the last <amount> values of buffer
T getMedian() const; // Get the median value in buffer double getMedian() const; // Get the median value in buffer
T getMedian(size_t amount) const; // Get the median value of the last <amount> values of buffer double getMedian(size_t amount) const; // Get the median value of the last <amount> values of buffer
size_t getCapacity() const; // Get the buffer capacity (maximum size) size_t getCapacity() const; // Get the buffer capacity (maximum size)
size_t getCurrentSize() const; // Get the current number of elements in buffer size_t getCurrentSize() const; // Get the current number of elements in buffer
size_t getFirstIdx() const; // Get the index of oldest value in buffer size_t getFirstIdx() const; // Get the index of oldest value in buffer
size_t getLastIdx() const; // Get the index of newest value in buffer size_t getLastIdx() const; // Get the index of newest value in buffer
bool isEmpty() const; // Check if buffer is empty bool isEmpty() const; // Check if buffer is empty
bool isFull() const; // Check if buffer is full bool isFull() const; // Check if buffer is full
T getMinVal() const; // Get lowest possible value for buffer double getMinVal() const; // Get lowest possible value for buffer
T getMaxVal() const; // Get highest possible value for buffer; used for unset/invalid buffer data double getMaxVal() const; // Get highest possible value for buffer; used for unset/invalid buffer data
void clear(); // Clear buffer void clear(); // Clear buffer
void resize(size_t size); // Delete buffer and set new size void resize(size_t size); // Delete buffer and set new size
T operator[](size_t index) const; // Operator[] for convenient access (same as get()) double operator[](size_t index) const; // Operator[] for convenient access (same as get())
std::vector<T> getAllValues() const; // Get all current values as a vector std::vector<double> getAllValues() const; // Get all current values in native buffer format as a vector
std::vector<double> getAllValues(size_t amount) const; // Get last <amount> values in native buffer format as a vector
}; };
#include "OBPRingBuffer.tpp" #include "OBPRingBuffer.tpp"

View File

@@ -1,14 +1,21 @@
#include "OBPRingBuffer.h" #include "OBPRingBuffer.h"
#include <algorithm>
#include <limits>
#include <cmath>
template <typename T> template <typename T>
void RingBuffer<T>::initCommon() { void RingBuffer<T>::initCommon()
{
MIN_VAL = std::numeric_limits<T>::lowest(); MIN_VAL = std::numeric_limits<T>::lowest();
MAX_VAL = std::numeric_limits<T>::max(); MAX_VAL = std::numeric_limits<T>::max();
dblMIN_VAL = static_cast<double>(MIN_VAL);
dblMAX_VAL = static_cast<double>(MAX_VAL);
dataName = ""; dataName = "";
dataFmt = ""; dataFmt = "";
updFreq = -1; updFreq = -1;
smallest = MIN_VAL; mltplr = 1;
largest = MAX_VAL; smallest = dblMIN_VAL;
largest = dblMAX_VAL;
bufLocker = xSemaphoreCreateMutex(); bufLocker = xSemaphoreCreateMutex();
} }
@@ -35,24 +42,27 @@ RingBuffer<T>::RingBuffer(size_t size)
, is_Full(false) , is_Full(false)
{ {
initCommon(); initCommon();
buffer.reserve(size);
buffer.resize(size, MAX_VAL); // MAX_VAL indicate invalid values buffer.resize(size, MAX_VAL); // MAX_VAL indicate invalid values
} }
// Specify meta data of buffer content // Specify meta data of buffer content
template <typename T> template <typename T>
void RingBuffer<T>::setMetaData(String name, String format, int updateFrequency, T minValue, T maxValue) void RingBuffer<T>::setMetaData(String name, String format, int updateFrequency, double multiplier, double minValue, double maxValue)
{ {
GWSYNCHRONIZED(&bufLocker); GWSYNCHRONIZED(&bufLocker);
dataName = name; dataName = name;
dataFmt = format; dataFmt = format;
updFreq = updateFrequency; updFreq = updateFrequency;
smallest = std::max(MIN_VAL, minValue); mltplr = multiplier;
largest = std::min(MAX_VAL, maxValue); smallest = std::max(dblMIN_VAL, minValue);
largest = std::min(dblMAX_VAL, maxValue);
} }
// Get meta data of buffer content // Get meta data of buffer content
template <typename T> template <typename T>
bool RingBuffer<T>::getMetaData(String& name, String& format, int& updateFrequency, T& minValue, T& maxValue) bool RingBuffer<T>::getMetaData(String& name, String& format, int& updateFrequency, double& multiplier, double& minValue, double& maxValue)
{ {
if (dataName == "" || dataFmt == "" || updFreq == -1) { if (dataName == "" || dataFmt == "" || updFreq == -1) {
return false; // Meta data not set return false; // Meta data not set
@@ -62,6 +72,7 @@ bool RingBuffer<T>::getMetaData(String& name, String& format, int& updateFrequen
name = dataName; name = dataName;
format = dataFmt; format = dataFmt;
updateFrequency = updFreq; updateFrequency = updFreq;
multiplier = mltplr;
minValue = smallest; minValue = smallest;
maxValue = largest; maxValue = largest;
return true; return true;
@@ -97,13 +108,13 @@ String RingBuffer<T>::getFormat() const
// Add a new value to buffer // Add a new value to buffer
template <typename T> template <typename T>
void RingBuffer<T>::add(const T& value) void RingBuffer<T>::add(const double& value)
{ {
GWSYNCHRONIZED(&bufLocker); GWSYNCHRONIZED(&bufLocker);
if (value < smallest || value > largest) { if (value < smallest || value > largest) {
buffer[head] = MAX_VAL; // Store MAX_VAL if value is out of range buffer[head] = MAX_VAL; // Store MAX_VAL if value is out of range
} else { } else {
buffer[head] = value; buffer[head] = static_cast<T>(std::round(value * mltplr));
} }
last = head; last = head;
@@ -115,63 +126,63 @@ void RingBuffer<T>::add(const T& value)
is_Full = true; is_Full = true;
} }
} }
// Serial.printf("Ringbuffer: value %.3f, multiplier: %.1f, buffer: %d\n", value, mltplr, buffer[head]);
head = (head + 1) % capacity; head = (head + 1) % capacity;
} }
// Get value at specific position (0-based index from oldest to newest) // Get value at specific position (0-based index from oldest to newest)
template <typename T> template <typename T>
T RingBuffer<T>::get(size_t index) const double RingBuffer<T>::get(size_t index) const
{ {
GWSYNCHRONIZED(&bufLocker); GWSYNCHRONIZED(&bufLocker);
if (isEmpty() || index < 0 || index >= count) { if (isEmpty() || index < 0 || index >= count) {
return MAX_VAL; return dblMAX_VAL;
} }
size_t realIndex = (first + index) % capacity; size_t realIndex = (first + index) % capacity;
return buffer[realIndex]; return static_cast<double>(buffer[realIndex] / mltplr);
} }
// Operator[] for convenient access (same as get()) // Operator[] for convenient access (same as get())
template <typename T> template <typename T>
T RingBuffer<T>::operator[](size_t index) const double RingBuffer<T>::operator[](size_t index) const
{ {
return get(index); return get(index);
} }
// Get the first (oldest) value in the buffer // Get the first (oldest) value in the buffer
template <typename T> template <typename T>
T RingBuffer<T>::getFirst() const double RingBuffer<T>::getFirst() const
{ {
if (isEmpty()) { if (isEmpty()) {
return MAX_VAL; return dblMAX_VAL;
} }
return get(0); return get(0);
} }
// Get the last (newest) value in the buffer // Get the last (newest) value in the buffer
template <typename T> template <typename T>
T RingBuffer<T>::getLast() const double RingBuffer<T>::getLast() const
{ {
if (isEmpty()) { if (isEmpty()) {
return MAX_VAL; return dblMAX_VAL;
} }
return get(count - 1); return get(count - 1);
} }
// Get the lowest value in the buffer // Get the lowest value in the buffer
template <typename T> template <typename T>
T RingBuffer<T>::getMin() const double RingBuffer<T>::getMin() const
{ {
if (isEmpty()) { if (isEmpty()) {
return MAX_VAL; return dblMAX_VAL;
} }
T minVal = MAX_VAL; double minVal = dblMAX_VAL;
T value; double value;
for (size_t i = 0; i < count; i++) { for (size_t i = 0; i < count; i++) {
value = get(i); value = get(i);
if (value < minVal && value != MAX_VAL) { if (value < minVal && value != dblMAX_VAL) {
minVal = value; minVal = value;
} }
} }
@@ -180,19 +191,19 @@ T RingBuffer<T>::getMin() const
// Get minimum value of the last <amount> values of buffer // Get minimum value of the last <amount> values of buffer
template <typename T> template <typename T>
T RingBuffer<T>::getMin(size_t amount) const double RingBuffer<T>::getMin(size_t amount) const
{ {
if (isEmpty() || amount <= 0) { if (isEmpty() || amount <= 0) {
return MAX_VAL; return dblMAX_VAL;
} }
if (amount > count) if (amount > count)
amount = count; amount = count;
T minVal = MAX_VAL; double minVal = dblMAX_VAL;
T value; double value;
for (size_t i = 0; i < amount; i++) { for (size_t i = 0; i < amount; i++) {
value = get(count - 1 - i); value = get(count - 1 - i);
if (value < minVal && value != MAX_VAL) { if (value < minVal && value != dblMAX_VAL) {
minVal = value; minVal = value;
} }
} }
@@ -201,75 +212,81 @@ T RingBuffer<T>::getMin(size_t amount) const
// Get the highest value in the buffer // Get the highest value in the buffer
template <typename T> template <typename T>
T RingBuffer<T>::getMax() const double RingBuffer<T>::getMax() const
{ {
if (isEmpty()) { if (isEmpty()) {
return MAX_VAL; return dblMAX_VAL;
} }
T maxVal = MIN_VAL; double maxVal = dblMIN_VAL;
T value; double value;
for (size_t i = 0; i < count; i++) { for (size_t i = 0; i < count; i++) {
value = get(i); value = get(i);
if (value > maxVal && value != MAX_VAL) { if (value > maxVal && value != dblMAX_VAL) {
maxVal = value; maxVal = value;
} }
} }
if (maxVal == dblMIN_VAL) { // no change of initial value -> buffer has only invalid values (MAX_VAL)
maxVal = dblMAX_VAL;
}
return maxVal; return maxVal;
} }
// Get maximum value of the last <amount> values of buffer // Get maximum value of the last <amount> values of buffer
template <typename T> template <typename T>
T RingBuffer<T>::getMax(size_t amount) const double RingBuffer<T>::getMax(size_t amount) const
{ {
if (isEmpty() || amount <= 0) { if (isEmpty() || amount <= 0) {
return MAX_VAL; return dblMAX_VAL;
} }
if (amount > count) if (amount > count)
amount = count; amount = count;
T maxVal = MIN_VAL; double maxVal = dblMIN_VAL;
T value; double value;
for (size_t i = 0; i < amount; i++) { for (size_t i = 0; i < amount; i++) {
value = get(count - 1 - i); value = get(count - 1 - i);
if (value > maxVal && value != MAX_VAL) { if (value > maxVal && value != dblMAX_VAL) {
maxVal = value; maxVal = value;
} }
} }
if (maxVal == dblMIN_VAL) { // no change of initial value -> buffer has only invalid values (MAX_VAL)
maxVal = dblMAX_VAL;
}
return maxVal; return maxVal;
} }
// Get mid value between <min> and <max> value in the buffer // Get mid value between <min> and <max> value in the buffer
template <typename T> template <typename T>
T RingBuffer<T>::getMid() const double RingBuffer<T>::getMid() const
{ {
if (isEmpty()) { if (isEmpty()) {
return MAX_VAL; return dblMAX_VAL;
} }
return (getMin() + getMax()) / static_cast<T>(2); return (getMin() + getMax()) / 2;
} }
// Get mid value between <min> and <max> value of the last <amount> values of buffer // Get mid value between <min> and <max> value of the last <amount> values of buffer
template <typename T> template <typename T>
T RingBuffer<T>::getMid(size_t amount) const double RingBuffer<T>::getMid(size_t amount) const
{ {
if (isEmpty() || amount <= 0) { if (isEmpty() || amount <= 0) {
return MAX_VAL; return dblMAX_VAL;
} }
if (amount > count) if (amount > count)
amount = count; amount = count;
return (getMin(amount) + getMax(amount)) / static_cast<T>(2); return (getMin(amount) + getMax(amount)) / 2;
} }
// Get the median value in the buffer // Get the median value in the buffer
template <typename T> template <typename T>
T RingBuffer<T>::getMedian() const double RingBuffer<T>::getMedian() const
{ {
if (isEmpty()) { if (isEmpty()) {
return MAX_VAL; return dblMAX_VAL;
} }
// Create a temporary vector with current valid elements // Create a temporary vector with current valid elements
@@ -285,20 +302,20 @@ T RingBuffer<T>::getMedian() const
if (count % 2 == 1) { if (count % 2 == 1) {
// Odd number of elements // Odd number of elements
return temp[count / 2]; return static_cast<double>(temp[count / 2]);
} else { } else {
// Even number of elements - return average of middle two // Even number of elements - return average of middle two
// Note: For integer types, this truncates. For floating point, it's exact. // Note: For integer types, this truncates. For floating point, it's exact.
return (temp[count / 2 - 1] + temp[count / 2]) / 2; return static_cast<double>((temp[count / 2 - 1] + temp[count / 2]) / 2);
} }
} }
// Get the median value of the last <amount> values of buffer // Get the median value of the last <amount> values of buffer
template <typename T> template <typename T>
T RingBuffer<T>::getMedian(size_t amount) const double RingBuffer<T>::getMedian(size_t amount) const
{ {
if (isEmpty() || amount <= 0) { if (isEmpty() || amount <= 0) {
return MAX_VAL; return dblMAX_VAL;
} }
if (amount > count) if (amount > count)
amount = count; amount = count;
@@ -308,7 +325,7 @@ T RingBuffer<T>::getMedian(size_t amount) const
temp.reserve(amount); temp.reserve(amount);
for (size_t i = 0; i < amount; i++) { for (size_t i = 0; i < amount; i++) {
temp.push_back(get(i)); temp.push_back(get(count - 1 - i));
} }
// Sort to find median // Sort to find median
@@ -316,11 +333,11 @@ T RingBuffer<T>::getMedian(size_t amount) const
if (amount % 2 == 1) { if (amount % 2 == 1) {
// Odd number of elements // Odd number of elements
return temp[amount / 2]; return static_cast<double>(temp[amount / 2]);
} else { } else {
// Even number of elements - return average of middle two // Even number of elements - return average of middle two
// Note: For integer types, this truncates. For floating point, it's exact. // Note: For integer types, this truncates. For floating point, it's exact.
return (temp[amount / 2 - 1] + temp[amount / 2]) / 2; return static_cast<double>((temp[amount / 2 - 1] + temp[amount / 2]) / 2);
} }
} }
@@ -368,16 +385,16 @@ bool RingBuffer<T>::isFull() const
// Get lowest possible value for buffer // Get lowest possible value for buffer
template <typename T> template <typename T>
T RingBuffer<T>::getMinVal() const double RingBuffer<T>::getMinVal() const
{ {
return MIN_VAL; return dblMIN_VAL;
} }
// Get highest possible value for buffer; used for unset/invalid buffer data // Get highest possible value for buffer; used for unset/invalid buffer data
template <typename T> template <typename T>
T RingBuffer<T>::getMaxVal() const double RingBuffer<T>::getMaxVal() const
{ {
return MAX_VAL; return dblMAX_VAL;
} }
// Clear buffer // Clear buffer
@@ -405,14 +422,15 @@ void RingBuffer<T>::resize(size_t newSize)
is_Full = false; is_Full = false;
buffer.clear(); buffer.clear();
buffer.reserve(newSize);
buffer.resize(newSize, MAX_VAL); buffer.resize(newSize, MAX_VAL);
} }
// Get all current values as a vector // Get all current values in native buffer format as a vector
template <typename T> template <typename T>
std::vector<T> RingBuffer<T>::getAllValues() const std::vector<double> RingBuffer<T>::getAllValues() const
{ {
std::vector<T> result; std::vector<double> result;
result.reserve(count); result.reserve(count);
for (size_t i = 0; i < count; i++) { for (size_t i = 0; i < count; i++) {
@@ -421,3 +439,24 @@ std::vector<T> RingBuffer<T>::getAllValues() const
return result; return result;
} }
// Get last <amount> values in native buffer format as a vector
template <typename T>
std::vector<double> RingBuffer<T>::getAllValues(size_t amount) const
{
std::vector<double> result;
if (isEmpty() || amount <= 0) {
return result;
}
if (amount > count)
amount = count;
result.reserve(amount);
for (size_t i = 0; i < amount; i++) {
result.push_back(get(count - 1 - i));
}
return result;
}

View File

@@ -457,6 +457,7 @@ void sensorTask(void *param){
// Get current RTC date and time all 500ms // Get current RTC date and time all 500ms
if (millis() > starttime12 + 500) { if (millis() > starttime12 + 500) {
starttime12 = millis(); starttime12 = millis();
// Send date and time from RTC chip if GPS not ready
if (rtcOn == "DS1388" && RTC_ready) { if (rtcOn == "DS1388" && RTC_ready) {
DateTime dt = ds1388.now(); DateTime dt = ds1388.now();
sensors.rtcTime.tm_year = dt.year() - 1900; // Save values in SensorData sensors.rtcTime.tm_year = dt.year() - 1900; // Save values in SensorData
@@ -496,7 +497,31 @@ void sensorTask(void *param){
} }
} }
// Send supply voltage value all 1s // Send 1Wire data for all temperature sensors to N2K all 2s
if(millis() > starttime13 + 2000 && String(oneWireOn) == "DS18B20" && oneWire_ready == true){
starttime13 = millis();
float tempC;
ds18b20.requestTemperatures(); // Collect all temperature values (max.8)
for(int i=0;i<numberOfDevices; i++){
// Send only one 1Wire data per loop step (time reduction)
if(i == loopCounter % numberOfDevices){
if(ds18b20.getAddress(tempDeviceAddress, i)){
// Read temperature value in Celsius
tempC = ds18b20.getTempC(tempDeviceAddress);
}
// Send to NMEA200 bus for each sensor with instance number
if(!isnan(tempC)){
sensors.onewireTemp[i] = tempC; // Save values in SensorData
api->getLogger()->logDebug(GwLog::DEBUG,"DS18B20-%1d Temp: %.1f",i,tempC);
SetN2kPGN130316(N2kMsg, 0, i, N2kts_OutsideTemperature, CToKelvin(tempC), N2kDoubleNA);
api->sendN2kMessage(N2kMsg);
}
}
}
loopCounter++;
}
// Send supply voltage value to N2K all 1s
if(millis() > starttime5 + 1000 && String(powsensor1) == "off"){ if(millis() > starttime5 + 1000 && String(powsensor1) == "off"){
starttime5 = millis(); starttime5 = millis();
float rawVoltage = 0; // Default value float rawVoltage = 0; // Default value
@@ -566,7 +591,7 @@ void sensorTask(void *param){
#endif #endif
} }
// Send data from environment sensor all 2s // Send data from environment sensor to N2K all 2s
if(millis() > starttime6 + 2000){ if(millis() > starttime6 + 2000){
starttime6 = millis(); starttime6 = millis();
@@ -636,7 +661,7 @@ void sensorTask(void *param){
} }
} }
// Send rotation angle all 500ms // Send rotation angle to N2K all 500ms
if(millis() > starttime7 + 500){ if(millis() > starttime7 + 500){
starttime7 = millis(); starttime7 = millis();
double rotationAngle=0; double rotationAngle=0;
@@ -684,7 +709,7 @@ void sensorTask(void *param){
} }
} }
// Send battery power value all 1s // Send battery power value to N2K all 1s
if(millis() > starttime8 + 1000 && (String(powsensor1) == "INA219" || String(powsensor1) == "INA226")){ if(millis() > starttime8 + 1000 && (String(powsensor1) == "INA219" || String(powsensor1) == "INA226")){
starttime8 = millis(); starttime8 = millis();
if(String(powsensor1) == "INA226" && INA226_1_ready == true){ if(String(powsensor1) == "INA226" && INA226_1_ready == true){
@@ -726,7 +751,7 @@ void sensorTask(void *param){
} }
} }
// Send solar power value all 1s // Send solar power value to N2K all 1s
if(millis() > starttime9 + 1000 && (String(powsensor2) == "INA219" || String(powsensor2) == "INA226")){ if(millis() > starttime9 + 1000 && (String(powsensor2) == "INA219" || String(powsensor2) == "INA226")){
starttime9 = millis(); starttime9 = millis();
if(String(powsensor2) == "INA226" && INA226_2_ready == true){ if(String(powsensor2) == "INA226" && INA226_2_ready == true){
@@ -756,7 +781,7 @@ void sensorTask(void *param){
} }
} }
// Send generator power value all 1s // Send generator power value to N2K all 1s
if(millis() > starttime10 + 1000 && (String(powsensor3) == "INA219" || String(powsensor3) == "INA226")){ if(millis() > starttime10 + 1000 && (String(powsensor3) == "INA219" || String(powsensor3) == "INA226")){
starttime10 = millis(); starttime10 = millis();
if(String(powsensor3) == "INA226" && INA226_3_ready == true){ if(String(powsensor3) == "INA226" && INA226_3_ready == true){

808
lib/obp60task/OBPcharts.cpp Normal file
View File

@@ -0,0 +1,808 @@
// Function lib for display of boat data in various chart formats
#include "OBPcharts.h"
#include "OBPDataOperations.h"
#include "OBPRingBuffer.h"
std::map<String, ChartProps> Chart::dfltChrtDta = {
{ "formatWind", { 60.0 * DEG_TO_RAD, 10.0 * DEG_TO_RAD } }, // default course range 60 degrees
{ "formatCourse", { 60.0 * DEG_TO_RAD, 10.0 * DEG_TO_RAD } }, // default course range 60 degrees
{ "formatKnots", { 7.71, 2.56 } }, // default speed range in m/s
{ "formatDepth", { 15.0, 5.0 } }, // default depth range in m
{ "kelvinToC", { 30.0, 5.0 } } // default temp range in °C/K
};
// --- Class Chart ---------------
// Chart - object holding the actual chart, incl. data buffer and format definition
// Parameters: <dataBuf> the history data buffer for the chart
// <dfltRng> default range of chart, e.g. 30 = [0..30]
// <common> common program data; required for logger and color data
// <useSimuData> flag to indicate if simulation data is active
Chart::Chart(RingBuffer<uint16_t>& dataBuf, double dfltRng, CommonData& common, bool useSimuData)
: dataBuf(dataBuf)
, dfltRng(dfltRng)
, commonData(&common)
, useSimuData(useSimuData)
{
logger = commonData->logger;
fgColor = commonData->fgcolor;
bgColor = commonData->bgcolor;
// display dimensions (avoid calling width()/height() on incomplete LGFX type)
dWidth = epd->width();
dHeight = epd->height();
dataBuf.getMetaData(dbName, dbFormat);
dbMIN_VAL = dataBuf.getMinVal();
dbMAX_VAL = dataBuf.getMaxVal();
bufSize = dataBuf.getCapacity();
// Initialize chart data format; shorter version of standard format indicator
if (dbFormat == "formatCourse" || dbFormat == "formatWind" || dbFormat == "formatRot") {
chrtDataFmt = WIND; // Chart is showing data of course / wind <degree> format
} else if (dbFormat == "formatRot") {
chrtDataFmt = ROTATION; // Chart is showing data of rotational <degree> format
} else if (dbFormat == "formatKnots") {
chrtDataFmt = SPEED; // Chart is showing data of speed or windspeed format
} else if (dbFormat == "formatDepth") {
chrtDataFmt = DEPTH; // Chart ist showing data of <depth> format
} else if (dbFormat == "kelvinToC") {
chrtDataFmt = TEMPERATURE; // Chart ist showing data of <temp> format
} else {
chrtDataFmt = OTHER; // Chart is showing any other data format
}
// "0" value is the same for any data format but for user defined temperature format
zeroValue = 0.0;
if (chrtDataFmt == TEMPERATURE) {
tempFormat = commonData->config->getString(commonData->config->tempFormat); // [K|°C|°F]
if (tempFormat == "K") {
zeroValue = 0.0;
} else if (tempFormat == "C") {
zeroValue = 273.15;
} else if (tempFormat == "F") {
zeroValue = 255.37;
}
}
// Read default range and range step for this chart type
if (dfltChrtDta.count(dbFormat)) {
dfltRng = dfltChrtDta[dbFormat].range;
rngStep = dfltChrtDta[dbFormat].step;
} else {
dfltRng = 15.0;
rngStep = 5.0;
}
// Initialize chart range values
chrtMin = zeroValue;
chrtMax = chrtMin + dfltRng;
chrtMid = (chrtMin + chrtMax) / 2;
chrtRng = dfltRng;
recalcRngMid = true; // initialize <chrtMid> and chart borders on first screen call
LOG_DEBUG(GwLog::DEBUG, "Chart Init: dWidth: %d, dHeight: %d, timAxis: %d, valAxis: %d, cRoot {x,y}: %d, %d, dbname: %s, rngStep: %.4f, chrtDataFmt: %d",
dWidth, dHeight, timAxis, valAxis, cRoot.x, cRoot.y, dbName, rngStep, chrtDataFmt);
};
Chart::~Chart()
{
}
// Perform all actions to draw chart
// Parameters: <chrtDir>: chart timeline direction: 'H' = horizontal, 'V' = vertical
// <chrtSz>: chart size: [0] = full size, [1] = half size left/top, [2] half size right/bottom
// <chrtIntv>: chart timeline interval
// <prntName>; print data name on horizontal half chart [true|false]
// <showCurrValue>: print current boat data value [true|false]
// <currValue>: current boat data value; used only for test on valid data
void Chart::showChrt(char chrtDir, int8_t chrtSz, const int8_t chrtIntv, bool prntName, bool showCurrValue, GwApi::BoatValue currValue)
{
if (!setChartDimensions(chrtDir, chrtSz)) {
return; // wrong chart dimension parameters
}
drawChrt(chrtDir, chrtIntv, currValue);
drawChrtTimeAxis(chrtDir, chrtSz, chrtIntv);
drawChrtValAxis(chrtDir, chrtSz, prntName);
if (!bufDataValid) { // No valid data available
prntNoValidData(chrtDir);
return;
}
if (showCurrValue) { // show latest value from history buffer; this should be the most current one
currValue.value = dataBuf.getLast();
currValue.valid = currValue.value != dbMAX_VAL;
prntCurrValue(chrtDir, currValue);
}
}
// define dimensions and start points for chart
bool Chart::setChartDimensions(const char direction, const int8_t size)
{
if ((direction != HORIZONTAL && direction != VERTICAL) || (size < 0 || size > 2)) {
LOG_DEBUG(GwLog::ERROR, "obp60:setChartDimensions %s: wrong parameters", dataBuf.getName());
return false;
}
if (direction == HORIZONTAL) {
// horizontal chart timeline direction
timAxis = dWidth - 1;
switch (size) {
case 0:
valAxis = dHeight - top - bottom;
cRoot = { 0, top - 1 };
break;
case 1:
valAxis = (dHeight - top - bottom) / 2 - hGap;
cRoot = { 0, top - 1 };
break;
case 2:
valAxis = (dHeight - top - bottom) / 2 - hGap;
cRoot = { 0, top + (valAxis + hGap) + hGap - 1 };
break;
}
} else if (direction == VERTICAL) {
// vertical chart timeline direction
timAxis = dHeight - top - bottom;
switch (size) {
case 0:
valAxis = dWidth - 1;
cRoot = { 0, top - 1 };
break;
case 1:
valAxis = dWidth / 2 - vGap;
cRoot = { 0, top - 1 };
break;
case 2:
valAxis = dWidth / 2 - vGap;
cRoot = { dWidth / 2 + vGap - 1, top - 1 };
break;
}
}
//LOG_DEBUG(GwLog::DEBUG, "obp60:setChartDimensions %s: direction: %c, size: %d, dWidth: %d, dHeight: %d, timAxis: %d, valAxis: %d, cRoot{%d, %d}, top: %d, bottom: %d, hGap: %d, vGap: %d",
// dataBuf.getName(), direction, size, dWidth, dHeight, timAxis, valAxis, cRoot.x, cRoot.y, top, bottom, hGap, vGap);
return true;
}
// draw chart
void Chart::drawChrt(const char chrtDir, const int8_t chrtIntv, GwApi::BoatValue& currValue)
{
double chrtScale; // Scale for data values in pixels per value
getBufferStartNSize(chrtIntv);
// LOG_DEBUG(GwLog::DEBUG, "Chart:drawChart: min: %.1f, mid: %.1f, max: %.1f, rng: %.1f", chrtMin, chrtMid, chrtMax, chrtRng);
calcChrtBorders(chrtMin, chrtMid, chrtMax, chrtRng);
chrtScale = double(valAxis) / chrtRng; // Chart scale: pixels per value step
// LOG_DEBUG(GwLog::DEBUG, "Chart:drawChart: min: %.1f, mid: %.1f, max: %.1f, rng: %.1f", chrtMin, chrtMid, chrtMax, chrtRng);
// Do we have valid buffer data?
if (dataBuf.getMax() == dbMAX_VAL) { // only <MAX_VAL> values in buffer -> no valid wind data available
bufDataValid = false;
return;
} else if (currValue.valid || useSimuData) { // latest boat data valid or simulation mode
numNoData = 0; // reset data error counter
bufDataValid = true;
} else { // currently no valid data
numNoData++;
bufDataValid = true;
if (numNoData > THRESHOLD_NO_DATA) { // If more than 4 invalid values in a row, flag for invalid data
bufDataValid = false;
return;
}
}
drawChartLines(chrtDir, chrtIntv, chrtScale);
}
// Identify buffer size and buffer start position for chart
void Chart::getBufferStartNSize(const int8_t chrtIntv)
{
count = dataBuf.getCurrentSize();
currIdx = dataBuf.getLastIdx();
numAddedBufVals = (currIdx - lastAddedIdx + bufSize) % bufSize; // Number of values added to buffer since last display
if (chrtIntv != oldChrtIntv || count == 1) {
// new data interval selected by user; this is only x * 230 values instead of 240 seconds (4 minutes) per interval step
numBufVals = min(count, (timAxis - MIN_FREE_VALUES) * chrtIntv); // keep free or release MIN_FREE_VALUES on chart for plotting of new values
bufStart = max(0, count - numBufVals);
lastAddedIdx = currIdx;
oldChrtIntv = chrtIntv;
} else {
numBufVals = numBufVals + numAddedBufVals;
lastAddedIdx = currIdx;
if (count == bufSize) {
bufStart = max(0, bufStart - numAddedBufVals);
}
}
}
// check and adjust chart range and set range borders and range middle
void Chart::calcChrtBorders(double& rngMin, double& rngMid, double& rngMax, double& rng)
{
if (chrtDataFmt == WIND || chrtDataFmt == ROTATION) {
if (chrtDataFmt == ROTATION) {
// if chart data is of type 'rotation', we want to have <rndMid> always to be '0'
rngMid = 0;
} else { // WIND: Chart data is of type 'course' or 'wind'
// initialize <rngMid> if data buffer has just been started filling
if ((count == 1 && rngMid == 0) || rngMid == dbMAX_VAL) {
recalcRngMid = true;
}
if (recalcRngMid) {
// Set rngMid
rngMid = dataBuf.getMid(numBufVals);
if (rngMid == dbMAX_VAL) {
rngMid = 0;
} else {
rngMid = std::round(rngMid / rngStep) * rngStep; // Set new center value; round to next <rngStep> value
// Check if range between 'min' and 'max' is > 180° or crosses '0'
rngMin = dataBuf.getMin(numBufVals);
rngMax = dataBuf.getMax(numBufVals);
rng = (rngMax >= rngMin ? rngMax - rngMin : M_TWOPI - rngMin + rngMax);
rng = std::max(rng, dfltRng); // keep at least default chart range
if (rng > M_PI) { // If wind range > 180°, adjust wndCenter to smaller wind range end
rngMid = WindUtils::to2PI(rngMid + M_PI);
}
}
recalcRngMid = false; // Reset flag for <rngMid> determination
// LOG_DEBUG(GwLog::DEBUG, "calcChrtRange: rngMin: %.1f°, rngMid: %.1f°, rngMax: %.1f°, rng: %.1f°, rngStep: %.1f°", rngMin * RAD_TO_DEG, rngMid * RAD_TO_DEG, rngMax * RAD_TO_DEG,
// rng * RAD_TO_DEG, rngStep * RAD_TO_DEG);
}
}
// check and adjust range between left, mid, and right chart limit
double halfRng = rng / 2.0; // we calculate with range between <rngMid> and edges
double tmpRng = getAngleRng(rngMid, numBufVals);
tmpRng = (tmpRng == dbMAX_VAL ? 0 : std::ceil(tmpRng / rngStep) * rngStep);
// LOG_DEBUG(GwLog::DEBUG, "calcChrtBorders: tmpRng: %.1f°, halfRng: %.1f°", tmpRng * RAD_TO_DEG, halfRng * RAD_TO_DEG);
if (tmpRng > halfRng) { // expand chart range to new value
halfRng = tmpRng;
}
else if (tmpRng + rngStep < halfRng) { // Contract chart range for higher resolution if possible
halfRng = std::max(dfltRng / 2.0, tmpRng);
}
rngMin = WindUtils::to2PI(rngMid - halfRng);
rngMax = (halfRng < M_PI ? rngMid + halfRng : rngMid + halfRng - (M_TWOPI / 360)); // if chart range is 360°, then make <rngMax> 1° smaller than <rngMin>
rngMax = WindUtils::to2PI(rngMax);
rng = halfRng * 2.0;
// LOG_DEBUG(GwLog::DEBUG, "calcChrtBorders: rngMin: %.1f°, rngMid: %.1f°, rngMax: %.1f°, tmpRng: %.1f°, rng: %.1f°, rngStep: %.1f°", rngMin * RAD_TO_DEG, rngMid * RAD_TO_DEG, rngMax * RAD_TO_DEG,
// tmpRng * RAD_TO_DEG, rng * RAD_TO_DEG, rngStep * RAD_TO_DEG);
} else { // chart data is of any other type
double currMinVal = dataBuf.getMin(numBufVals);
double currMaxVal = dataBuf.getMax(numBufVals);
if (currMinVal == dbMAX_VAL || currMaxVal == dbMAX_VAL) {
return; // no valid data
}
// check if current chart border have to be adjusted
if (currMinVal < rngMin || (currMinVal > (rngMin + rngStep))) { // decrease rngMin if required or increase if lowest value is higher than old rngMin
rngMin = std::floor(currMinVal / rngStep) * rngStep; // align low range to lowest buffer value and nearest range interval
}
if ((currMaxVal > rngMax) || (currMaxVal < (rngMax - rngStep))) { // increase rngMax if required or decrease if lowest value is lower than old rngMax
rngMax = std::ceil(currMaxVal / rngStep) * rngStep;
}
// Chart range starts at least at '0' if minimum data value allows it
if (rngMin > zeroValue && dbMIN_VAL <= zeroValue) {
rngMin = zeroValue;
}
// ensure minimum chart range in user format
if ((rngMax - rngMin) < dfltRng) {
rngMax = rngMin + dfltRng;
}
rngMid = (rngMin + rngMax) / 2.0;
rng = rngMax - rngMin;
// LOG_DEBUG(GwLog::DEBUG, "calcChrtRange-end: currMinVal: %.1f, currMaxVal: %.1f, rngMin: %.1f, rngMid: %.1f, rngMax: %.1f, rng: %.1f, rngStep: %.1f, zeroValue: %.1f, dbMIN_VAL: %.1f",
// currMinVal, currMaxVal, rngMin, rngMid, rngMax, rng, rngStep, zeroValue, dbMIN_VAL);
}
}
// Draw chart graph
void Chart::drawChartLines(const char direction, const int8_t chrtIntv, const double chrtScale)
{
double chrtVal; // Current data value
Pos point, prevPoint; // current and previous chart point
for (int i = 0; i < (numBufVals / chrtIntv); i++) {
chrtVal = dataBuf.get(bufStart + (i * chrtIntv)); // show the latest wind values in buffer; keep 1st value constant in a rolling buffer
if (chrtVal == dbMAX_VAL) {
chrtPrevVal = dbMAX_VAL;
} else {
point = setCurrentChartPoint(i, direction, chrtVal, chrtScale);
// if (i >= (numBufVals / chrtIntv) - 5) // log chart data of 1 line (adjust for test purposes)
// LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Chart: i: %d, chrtVal: %.2f, chrtMin: %.2f, {x,y} {%d,%d}", i, chrtVal, chrtMin, x, y);
if ((i == 0) || (chrtPrevVal == dbMAX_VAL)) {
// just a dot for 1st chart point or after some invalid values
prevPoint = point;
} else if (chrtDataFmt == WIND || chrtDataFmt == ROTATION) {
// cross borders check for degree values; shift values to [-PI..0..PI]; when crossing borders, range is 2x PI degrees
double normCurrVal = WindUtils::to2PI(chrtVal - chrtMin);
double normPrevVal = WindUtils::to2PI(chrtPrevVal - chrtMin);
// Check if pixel positions are far apart (crossing chart boundary); happens when one value is near chrtMax and the other near chrtMin
bool crossedBorders = std::abs(normCurrVal - normPrevVal) > (chrtRng / 2.0);
if (crossedBorders) { // If current value crosses chart borders compared to previous value, split line
// LOG_DEBUG(GwLog::DEBUG, "PageWindPlot Chart: crossedBorders: %d, chrtVal: %.2f, chrtPrevVal: %.2f", crossedBorders, chrtVal, chrtPrevVal);
bool wrappingFromHighToLow = normCurrVal < normPrevVal; // Determine which edge we're crossing
if (direction == HORIZONTAL) {
int ySplit = wrappingFromHighToLow ? (cRoot.y + valAxis) : cRoot.y;
drawBoldLine(prevPoint.x, prevPoint.y, point.x, ySplit);
prevPoint.y = wrappingFromHighToLow ? cRoot.y : (cRoot.y + valAxis);
} else { // vertical chart
int xSplit = wrappingFromHighToLow ? (cRoot.x + valAxis) : cRoot.x;
drawBoldLine(prevPoint.x, prevPoint.y, xSplit, point.y);
prevPoint.x = wrappingFromHighToLow ? cRoot.x : (cRoot.x + valAxis);
}
}
}
if (chrtDataFmt == DEPTH) {
if (direction == HORIZONTAL) { // horizontal chart
drawBoldLine(point.x, point.y, point.x, cRoot.y + valAxis);
} else { // vertical chart
drawBoldLine(point.x, point.y, cRoot.x + valAxis, point.y);
}
} else {
drawBoldLine(prevPoint.x, prevPoint.y, point.x, point.y);
}
chrtPrevVal = chrtVal;
prevPoint = point;
}
// Reaching chart area top end
if (i >= timAxis - 1) {
oldChrtIntv = 0; // force reset of buffer start and number of values to show in next display loop
if (chrtDataFmt == WIND) { // degree of course or wind
recalcRngMid = true;
LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: chart end: timAxis: %d, i: %d, bufStart: %d, numBufVals: %d, recalcRngCntr: %d", timAxis, i, bufStart, numBufVals, recalcRngMid);
}
break;
}
taskYIELD(); // we run for 50-150ms; be polite to other tasks with same priority
}
}
// Set current chart point to draw
Pos Chart::setCurrentChartPoint(const int i, const char direction, const double chrtVal, const double chrtScale)
{
Pos currentPoint;
if (direction == HORIZONTAL) {
currentPoint.x = cRoot.x + i; // Position in chart area
if (chrtDataFmt == WIND || chrtDataFmt == ROTATION) { // degree type value
currentPoint.y = cRoot.y + static_cast<int>((WindUtils::to2PI(chrtVal - chrtMin) * chrtScale) + 0.5); // calculate chart point and round
} else if (chrtDataFmt == SPEED or chrtDataFmt == TEMPERATURE) { // speed or temperature data format -> print low values at bottom
currentPoint.y = cRoot.y + valAxis - static_cast<int>(((chrtVal - chrtMin) * chrtScale) + 0.5); // calculate chart point and round
} else { // any other data format
currentPoint.y = cRoot.y + static_cast<int>(((chrtVal - chrtMin) * chrtScale) + 0.5); // calculate chart point and round
}
} else { // vertical chart
currentPoint.y = cRoot.y + timAxis - i; // Position in chart area
if (chrtDataFmt == WIND || chrtDataFmt == ROTATION) { // degree type value
currentPoint.x = cRoot.x + static_cast<int>((WindUtils::to2PI(chrtVal - chrtMin) * chrtScale) + 0.5); // calculate chart point and round
} else {
currentPoint.x = cRoot.x + static_cast<int>(((chrtVal - chrtMin) * chrtScale) + 0.5); // calculate chart point and round
}
}
return currentPoint;
}
// chart time axis label + lines
void Chart::drawChrtTimeAxis(const char chrtDir, const int8_t chrtSz, const int8_t chrtIntv)
{
float axSlots, intv, i;
char sTime[6];
int timeRng = chrtIntv * 4; // chart time interval: [1] 4 min., [2] 8 min., [3] 12 min., [4] 16 min., [8] 32 min.
epd->setFont(&Ubuntu_Bold8pt8b);
epd->setTextColor(fgColor);
axSlots = 5; // number of axis labels
intv = timAxis / (axSlots - 1); // minutes per chart axis interval (interval is 1 less than axSlots)
i = timeRng; // Chart axis label start at -32, -16, -12, ... minutes
if (chrtDir == HORIZONTAL) {
epd->fillRect(0, cRoot.y, dWidth, 2, fgColor);
for (float j = 0; j < timAxis - 1; j += intv) { // fill time axis with values but keep area free on right hand side for value label
// draw text with appropriate offset
int tOffset = j == 0 ? 13 : -4;
snprintf(sTime, sizeof(sTime), "-%.0f", i);
drawTextCenter(cRoot.x + j + tOffset, cRoot.y - 8, sTime);
epd->drawLine(cRoot.x + j, cRoot.y, cRoot.x + j, cRoot.y + 5, fgColor); // draw short vertical time mark
i -= chrtIntv;
}
} else { // vertical chart
for (float j = intv; j < timAxis - 1; j += intv) { // don't print time label at upper and lower end of time axis
i -= chrtIntv; // we start not at top chart position
snprintf(sTime, sizeof(sTime), "-%.0f", i);
epd->drawLine(cRoot.x, cRoot.y + j, cRoot.x + valAxis, cRoot.y + j, fgColor); // Grid line
if (chrtSz == FULL_SIZE) { // full size chart
epd->fillRect(0, cRoot.y + j - 9, 32, 15, bgColor); // clear small area to remove potential chart lines
epd->setCursor((4 - strlen(sTime)) * 7, cRoot.y + j + 3); // time value; print left screen; value right-formated
epd->printf("%s", sTime); // Range value
} else if (chrtSz == HALF_SIZE_RIGHT) { // half size chart; right side
drawTextCenter(dWidth / 2, cRoot.y + j, sTime); // time value; print mid screen
}
}
}
}
// chart value axis labels + lines
void Chart::drawChrtValAxis(const char chrtDir, const int8_t chrtSz, bool prntName)
{
const GFXfont* font;
constexpr bool NO_LABEL = false;
constexpr bool LABEL = true;
epd->setTextColor(fgColor);
if (chrtDir == HORIZONTAL) {
if (chrtSz == FULL_SIZE) {
font = &Ubuntu_Bold12pt8b;
// print buffer data name on right hand side of time axis (max. size 5 characters)
epd->setFont(font);
drawTextRalign(cRoot.x + timAxis, cRoot.y - 3, dbName.substring(0, 5));
if (chrtDataFmt == WIND) {
prntHorizChartThreeValueAxisLabel(font);
return;
}
// for any other data formats print multiple axis value lines on full charts
prntHorizChartMultiValueAxisLabel(font);
return;
} else { // half size chart -> just print edge values + middle chart line
font = &Ubuntu_Bold10pt8b;
if (prntName) {
// print buffer data name on right hand side of time axis (max. size 5 characters)
epd->setFont(font);
drawTextRalign(cRoot.x + timAxis, cRoot.y - 3, dbName.substring(0, 5));
}
prntHorizChartThreeValueAxisLabel(font);
return;
}
} else { // vertical chart
if (chrtSz == FULL_SIZE) {
font = &Ubuntu_Bold12pt8b;
epd->setFont(font); // use larger font
drawTextRalign(cRoot.x + (valAxis * 0.42), cRoot.y - 2, dbName.substring(0, 6)); // print buffer data name (max. size 5 characters)
} else {
font = &Ubuntu_Bold10pt8b;
}
prntVerticChartThreeValueAxisLabel(font);
}
}
// Print current data value
void Chart::prntCurrValue(const char direction, GwApi::BoatValue& currValue)
{
const int xPosVal = (direction == HORIZONTAL) ? cRoot.x + (timAxis / 2) - 56 : cRoot.x + 32;
const int yPosVal = (direction == HORIZONTAL) ? cRoot.y + valAxis - 7 : cRoot.y + timAxis - 7;
FormattedData frmtDbData = commonData->fmt->formatValue(&currValue, *commonData, NO_SIMUDATA);
String sdbValue = frmtDbData.svalue; // value as formatted string
String dbUnit = frmtDbData.unit; // Unit of value; limit length to 3 characters
epd->fillRect(xPosVal - 1, yPosVal - 35, 128, 41, bgColor); // Clear area for TWS value
epd->drawRect(xPosVal, yPosVal - 34, 126, 40, fgColor); // Draw box for TWS value
epd->setFont(&DSEG7Classic_BoldItalic16pt7b);
epd->setCursor(xPosVal + 1, yPosVal);
epd->print(sdbValue); // value
epd->setFont(&Ubuntu_Bold10pt8b);
epd->setCursor(xPosVal + 76, yPosVal - 17);
epd->print(dbName.substring(0, 3)); // Name, limited to 3 characters
epd->setFont(&Ubuntu_Bold8pt8b);
epd->setCursor(xPosVal + 76, yPosVal + 0);
epd->print(dbUnit); // Unit
}
// print message for no valid data availabletemplate <typename T>
void Chart::prntNoValidData(const char direction)
{
Pos p;
epd->setFont(&Ubuntu_Bold10pt8b);
if (direction == HORIZONTAL) {
p.x = cRoot.x + (timAxis / 2);
p.y = cRoot.y + (valAxis / 2) - 10;
} else {
p.x = cRoot.x + (valAxis / 2);
p.y = cRoot.y + (timAxis / 2) - 10;
}
epd->fillRect(p.x - 37, p.y - 10, 78, 24, bgColor); // Clear area for message
drawTextCenter(p.x, p.y, "No data");
LOG_DEBUG(GwLog::LOG, "Page chart <%s>: No valid data available", dbName);
}
// Get maximum difference of last <amount> of dataBuf ringbuffer values to center chart; for angle data only
double Chart::getAngleRng(const double center, size_t amount)
{
size_t count = dataBuf.getCurrentSize();
if (dataBuf.isEmpty() || amount <= 0) {
return dbMAX_VAL;
}
if (amount > count)
amount = count;
double value = 0;
double range = 0;
double maxRng = dbMIN_VAL;
// Start from the newest value (last) and go backwards x times
for (size_t i = 0; i < amount; i++) {
value = dataBuf.get(count - 1 - i);
if (value == dbMAX_VAL) {
continue; // ignore invalid values
}
range = abs(fmod((value - center + (M_TWOPI + M_PI)), M_TWOPI) - M_PI);
if (range > maxRng)
maxRng = range;
}
if (maxRng > M_PI) {
maxRng = M_PI;
}
return (maxRng != dbMIN_VAL ? maxRng : dbMAX_VAL); // Return range from <mid> to <max>
}
// print value axis label with only three values: top, mid, and bottom for vertical chart
void Chart::prntVerticChartThreeValueAxisLabel(const GFXfont* font)
{
double cVal;
char sVal[7];
epd->fillRect(cRoot.x, cRoot.y, valAxis, 2, fgColor); // top chart line
epd->setFont(font);
cVal = chrtMin;
cVal = commonData->fmt->convertValue(cVal, dbName, dbFormat, *commonData); // value (converted)
snprintf(sVal, sizeof(sVal), "%.0f", round(cVal));
epd->setCursor(cRoot.x, cRoot.y - 2);
epd->printf("%s", sVal); // Range low end
cVal = chrtMid;
cVal = commonData->fmt->convertValue(cVal, dbName, dbFormat, *commonData); // value (converted)
snprintf(sVal, sizeof(sVal), "%.0f", round(cVal));
drawTextCenter(cRoot.x + (valAxis / 2), cRoot.y - 9, sVal); // Range mid end
cVal = chrtMax;
cVal = commonData->fmt->convertValue(cVal, dbName, dbFormat, *commonData); // value (converted)
snprintf(sVal, sizeof(sVal), "%.0f", round(cVal));
drawTextRalign(cRoot.x + valAxis - 2, cRoot.y - 2, sVal); // Range high end
// draw vertical grid lines for each axis label
for (int j = 0; j <= valAxis; j += (valAxis / 2)) {
epd->drawLine(cRoot.x + j, cRoot.y, cRoot.x + j, cRoot.y + timAxis, fgColor);
}
}
// print value axis label with only three values: top, mid, and bottom for horizontal chart
void Chart::prntHorizChartThreeValueAxisLabel(const GFXfont* font)
{
double axLabel;
double chrtMin, chrtMid, chrtMax;
int xOffset, yOffset; // offset for text position of x axis label for different font sizes
String sVal;
if (font == &Ubuntu_Bold10pt8b) {
xOffset = 39;
yOffset = 16;
} else if (font == &Ubuntu_Bold12pt8b) {
xOffset = 51;
yOffset = 18;
}
epd->setFont(font);
// convert & round chart bottom+top label to next range step
chrtMin = commonData->fmt->convertValue(this->chrtMin, dbName, dbFormat, *commonData);
chrtMid = commonData->fmt->convertValue(this->chrtMid, dbName, dbFormat, *commonData);
chrtMax = commonData->fmt->convertValue(this->chrtMax, dbName, dbFormat, *commonData);
chrtMin = std::round(chrtMin * 100.0) / 100.0;
chrtMid = std::round(chrtMid * 100.0) / 100.0;
chrtMax = std::round(chrtMax * 100.0) / 100.0;
// print top axis label
axLabel = (chrtDataFmt == SPEED || chrtDataFmt == TEMPERATURE) ? chrtMax : chrtMin;
sVal = formatLabel(axLabel);
epd->fillRect(cRoot.x, cRoot.y + 2, xOffset + 3, yOffset, bgColor); // Clear small area to remove potential chart lines
drawTextRalign(cRoot.x + xOffset, cRoot.y + yOffset, sVal); // range value
// print mid axis label
axLabel = chrtMid;
sVal = formatLabel(axLabel);
epd->fillRect(cRoot.x, cRoot.y + (valAxis / 2) - 8, xOffset + 3, 16, bgColor); // Clear small area to remove potential chart lines
drawTextRalign(cRoot.x + xOffset, cRoot.y + (valAxis / 2) + 6, sVal); // range value
epd->drawLine(cRoot.x + xOffset + 3, cRoot.y + (valAxis / 2), cRoot.x + timAxis, cRoot.y + (valAxis / 2), fgColor);
// print bottom axis label
axLabel = (chrtDataFmt == SPEED || chrtDataFmt == TEMPERATURE) ? chrtMin : chrtMax;
sVal = formatLabel(axLabel);
epd->fillRect(cRoot.x, cRoot.y + valAxis - 14, xOffset + 3, 15, bgColor); // Clear small area to remove potential chart lines
drawTextRalign(cRoot.x + xOffset, cRoot.y + valAxis, sVal); // range value
epd->drawLine(cRoot.x + xOffset + 3, cRoot.y + valAxis, cRoot.x + timAxis, cRoot.y + valAxis, fgColor);
}
// print value axis label with multiple axis lines for horizontal chart
void Chart::prntHorizChartMultiValueAxisLabel(const GFXfont* font)
{
double chrtMin, chrtMax, chrtRng;
double axSlots, axIntv, axLabel;
int xOffset; // offset for text position of x axis label for different font sizes
String sVal;
if (font == &Ubuntu_Bold10pt8b) {
xOffset = 38;
} else if (font == &Ubuntu_Bold12pt8b) {
xOffset = 50;
}
epd->setFont(font);
chrtMin = commonData->fmt->convertValue(this->chrtMin, dbName, dbFormat, *commonData);
// chrtMin = std::floor(chrtMin / rngStep) * rngStep;
chrtMin = std::round(chrtMin * 100.0) / 100.0;
chrtMax = commonData->fmt->convertValue(this->chrtMax, dbName, dbFormat, *commonData);
// chrtMax = std::ceil(chrtMax / rngStep) * rngStep;
chrtMax = std::round(chrtMax * 100.0) / 100.0;
chrtRng = std::round((chrtMax - chrtMin) * 100) / 100;
axSlots = valAxis / static_cast<double>(VALAXIS_STEP); // number of axis labels (and we want to have a double calculation, no integer)
axIntv = chrtRng / axSlots;
axLabel = chrtMin + axIntv;
// LOG_DEBUG(GwLog::DEBUG, "Chart::printHorizMultiValueAxisLabel: chrtRng: %.2f, th-chrtRng: %.2f, axSlots: %.2f, axIntv: %.2f, axLabel: %.2f, chrtMin: %.2f, chrtMid: %.2f, chrtMax: %.2f", chrtRng, this->chrtRng, axSlots, axIntv, axLabel, this->chrtMin, chrtMid, chrtMax);
int loopStrt, loopEnd, loopStp;
if (chrtDataFmt == SPEED || chrtDataFmt == TEMPERATURE || chrtDataFmt == OTHER) {
// High value at top
loopStrt = valAxis - VALAXIS_STEP;
loopEnd = VALAXIS_STEP / 2;
loopStp = VALAXIS_STEP * -1;
} else {
// Low value at top
loopStrt = VALAXIS_STEP;
loopEnd = valAxis - (VALAXIS_STEP / 2);
loopStp = VALAXIS_STEP;
}
for (int j = loopStrt; (loopStp > 0) ? (j < loopEnd) : (j > loopEnd); j += loopStp) {
sVal = formatLabel(axLabel);
epd->fillRect(cRoot.x, cRoot.y + j - 11, xOffset + 3, 21, bgColor); // Clear small area to remove potential chart lines
drawTextRalign(cRoot.x + xOffset, cRoot.y + j + 7, sVal); // range value
epd->drawLine(cRoot.x + xOffset + 3, cRoot.y + j, cRoot.x + timAxis, cRoot.y + j, fgColor);
axLabel += axIntv;
}
}
// Draw chart line with thickness of 2px
void Chart::drawBoldLine(const int16_t x1, const int16_t y1, const int16_t x2, const int16_t y2)
{
int16_t dx = std::abs(x2 - x1);
int16_t dy = std::abs(y2 - y1);
epd->drawLine(x1, y1, x2, y2, fgColor);
if (dx >= dy) { // line has horizontal tendency
epd->drawLine(x1, y1 - 1, x2, y2 - 1, fgColor);
} else { // line has vertical tendency
epd->drawLine(x1 - 1, y1, x2 - 1, y2, fgColor);
}
}
// Convert and format current axis label to user defined format; helper function for easier handling of OBP60Formatter
String Chart::convNformatLabel(const double& label)
{
GwApi::BoatValue tmpBVal(dbName); // temporary boat value for string formatter
String sVal;
tmpBVal.setFormat(dbFormat);
tmpBVal.valid = true;
tmpBVal.value = label;
sVal = commonData->fmt->formatValue(&tmpBVal, *commonData, NO_SIMUDATA).svalue; // Formatted value as string including unit conversion and switching decimal places
if (sVal.length() > 0 && sVal[0] == '!') {
sVal = sVal.substring(1); // cut leading "!" created at OBPFormatter; doesn't work for other fonts than 7SEG
}
return sVal;
}
// Format current axis label for printing w/o data format conversion (has been done earlier)
String Chart::formatLabel(const double& label)
{
char sVal[11];
if (dbFormat == "formatCourse" || dbFormat == "formatWind") {
// Format 3 numbers with prefix zero
snprintf(sVal, sizeof(sVal), "%03.0f", label);
} else if (dbFormat == "formatRot") {
if (label > -10 && label < 10) {
snprintf(sVal, sizeof(sVal), "%3.2f", label);
} else {
snprintf(sVal, sizeof(sVal), "%3.0f", label);
}
}
else {
if (label < 10) {
snprintf(sVal, sizeof(sVal), "%3.1f", label);
} else {
snprintf(sVal, sizeof(sVal), "%3.0f", label);
}
}
return String(sVal);
}
// --- Class Chart ---------------

116
lib/obp60task/OBPcharts.h Normal file
View File

@@ -0,0 +1,116 @@
// Function lib for display of boat data in various graphical chart formats
#pragma once
#include "Pagedata.h"
#include "OBP60Extensions.h"
struct Pos {
int x;
int y;
};
struct ChartProps {
double range;
double step;
};
template <typename T>
class RingBuffer;
class GwLog;
class Chart {
protected:
CommonData* commonData;
GwLog* logger;
enum ChrtDataFormat {
WIND,
ROTATION,
SPEED,
DEPTH,
TEMPERATURE,
OTHER
};
static constexpr char HORIZONTAL = 'H';
static constexpr char VERTICAL = 'V';
static constexpr int8_t FULL_SIZE = 0;
static constexpr int8_t HALF_SIZE_LEFT = 1;
static constexpr int8_t HALF_SIZE_RIGHT = 2;
static constexpr int8_t MIN_FREE_VALUES = 60; // free 60 values when chart line reaches chart end
static constexpr int8_t THRESHOLD_NO_DATA = 3; // max. seconds of invalid values in a row
static constexpr int8_t VALAXIS_STEP = 60; // pixels between two chart value axis labels
static constexpr bool NO_SIMUDATA = true; // switch off simulation feature of <formatValue> function
RingBuffer<uint16_t>& dataBuf; // Buffer to display
//char chrtDir; // Chart timeline direction: 'H' = horizontal, 'V' = vertical
//int8_t chrtSz; // Chart size: [0] = full size, [1] = half size left/top, [2] half size right/bottom
double dfltRng; // Default range of chart, e.g. 30 = [0..30]
uint16_t fgColor; // color code for any screen writing
uint16_t bgColor; // color code for screen background
bool useSimuData; // flag to indicate if simulation data is active
String tempFormat; // user defined format for temperature
double zeroValue; // "0" SI value for temperature
int dWidth; // Display width
int dHeight; // Display height
int top = 44; // chart gap at top of display (25 lines for standard gap + 19 lines for axis labels)
int bottom = 25; // chart gap at bottom of display to keep space for status line
int hGap = 11; // gap between 2 horizontal charts; actual gap is 2x <gap>
int vGap = 17; // gap between 2 vertical charts; actual gap is 2x <gap>
int timAxis, valAxis; // size of time and value chart axis
Pos cRoot; // start point of chart area
double chrtRng; // Range of buffer values from min to max value
double chrtMin; // Range low end value
double chrtMax; // Range high end value
double chrtMid; // Range mid value
double rngStep; // Defines the step of adjustment (e.g. 10 m/s) for value axis range
bool recalcRngMid = false; // Flag for re-calculation of mid value of chart for wind data types
String dbName, dbFormat; // Name and format of data buffer
ChrtDataFormat chrtDataFmt; // Data format of chart boat data type
double dbMIN_VAL; // Lowest possible value of buffer of type <T>
double dbMAX_VAL; // Highest possible value of buffer of type <T>; indicates invalid value in buffer
size_t bufSize; // History buffer size: 1.920 values for 32 min. history chart
int count; // current size of buffer
int numBufVals; // number of wind values available for current interval selection
int bufStart; // 1st data value in buffer to show
int numAddedBufVals; // Number of values added to buffer since last display
size_t currIdx; // Current index in TWD history buffer
size_t lastIdx; // Last index of TWD history buffer
size_t lastAddedIdx = 0; // Last index of TWD history buffer when new data was added
int numNoData; // Counter for multiple invalid data values in a row
bool bufDataValid = false; // Flag to indicate if buffer data is valid
int oldChrtIntv = 0; // remember recent user selection of data interval
double chrtPrevVal; // Last data value in chart area
int x, y; // x and y coordinates for drawing
int prevX, prevY; // Last x and y coordinates for drawing
bool setChartDimensions(const char direction, const int8_t size); //define dimensions and start points for chart
void drawChrt(const char chrtDir, const int8_t chrtIntv, GwApi::BoatValue& currValue); // Draw chart line
void getBufferStartNSize(const int8_t chrtIntv); // Identify buffer size and buffer start position for chart
void calcChrtBorders(double& rngMin, double& rngMid, double& rngMax, double& rng); // Calculate chart points for value axis and return range between <min> and <max>
void drawChartLines(const char direction, const int8_t chrtIntv, const double chrtScale); // Draw chart graph
Pos setCurrentChartPoint(const int i, const char direction, const double chrtVal, const double chrtScale); // Set current chart point to draw
void drawChrtTimeAxis(const char chrtDir, const int8_t chrtSz, const int8_t chrtIntv); // Draw time axis of chart, value and lines
void drawChrtValAxis(const char chrtDir, const int8_t chrtSz, bool prntLabel); // Draw value axis of chart, value and lines
void prntCurrValue(const char chrtDir, GwApi::BoatValue& currValue); // Add current boat data value to chart
void prntNoValidData(const char chrtDir); // print message for no valid data available
double getAngleRng(const double center, size_t amount); // Calculate range between chart center and edges
void prntVerticChartThreeValueAxisLabel(const GFXfont* font); // print value axis label with only three values: top, mid, and bottom for vertical chart
void prntHorizChartThreeValueAxisLabel(const GFXfont* font); // print value axis label with only three values: top, mid, and bottom for horizontal chart
void prntHorizChartMultiValueAxisLabel(const GFXfont* font); // print value axis label with multiple axis lines for horizontal chart
void drawBoldLine(const int16_t x1, const int16_t y1, const int16_t x2, const int16_t y2); // Draw chart line with thickness of 2px
String convNformatLabel(const double& label); // Convert and format current axis label to user defined format; helper function for easier handling of OBP60Formatter
String formatLabel(const double& label); // Format current axis label for printing w/o data format conversion (has been done earlier)
public:
// Define default chart range and range step for each boat data type
static std::map<String, ChartProps> dfltChrtDta;
Chart(RingBuffer<uint16_t>& dataBuf, double dfltRng, CommonData& common, bool useSimuData); // Chart object of data chart
~Chart();
void showChrt(char chrtDir, int8_t chrtSz, const int8_t chrtIntv, bool prntName, bool showCurrValue, GwApi::BoatValue currValue); // Perform all actions to draw chart
};

View File

@@ -0,0 +1,153 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#if defined BOARD_OBP60S3 || defined BOARD_OBP40S3
#include <PCF8574.h> // PCF8574 modules from Horter
#include "Pagedata.h"
#include "OBP60Extensions.h"
class PageDigitalOut : public Page
{
private:
// Status values
bool button1 = false;
bool button2 = false;
bool button3 = false;
bool button4 = false;
bool button5 = false;
// Button labels
String name1;
String name2;
String name3;
String name4;
String name5;
public:
PageDigitalOut(CommonData &common) : Page(common)
{
logger->logDebug(GwLog::LOG, "Instantiate PageDigitalOut");
// Get config data
String lengthformat = config->getString(config->lengthFormat);
name1 = config->getString(config->mod1Out1);
name2 = config->getString(config->mod1Out2);
name3 = config->getString(config->mod1Out3);
name4 = config->getString(config->mod1Out4);
name5 = config->getString(config->mod1Out5);
}
// Set botton labels
virtual void setupKeys(){
Page::setupKeys();
commonData->keydata[0].label = "1";
commonData->keydata[1].label = "2";
commonData->keydata[2].label = "3";
commonData->keydata[3].label = "4";
commonData->keydata[4].label = "5";
}
virtual int handleKey(int key){
// Code for keylock
if (key == 11) {
commonData->keylock = !commonData->keylock;
return 0;
}
// Code for button 1
if (key == 1) {
button1 = !button1;
setPCF8574PortPinModul1(0, button1 ? 0 : 1); // Attention! Inverse logic for PCF8574
return 0;
}
// Code for button 2
if (key == 2) {
button2 = !button2;
setPCF8574PortPinModul1(1, button2 ? 0 : 1); // Attention! Inverse logic for PCF8574
return 0;
}
// Code for button 3
if (key == 3) {
button3 = !button3;
setPCF8574PortPinModul1(2, button3 ? 0 : 1); // Attention! Inverse logic for PCF8574
return 0;
}
// Code for button 4
if (key == 4) {
button4 = !button4;
setPCF8574PortPinModul1(3, button4 ? 0 : 1); // Attention! Inverse logic for PCF8574
return 0;
}
// Code for button 5
if (key == 5) {
button5 = !button5;
setPCF8574PortPinModul1(4, button5 ? 0 : 1); // Attention! Inverse logic for PCF8574
return 0;
}
return key;
}
void displayNew(PageData &pageData) {
#ifdef BOARD_OBP60S3
// Clear optical warning
if (flashLED == "Limit Violation") {
setBlinkingLED(false);
setFlashLED(false);
}
#endif
};
int displayPage(PageData &pageData){
// Logging boat values
LOG_DEBUG(GwLog::LOG,"Drawing at PageDigitalOut");
// Draw page
//***********************************************************
// Set display in partial refresh mode
epd->setPartialWindow(0, 0, epd->width(), epd->height()); // Set partial update
epd->setTextColor(commonData->fgcolor);
epd->setFont(&Ubuntu_Bold12pt8b);
// Draw labels
epd->setCursor(100, 50 + 8);
epd->print(name1);
epd->setCursor(100, 100 + 8);
epd->print(name2);
epd->setCursor(100, 150 + 8);
epd->print(name3);
epd->setCursor(100,200 + 8);
epd->print(name4);
epd->setCursor(100, 250 + 8);
epd->print(name5);
// Draw bottons
drawButtonCenter(50, 50, 40, 27, "1", commonData->fgcolor, commonData->bgcolor, button1);
drawButtonCenter(50, 100, 40, 27, "2", commonData->fgcolor, commonData->bgcolor, button2);
drawButtonCenter(50, 150, 40, 27, "3", commonData->fgcolor, commonData->bgcolor, button3);
drawButtonCenter(50, 200, 40, 27, "4", commonData->fgcolor, commonData->bgcolor, button4);
drawButtonCenter(50, 250, 40, 27, "5", commonData->fgcolor, commonData->bgcolor, button5);
return PAGE_UPDATE;
};
};
static Page* createPage(CommonData &common){
return new PageDigitalOut(common);
}
/**
* with the code below we make this page known to the PageTask
* we give it a type (name) that can be selected in the config
* we define which function is to be called
* and we provide the number of user parameters we expect
* this will be number of BoatValue pointers in pageData.values
*/
PageDescription registerPageDigitalOut(
"DigitalOut", // Page name
createPage, // Action
0, // Number of bus values depends on selection in Web configuration
true // Show display header on/off
);
#endif

View File

@@ -3,7 +3,6 @@
#include "Pagedata.h" #include "Pagedata.h"
#include "OBP60Extensions.h" #include "OBP60Extensions.h"
#include "BoatDataCalibration.h"
class PageFourValues2 : public Page class PageFourValues2 : public Page
{ {
@@ -54,9 +53,6 @@ public:
GwApi::BoatValue *bvalue1 = pageData.values[0]; // First element in list (only one value by PageOneValue) GwApi::BoatValue *bvalue1 = pageData.values[0]; // First element in list (only one value by PageOneValue)
String name1 = xdrDelete(bvalue1->getName()); // Value name String name1 = xdrDelete(bvalue1->getName()); // Value name
name1 = name1.substring(0, 6); // String length limit for value name name1 = name1.substring(0, 6); // String length limit for value name
#ifdef ENABLE_CALIBRATION
calibrationData.calibrateInstance(bvalue1, logger); // Check if boat data value is to be calibrated
#endif
double value1 = bvalue1->value; // Value as double in SI unit double value1 = bvalue1->value; // Value as double in SI unit
bool valid1 = bvalue1->valid; // Valid information bool valid1 = bvalue1->valid; // Valid information
String svalue1 = commonData->fmt->formatValue(bvalue1, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places String svalue1 = commonData->fmt->formatValue(bvalue1, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places
@@ -66,9 +62,6 @@ public:
GwApi::BoatValue *bvalue2 = pageData.values[1]; // Second element in list (only one value by PageOneValue) GwApi::BoatValue *bvalue2 = pageData.values[1]; // Second element in list (only one value by PageOneValue)
String name2 = xdrDelete(bvalue2->getName()); // Value name String name2 = xdrDelete(bvalue2->getName()); // Value name
name2 = name2.substring(0, 6); // String length limit for value name name2 = name2.substring(0, 6); // String length limit for value name
#ifdef ENABLE_CALIBRATION
calibrationData.calibrateInstance(bvalue2, logger); // Check if boat data value is to be calibrated
#endif
double value2 = bvalue2->value; // Value as double in SI unit double value2 = bvalue2->value; // Value as double in SI unit
bool valid2 = bvalue2->valid; // Valid information bool valid2 = bvalue2->valid; // Valid information
String svalue2 = commonData->fmt->formatValue(bvalue2, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places String svalue2 = commonData->fmt->formatValue(bvalue2, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places
@@ -78,9 +71,6 @@ public:
GwApi::BoatValue *bvalue3 = pageData.values[2]; // Second element in list (only one value by PageOneValue) GwApi::BoatValue *bvalue3 = pageData.values[2]; // Second element in list (only one value by PageOneValue)
String name3 = xdrDelete(bvalue3->getName()); // Value name String name3 = xdrDelete(bvalue3->getName()); // Value name
name3 = name3.substring(0, 6); // String length limit for value name name3 = name3.substring(0, 6); // String length limit for value name
#ifdef ENABLE_CALIBRATION
calibrationData.calibrateInstance(bvalue3, logger); // Check if boat data value is to be calibrated
#endif
double value3 = bvalue3->value; // Value as double in SI unit double value3 = bvalue3->value; // Value as double in SI unit
bool valid3 = bvalue3->valid; // Valid information bool valid3 = bvalue3->valid; // Valid information
String svalue3 = commonData->fmt->formatValue(bvalue3, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places String svalue3 = commonData->fmt->formatValue(bvalue3, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places
@@ -90,9 +80,6 @@ public:
GwApi::BoatValue *bvalue4 = pageData.values[3]; // Second element in list (only one value by PageOneValue) GwApi::BoatValue *bvalue4 = pageData.values[3]; // Second element in list (only one value by PageOneValue)
String name4 = xdrDelete(bvalue4->getName()); // Value name String name4 = xdrDelete(bvalue4->getName()); // Value name
name4 = name4.substring(0, 6); // String length limit for value name name4 = name4.substring(0, 6); // String length limit for value name
#ifdef ENABLE_CALIBRATION
calibrationData.calibrateInstance(bvalue4, logger); // Check if boat data value is to be calibrated
#endif
double value4 = bvalue4->value; // Value as double in SI unit double value4 = bvalue4->value; // Value as double in SI unit
bool valid4 = bvalue4->valid; // Valid information bool valid4 = bvalue4->valid; // Valid information
String svalue4 = commonData->fmt->formatValue(bvalue4, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places String svalue4 = commonData->fmt->formatValue(bvalue4, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places

View File

@@ -0,0 +1,607 @@
#if defined BOARD_OBP60S3 || defined BOARD_OBP40S3
#include "Pagedata.h"
#include "OBP60Extensions.h"
#include "NetworkClient.h" // Network connection
#include "ImageDecoder.h" // Image decoder for navigation map
#include <mbedtls/base64.h>
// Defines for reading of navigation map
#define JSON_BUFFER 30000 // Max buffer size for JSON content (30 kB picture + values)
NetworkClient net(JSON_BUFFER); // Define network client
ImageDecoder decoder; // Define image decoder
class PageNavigation : public Page
{
private:
// Values for buttons
bool firstRun = true; // Detect the first page run
int zoom = 15; // Default zoom level
bool showValues = false; // Show values HDT, SOG, DBT in navigation map
uint8_t* imageBackupData = nullptr;
int imageBackupWidth = 0;
int imageBackupHeight = 0;
size_t imageBackupSize = 0;
size_t imageBackupCapacity = 0;
bool hasImageBackup = false;
bool imageBackupIsRgb565 = false;
String lengthformat;
String mapsource;
String ipAddress;
int localPort;
String mapType;
int zoomLevel;
bool grid;
String orientation;
int refreshDistance;
bool showValuesMap;
bool ownHeading;
public:
PageNavigation(CommonData &common) : Page(common)
{
logger->logDebug(GwLog::LOG,"Instantiate PageNavigation");
imageBackupCapacity = (size_t) epd->width() * (size_t) epd->height();
imageBackupData = (uint8_t*)heap_caps_malloc(imageBackupCapacity, MALLOC_CAP_SPIRAM);
// Get config data
lengthformat = config->getString(config->lengthFormat);
mapsource = config->getString(config->mapsource);
ipAddress = config->getString(config->ipAddress);
localPort = config->getInt(config->localPort);
mapType = config->getString(config->maptype);
zoomLevel = config->getInt(config->zoomlevel);
grid = config->getBool(config->grid);
orientation = config->getString(config->orientation);
refreshDistance = config->getInt(config->refreshDistance);
showValuesMap = config->getBool(config->showvalues);
ownHeading = config->getBool(config->ownheading);
}
// Set botton labels
void setupKeys(){
Page::setupKeys();
commonData->keydata[0].label = "ZOOM -";
commonData->keydata[1].label = "ZOOM +";
commonData->keydata[4].label = "VALUES";
}
int handleKey(int key){
// Code for keylock
if(key == 11){
commonData->keylock = !commonData->keylock;
return 0; // Commit the key
}
// Code for zoom -
if(key == 1){
zoom --; // Zoom -
if(zoom <7){
zoom = 7;
}
return 0; // Commit the key
}
// Code for zoom -
if(key == 2){
zoom ++; // Zoom +
if(zoom >17){
zoom = 17;
}
return 0; // Commit the key
}
if(key == 5){
showValues = !showValues; // Toggle show values
return 0; // Commit the key
}
return key;
}
void displayNew(PageData &pageData) {
#ifdef BOARD_OBP60S3
// Clear optical warning
if (flashLED == "Limit Violation") {
setBlinkingLED(false);
setFlashLED(false);
}
#endif
};
int displayPage(PageData &pageData) {
if (firstRun == true) {
zoom = zoomLevel; // Overwrite zoom level with setup value
showValues = showValuesMap; // Overwrite showValues with setup value
firstRun = false;
}
// Local variables
String server = "norbert-walter.dnshome.de";
int port = 80;
int mType = 1;
int dType = 1;
int mapRot = 0;
int symbolRot = 0;
int mapGrid = 0;
// Old values for hold function
static double value1old = 0;
static String svalue1old = "";
static String unit1old = "";
static double value2old = 0;
static String svalue2old = "";
static String unit2old = "";
static double value3old = 0; // Deg
static String svalue3old = "";
static String unit3old = "";
static double value4old = 0;
static String svalue4old = "";
static String unit4old = "";
static double value5old = 0;
static String svalue5old = "";
static String unit5old = "";
static double value6old = 0;
static String svalue6old = "";
static String unit6old = "";
static double latitude = 0;
static double latitudeold = 0;
static double longitude = 0;
static double longitudeold = 0;
static double trueHeading = 0;
static double magneticHeading = 0;
static double speedOverGround = 0;
static double depthBelowTransducer = 0;
static int lostCounter = 0; // Counter for connection lost to the map server (increment by each page refresh)
int imgWidth = 0;
int imgHeight = 0;
// Get boat values #1 Latitude
GwApi::BoatValue *bvalue1 = pageData.values[0]; // First element in list (only one value by PageOneValue)
String name1 = xdrDelete(bvalue1->getName()); // Value name
name1 = name1.substring(0, 6); // String length limit for value name
double value1 = bvalue1->value; // Value as double in SI unit
bool valid1 = bvalue1->valid; // Valid information
String svalue1 = commonData->fmt->formatValue(bvalue1, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places
String unit1 = commonData->fmt->formatValue(bvalue1, *commonData).unit; // Unit of value
// Get boat values #2 Longitude
GwApi::BoatValue *bvalue2 = pageData.values[1]; // Second element in list (only one value by PageOneValue)
String name2 = xdrDelete(bvalue2->getName()); // Value name
name2 = name2.substring(0, 6); // String length limit for value name
double value2 = bvalue2->value; // Value as double in SI unit
bool valid2 = bvalue2->valid; // Valid information
String svalue2 = commonData->fmt->formatValue(bvalue2, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places
String unit2 = commonData->fmt->formatValue(bvalue2, *commonData).unit; // Unit of value
// Get boat values #3 HDT
GwApi::BoatValue *bvalue3 = pageData.values[2]; // Second element in list (only one value by PageOneValue)
String name3 = xdrDelete(bvalue3->getName()); // Value name
name3 = name3.substring(0, 6); // String length limit for value name
double value3 = bvalue3->value; // Value as double in SI unit
bool valid3 = bvalue3->valid; // Valid information
String svalue3 = commonData->fmt->formatValue(bvalue3, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places
String unit3 = commonData->fmt->formatValue(bvalue3, *commonData).unit; // Unit of value
// Get boat values #4 HDM
GwApi::BoatValue *bvalue4 = pageData.values[3]; // Second element in list (only one value by PageOneValue)
String name4 = xdrDelete(bvalue4->getName()); // Value name
name4 = name4.substring(0, 6); // String length limit for value name
double value4 = bvalue4->value; // Value as double in SI unit
bool valid4 = bvalue4->valid; // Valid information
String svalue4 = commonData->fmt->formatValue(bvalue4, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places
String unit4 = commonData->fmt->formatValue(bvalue4, *commonData).unit; // Unit of value
// Get boat values #5 SOG
GwApi::BoatValue *bvalue5 = pageData.values[4]; // Second element in list (only one value by PageOneValue)
String name5 = xdrDelete(bvalue5->getName()); // Value name
name5 = name5.substring(0, 6); // String length limit for value name
double value5 = bvalue5->value; // Value as double in SI unit
bool valid5 = bvalue5->valid; // Valid information
String svalue5 = commonData->fmt->formatValue(bvalue5, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places
String unit5 = commonData->fmt->formatValue(bvalue5, *commonData).unit; // Unit of value
// Get boat values #6 DBT
GwApi::BoatValue *bvalue6 = pageData.values[5]; // Second element in list (only one value by PageOneValue)
String name6 = xdrDelete(bvalue6->getName()); // Value name
name6 = name6.substring(0, 6); // String length limit for value name
double value6 = bvalue6->value; // Value as double in SI unit
bool valid6 = bvalue6->valid; // Valid information
String svalue6 = commonData->fmt->formatValue(bvalue6, *commonData).svalue; // Formatted value as string including unit conversion and switching decimal places
String unit6 = commonData->fmt->formatValue(bvalue6, *commonData).unit; // Unit of value
// Logging boat values
if (bvalue1 == NULL) return PAGE_OK; // WTF why this statement?
logger->logDebug(GwLog::LOG, "Drawing at PageNavigation, %s: %f, %s: %f, %s: %f, %s: %f, %s: %f, %s: %f",
name1.c_str(), value1, name2.c_str(), value2, name3.c_str(), value3,
name4.c_str(), value4, name5.c_str(), value5, name6.c_str(), value6);
// Set variables
//***********************************************************
// Latitude
if(valid1){
latitude = value1;
latitudeold = value1;
value3old = value1;
}
else{
latitude = value1old;
}
// Longitude
if(valid2){
longitude = value2;
longitudeold = value2;
value2old = value2;
}
else{
longitude = value2old;
}
// HDT value (True Heading, GPS)
if(valid3){
trueHeading = (value3 * 360) / (2 * PI);
value3old = trueHeading;
}
else{
trueHeading = value3old;
}
// HDM value (Magnetic Heading)
if(valid4){
magneticHeading = (value4 * 360) / (2 * PI);
value4old = magneticHeading;
}
else{
speedOverGround = value4old;
}
// SOG value (Speed Over Ground)
if(valid5){
speedOverGround = value5;
value5old = value5;
}
else{
speedOverGround = value5old;
}
// DBT value (Depth Below Transducer)
if(valid6){
depthBelowTransducer = value6;
value6old = value6;
}
else{
depthBelowTransducer = value6old;
}
// Prepare config values for URL
//***********************************************************
// Server settings
if(mapsource == "OBP Service"){
server = "norbert-walter.dnshome.de";
port = 80;
}
else if(mapsource == "Local Service"){
server = String(ipAddress);
port = localPort;
}
else{
server = "norbert-walter.dnshome.de";
port = 80;
}
// Type of navigation map
if(mapType == "Open Street Map"){
mType = 1; // Map type
dType = 1; // Dithering type
}
else if(mapType == "Google Street"){
mType = 3;
dType = 2;
}
else if(mapType == "Open Topo Map"){
mType = 5;
dType = 2;
}
else if(mapType == "Stadimaps Toner"){
mType = 7;
dType = 1;
}
else if(mapType == "Free Nautical Chart"){
mType = 9;
dType = 1;
}
else if(mapType == "C-Map"){
mType = 103486987;
dType = 1;
}
else if(mapType == "Garmin Fish"){
mType = 113486987;
dType = 1;
}
else if(mapType == "Garmin Nav"){
mType = 123486987;
dType = 1;
}
else{
mType = 1;
dType = 1;
}
// Map grid on/off
if(grid == true){
mapGrid = 1;
}
else{
mapGrid = 0;
}
// Map orientation
if(orientation == "North Direction"){
mapRot = 0;
// If true heading available then use HDT oterwise HDM
if(valid3 == true){
symbolRot = trueHeading;
}
else{
symbolRot = magneticHeading;
}
}
else if(orientation == "Travel Direction"){
// If true heading available then use HDT oterwise HDM
if(valid3 == true){
mapRot = trueHeading;
symbolRot = trueHeading;
}
else{
mapRot = magneticHeading;
symbolRot = magneticHeading;
}
}
else{
mapRot = 0;
// If true heading available then use HDT oterwise HDM
if(valid3 == true){
symbolRot = trueHeading;
}
else{
symbolRot = magneticHeading;
}
}
// Load navigation map
//***********************************************************
// URL to OBP Maps Converter
// For more details see: https://github.com/norbert-walter/maps-converter
String url = String("http://") + server + ":" + port + // OBP Server
String("/get_image_json?") + // Service: Output B&W picture as JSON (Base64 + gzip)
#ifdef DISPLAY_ST7796
"oformat=3" + // Image output format in JSON: 3=RGB565 format
#else
"oformat=4" + // Image output format in JSON: 4=b/w 1-Bit format
#endif
"&zoom=" + zoom + // Default zoom level: 15
"&lat=" + String(latitude, 6) + // Latitude
"&lon=" + String(longitude, 6) + // Longitude
"&mrot=" + mapRot + // Rotation angle navigation map in degree
"&mtype=" + mType + // Default Map: Open Street Map
#ifdef DISPLAY_ST7796
"&itype=1" + // Image type: 1=Color
#else
"&itype=4" + // Image type: 4=b/w with dithering
#endif
"&dtype=" + dType + // Dithering type: Atkinson dithering (only activ when itype=4 otherwise inactive)
"&width=400" + // With navigation map
"&height=250" + // Height navigation map
"&cutout=0" + // No picture cutouts (tab, border and alpha are unused when cutout=0)
"&tab=0" + // No tab size (only available when sqare cutouts selected coutout=3...7)
"&border=2" + // Border line size: 2 pixel (only available when sqare cutouts selected)
"&alpha=80" + // Alpha for tabs: 80% visible (only available when sqare cutouts selected)
"&symbol=2" + // Symbol: Triangle
"&srot=" + symbolRot + // Symbol rotation angle
"&ssize=15" + // Symbole size: 15 pixel (center pointer)
"&grid=" + mapGrid // Show grid: On
;
// Draw page
//***********************************************************
// ############### Draw Navigation Map ################
// Set display in partial refresh mode
epd->setPartialWindow(0, 0, epd->width(), epd->height()); // Set partial update
epd->setTextColor(commonData->fgcolor);
// NEW: simple exponential backoff for 1 Hz polling (prevents connection-refused storms)
static uint32_t nextAllowedMs = 0;
static uint8_t failCount = 0;
uint32_t now = millis();
// NEW: if we are in backoff window, skip network call and use backup immediately
bool allowFetch = ((int32_t)(now - nextAllowedMs) >= 0);
// If a network connection to URL then load the navigation map
if (allowFetch && net.fetchAndDecompressJson(url)) {
// NEW: reset backoff on success
failCount = 0;
nextAllowedMs = now + 1000; // keep 1 Hz on success
int numPix = net.numberPixels(); // Read number of pixels
imgWidth = net.imageWidth(); // Read width of image
imgHeight = net.imageHeight(); // Read height of image
size_t requiredBytesMono = 0;
size_t requiredBytesRgb565 = 0;
if (imgWidth > 0 && imgHeight > 0){
requiredBytesMono = (size_t)((imgWidth + 7) / 8) * (size_t)imgHeight;
requiredBytesRgb565 = (size_t)imgWidth * (size_t)imgHeight * 2U;
}
if (requiredBytesMono == 0){
logger->logDebug(GwLog::ERROR,"Error PageNavigation: invalid image geometry w=%d h=%d",imgWidth,imgHeight);
return PAGE_UPDATE;
}
const char* b64src = net.pictureBase64(); // Read picture as Base64 content
if (b64src == nullptr){
logger->logDebug(GwLog::ERROR,"Error PageNavigation: picture_base64 missing");
return PAGE_UPDATE;
}
size_t b64len = net.pictureBase64Len(); // Calculate length of Base64 content
// Copy Base64 content in PSRAM
char* b64 = (char*) heap_caps_malloc(b64len + 1, MALLOC_CAP_SPIRAM); // Allcate PSRAM for Base64 content
if (!b64) {
logger->logDebug(GwLog::ERROR,"Error PageNavigation: PSRAM alloc base64 failed");
return PAGE_UPDATE;
}
memcpy(b64, b64src, b64len + 1); // Copy Base64 content in PSRAM
// Set image buffer in PSRAM
size_t imgSize = (numPix > 0) ? (size_t)numPix : requiredBytesMono; // Calculate image size
if (imgSize < requiredBytesMono){
imgSize = requiredBytesMono;
}
uint8_t* imageData = (uint8_t*) heap_caps_malloc(imgSize, MALLOC_CAP_SPIRAM); // Allocate PSRAM for image
if (!imageData) {
logger->logDebug(GwLog::ERROR,"Error PageNavigation: PSRAM alloc image buffer failed");
free(b64);
return PAGE_UPDATE;
}
// Decode Base64 content to image
size_t decodedSize = 0;
bool decodeOk = decoder.decodeBase64(b64, b64len, imageData, imgSize, decodedSize);
if (!decodeOk || decodedSize < requiredBytesMono){
int base64Ret = mbedtls_base64_decode(
nullptr,
0,
&decodedSize,
(const unsigned char*)b64,
b64len
);
logger->logDebug(GwLog::ERROR,
"Error PageNavigation: decode failed (ok=%d, decoded=%u, required=%u, b64ret=%d)",
decodeOk ? 1 : 0,
(unsigned int)decodedSize,
(unsigned int)requiredBytesMono,
base64Ret
);
free(b64);
free(imageData);
return PAGE_UPDATE;
}
// Copy actual navigation map to backup map
imageBackupWidth = imgWidth;
imageBackupHeight = imgHeight;
imageBackupSize = imgSize;
if (decodedSize > 0 && imageBackupData != nullptr) {
size_t copySize = (decodedSize > imageBackupCapacity) ? imageBackupCapacity : decodedSize;
memcpy(imageBackupData, imageData, copySize);
imageBackupSize = copySize;
}
hasImageBackup = (imageBackupData != nullptr);
lostCounter = 0;
// Show image (navigation map)
epd->drawBitmap(0, 25, imageData, imgWidth, imgHeight, commonData->fgcolor);
// Clean PSRAM
free(b64);
free(imageData);
}
// If no network connection then use backup navigation map
else {
// NEW: update backoff only if we actually attempted a fetch (not when skipping due to backoff)
if (allowFetch) {
// NEW: exponential backoff: 1s,2s,4s,8s,16s,30s (capped)
if (failCount < 6) failCount++;
uint32_t backoffMs = 1000u << failCount;
if (backoffMs > 30000u) backoffMs = 30000u;
nextAllowedMs = now + backoffMs;
} else {
// NEW: we are currently backing off; do not increase failCount further
// nextAllowedMs stays unchanged
}
// Show backup image (backup navigation map)
if (hasImageBackup) {
epd->drawBitmap(0, 25, imageBackupData, imageBackupWidth, imageBackupHeight, commonData->fgcolor);
}
// Show connection lost info when 5 page refreshes has a connection lost to the map server
// Short connection losts are uncritical
if(lostCounter >= 5){
epd->setFont(&Ubuntu_Bold12pt8b);
epd->fillRect(200, 250 , 200, 25, commonData->fgcolor);
epd->fillRect(202, 252 , 196, 21, commonData->bgcolor);
epd->setCursor(210, 270);
epd->print("Map server lost");
}
lostCounter++;
}
// ############### Draw Values ################
epd->setFont(&Ubuntu_Bold12pt8b);
// Show zoom level
epd->fillRect(355, 25 , 45, 25, commonData->fgcolor);
epd->fillRect(357, 27 , 41, 21, commonData->bgcolor);
epd->setCursor(364, 45);
epd->print(zoom);
// If true heading available then use HDT oterwise HDM
if (showValues == true) {
// Frame
epd->fillRect(0, 25 , 130, 65, commonData->fgcolor);
epd->fillRect(2, 27 , 126, 61, commonData->bgcolor);
if(valid3 == true){
// HDT
epd->setCursor(10, 45);
epd->print(name3);
epd->setCursor(70, 45);
epd->print(svalue3);
}
else{
// HDM
epd->setCursor(10, 45);
epd->print(name4);
epd->setCursor(70, 45);
epd->print(svalue4);
}
// SOG
epd->setCursor(10, 65);
epd->print(name5);
epd->setCursor(70, 65);
epd->print(svalue5);
// DBT
epd->setCursor(10, 85);
epd->print(name6);
epd->setCursor(70, 85);
epd->print(svalue6);
}
return PAGE_UPDATE;
};
};
static Page *createPage(CommonData &common){
return new PageNavigation(common);
}/**
* with the code below we make this page known to the PageTask
* we give it a type (name) that can be selected in the config
* we define which function is to be called
* and we provide the number of user parameters we expect
* this will be number of BoatValue pointers in pageData.values
*/
PageDescription registerPageNavigation(
"Navigation", // Page name
createPage, // Action
0, // Number of bus values depends on selection in Web configuration
{"LAT","LON","HDT","HDM","SOG","DBT"}, // Bus values we need in the page
true // Show display header on/off
);
#endif

View File

@@ -30,6 +30,7 @@
#include <esp32/clk.h> #include <esp32/clk.h>
#include "qrcode.h" #include "qrcode.h"
#include "Nmea2kTwai.h" #include "Nmea2kTwai.h"
#include <vector>
#ifdef BOARD_OBP40S3 #ifdef BOARD_OBP40S3
#include "dirent.h" #include "dirent.h"
@@ -58,6 +59,7 @@ private:
String buzzer_mode; String buzzer_mode;
uint8_t buzzer_power; uint8_t buzzer_power;
String cpuspeed; String cpuspeed;
String powermode;
String rtc_module; String rtc_module;
String gps_module; String gps_module;
String env_module; String env_module;
@@ -80,6 +82,15 @@ private:
ConfigMenu *menu; ConfigMenu *menu;
struct device {
uint64_t NAME;
uint8_t id;
char hex_name[17];
uint16_t manuf_code;
const char *model;
};
std::vector<device> devicelist;
void incMode() { void incMode() {
if (mode == 'N') { // Normal if (mode == 'N') { // Normal
mode = 'S'; mode = 'S';
@@ -102,9 +113,9 @@ private:
void decMode() { void decMode() {
if (mode == 'N') { if (mode == 'N') {
if (hasSDCard) { if (hasSDCard) {
mode = 'A'; mode = 'A'; // SD-Card
} else { } else {
mode = 'D'; mode = 'D'; // Device list
} }
} else if (mode == 'S') { // Settings } else if (mode == 'S') { // Settings
mode = 'N'; mode = 'N';
@@ -344,6 +355,11 @@ private:
epd->setCursor(x0, y0 + 144); epd->setCursor(x0, y0 + 144);
epd->print("Home Lon.:"); epd->print("Home Lon.:");
drawTextRalign(230, y0 + 144, formatLongitude(homelon)); drawTextRalign(230, y0 + 144, formatLongitude(homelon));
// Power
epd->setCursor(x0, y0 + 176);
epd->print("Power mode:");
epd->setCursor(120, y0 + 176);
epd->print(powermode);
// right column // right column
epd->setCursor(202, y0); epd->setCursor(202, y0);
@@ -362,6 +378,15 @@ private:
epd->print("Gen. sensor:"); epd->print("Gen. sensor:");
epd->setCursor(320, y0 + 32); epd->setCursor(320, y0 + 32);
epd->print(gen_sensor); epd->print(gen_sensor);
// TODO
// Gyro sensor (rotation)
epd->setCursor(202, y0 + 48);
epd->print("Rot. sensor:");
epd->setCursor(320, y0 + 48);
epd->print(rot_sensor);
// Temp.-sensor
// Power Mode
#ifdef BOARD_OBP60S3 #ifdef BOARD_OBP60S3
// Backlight infos // Backlight infos
@@ -532,6 +557,42 @@ private:
epd->setCursor(20, 140); epd->setCursor(20, 140);
epd->printf("N2k source address: %d", NMEA2000->GetN2kSource()); epd->printf("N2k source address: %d", NMEA2000->GetN2kSource());
uint16_t x0 = 20;
uint16_t y0 = 100;
epd->setFont(&Ubuntu_Bold10pt8b);
epd->setCursor(x0, y0);
epd->print("ID");
epd->setCursor(x0 + 50, y0);
epd->print("Model");
epd->setCursor(x0 + 250, y0);
epd->print("Manuf.");
epd->drawLine(18, y0 + 4, 360 , y0 + 4 , commonData->fgcolor);
epd->setFont(&Ubuntu_Bold8pt8b);
y0 = 120;
uint8_t n_dev = 0;
for (const device& item : devicelist) {
if (n_dev > 8) {
break;
}
epd->setCursor(x0, y0 + n_dev * 20);
epd->print(item.id);
epd->setCursor(x0 + 50, y0 + n_dev * 20);
epd->print(item.model);
epd->setCursor(x0 + 250, y0 + n_dev * 20);
epd->print(item.manuf_code);
n_dev++;
}
epd->setCursor(x0, y0 + (n_dev + 1) * 20);
if (n_dev == 0) {
epd->printf("no devices found on bus");
} else {
epd->drawLine(18, y0 + n_dev * 20, 360 , y0 + n_dev * 20, commonData->fgcolor);
epd->printf("%d devices of %d in total", n_dev, devicelist.size());
}
} }
void storeConfig() { void storeConfig() {
@@ -556,6 +617,7 @@ public:
buzzer_mode.toLowerCase(); buzzer_mode.toLowerCase();
buzzer_power = config->getInt(config->buzzerPower); buzzer_power = config->getInt(config->buzzerPower);
cpuspeed = config->getString(config->cpuSpeed); cpuspeed = config->getString(config->cpuSpeed);
powermode = config->getString(config->powerMode);
env_module = config->getString(config->useEnvSensor); env_module = config->getString(config->useEnvSensor);
rtc_module = config->getString(config->useRTC); rtc_module = config->getString(config->useRTC);
gps_module = config->getString(config->useGPS); gps_module = config->getString(config->useGPS);
@@ -681,6 +743,28 @@ public:
// Get references from API // Get references from API
logger->logDebug(GwLog::LOG, "New page display: PageSystem"); logger->logDebug(GwLog::LOG, "New page display: PageSystem");
NMEA2000 = pageData.api->getNMEA2000(); NMEA2000 = pageData.api->getNMEA2000();
// load current device list
tN2kDeviceList *pDevList = pageData.api->getN2kDeviceList();
// TODO check if changed
if (pDevList->ReadResetIsListUpdated()) {
// only reload if changed
devicelist.clear();
for (uint8_t i = 0; i <= 252; i++) {
const tNMEA2000::tDevice *d = pDevList->FindDeviceBySource(i);
if (d == nullptr) {
continue;
}
device dev;
dev.id = i;
dev.NAME = d->GetName();
snprintf(dev.hex_name, sizeof(dev.hex_name), "%08X%08X", (uint32_t)(dev.NAME >> 32), (uint32_t)(dev.NAME & 0xFFFFFFFF));
dev.manuf_code = d->GetManufacturerCode();
dev.model = d->GetModelID();
devicelist.push_back(dev);
}
}
}; };
int displayPage(PageData &pageData) { int displayPage(PageData &pageData) {

View File

@@ -1,127 +1,113 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#if defined BOARD_OBP60S3 || defined BOARD_OBP40S3 #if defined BOARD_OBP60S3 || defined BOARD_OBP40S3
#include "Pagedata.h" #include "Pagedata.h"
#include "OBP60Extensions.h" #include "OBP60Extensions.h"
#include "OBPRingBuffer.h"
#include "OBPDataOperations.h" #include "OBPDataOperations.h"
#include "BoatDataCalibration.h" #include "OBPcharts.h"
#include <vector>
static const double radToDeg = 180.0 / M_PI; // Conversion factor from radians to degrees
// Get maximum difference of last <amount> of TWD ringbuffer values to center chart; returns "0" if data is not valid
int getCntr(const RingBuffer<int16_t>& windDirHstry, size_t amount)
{
const int MAX_VAL = windDirHstry.getMaxVal();
size_t count = windDirHstry.getCurrentSize();
if (windDirHstry.isEmpty() || amount <= 0) {
return 0;
}
if (amount > count)
amount = count;
uint16_t midWndDir, minWndDir, maxWndDir = 0;
int wndCenter = 0;
midWndDir = windDirHstry.getMid(amount);
if (midWndDir != MAX_VAL) {
midWndDir = midWndDir / 1000.0 * radToDeg;
wndCenter = int((midWndDir + (midWndDir >= 0 ? 5 : -5)) / 10) * 10; // Set new center value; round to nearest 10 degree value
minWndDir = windDirHstry.getMin(amount) / 1000.0 * radToDeg;
maxWndDir = windDirHstry.getMax(amount) / 1000.0 * radToDeg;
if ((maxWndDir - minWndDir) > 180 && !(minWndDir > maxWndDir)) { // if wind range is > 180 and no 0° crossover, adjust wndCenter to smaller wind range end
wndCenter = WindUtils::to360(wndCenter + 180);
}
}
return wndCenter;
}
// Get maximum difference of last <amount> of TWD ringbuffer values to center chart
int getRng(const RingBuffer<int16_t>& windDirHstry, int center, size_t amount)
{
int minVal = windDirHstry.getMinVal();
const int MAX_VAL = windDirHstry.getMaxVal();
size_t count = windDirHstry.getCurrentSize();
if (windDirHstry.isEmpty() || amount <= 0) {
return MAX_VAL;
}
if (amount > count)
amount = count;
int value = 0;
int rng = 0;
int maxRng = minVal;
// Start from the newest value (last) and go backwards x times
for (size_t i = 0; i < amount; i++) {
value = windDirHstry.get(count - 1 - i);
if (value == MAX_VAL) {
continue; // ignore invalid values
}
value = value / 1000.0 * radToDeg;
rng = abs(((value - center + 540) % 360) - 180);
if (rng > maxRng)
maxRng = rng;
}
if (maxRng > 180) {
maxRng = 180;
}
return (maxRng != minVal ? maxRng : MAX_VAL);
}
// **************************************************************** // ****************************************************************
class PageWindPlot : public Page { class PageWindPlot : public Page
{
private: private:
GwLog* logger;
enum ChartMode {
DIRECTION,
SPEED,
BOTH
};
static constexpr char HORIZONTAL = 'H';
static constexpr char VERTICAL = 'V';
static constexpr int8_t FULL_SIZE = 0;
static constexpr int8_t HALF_SIZE_LEFT = 1;
static constexpr int8_t HALF_SIZE_RIGHT = 2;
static constexpr bool PRNT_NAME = true;
static constexpr bool NO_PRNT_NAME = false;
static constexpr bool PRNT_VALUE = true;
static constexpr bool NO_PRNT_VALUE = false;
// int width; // Screen width
// int height; // Screen height
bool keylock = false; // Keylock bool keylock = false; // Keylock
char chrtMode = 'D'; // Chart mode: 'D' for TWD, 'S' for TWS, 'B' for both ChartMode chrtMode = DIRECTION;
bool showTruW = true; // Show true wind or apparant wind in chart area bool showTruW = true; // Show true wind or apparent wind in chart area
bool oldShowTruW = false; // remember recent user selection of wind data type bool oldShowTruW = false; // remember recent user selection of wind data type
int dataIntv = 1; // Update interval for wind history chart: int8_t dataIntv = 1; // Update interval for wind history chart:
// (1)|(2)|(3)|(4) seconds for approx. 4, 8, 12, 16 min. history chart // (1)|(2)|(3)|(4)|(8) x 240 seconds for 4, 8, 12, 16, 32 min. history chart
bool useSimuData;
// bool holdValues;
String flashLED;
String backlightMode;
#ifdef BOARD_OBP40S3
String wndSrc; // Wind source true/apparent wind - preselection for OBP40
#endif
// Data buffers pointers (owned by HstryBuffers)
RingBuffer<uint16_t>* twdHstry = nullptr;
RingBuffer<uint16_t>* twsHstry = nullptr;
RingBuffer<uint16_t>* awdHstry = nullptr;
RingBuffer<uint16_t>* awsHstry = nullptr;
// Chart objects
std::unique_ptr<Chart> twdChart, awdChart; // Chart object for wind direction
std::unique_ptr<Chart> twsChart, awsChart; // Chart object for wind speed
// Active charts and values
Chart* wdChart = nullptr;
Chart* wsChart = nullptr;
GwApi::BoatValue* wdBVal = nullptr;
GwApi::BoatValue* wsBVal = nullptr;
public: public:
PageWindPlot(CommonData& common) : Page(common) PageWindPlot(CommonData& common) : Page(common)
{ {
logger->logDebug(GwLog::LOG, "Instantiate PageWindPlot"); logger->logDebug(GwLog::LOG, "Instantiate PageWindPlot");
//width = epd->width(); // Screen width
//height = epd->height(); // Screen height
// Get config data
oldShowTruW = !showTruW; // makes wind source being initialized at initial page call
} }
void setupKeys() { void setupKeys()
{
Page::setupKeys(); Page::setupKeys();
// commonData->keydata[0].label = "MODE"; commonData->keydata[0].label = "MODE";
#if defined BOARD_OBP60S3 #if defined BOARD_OBP60S3
commonData->keydata[1].label = "SRC"; commonData->keydata[1].label = "SRC";
commonData->keydata[4].label = "INTV"; commonData->keydata[4].label = "ZOOM";
#elif defined BOARD_OBP40S3 #elif defined BOARD_OBP40S3
commonData->keydata[1].label = "INTV"; commonData->keydata[1].label = "ZOOM";
#endif #endif
} }
// Key functions // Key functions
int handleKey(int key) { int handleKey(int key)
// Set chart mode TWD | TWS -> to be implemented {
// Set chart mode
if (key == 1) { if (key == 1) {
if (chrtMode == 'D') { if (chrtMode == DIRECTION) {
chrtMode = 'S'; chrtMode = SPEED;
} else if (chrtMode == 'S') { } else if (chrtMode == SPEED) {
chrtMode = 'B'; chrtMode = BOTH;
} else { } else {
chrtMode = 'D'; chrtMode = DIRECTION;
} }
return 0; // Commit the key return 0;
} }
#if defined BOARD_OBP60S3 #if defined BOARD_OBP60S3
// Set data source TRUE | APP // Set data source TRUE | APP
if (key == 2) { if (key == 2) {
showTruW = !showTruW; showTruW = !showTruW;
return 0; // Commit the key return 0;
} }
// Set interval for wind history chart update time (interval) // Set interval for wind history chart update time (interval)
@@ -135,10 +121,12 @@ public:
dataIntv = 3; dataIntv = 3;
} else if (dataIntv == 3) { } else if (dataIntv == 3) {
dataIntv = 4; dataIntv = 4;
} else if (dataIntv == 4) {
dataIntv = 8;
} else { } else {
dataIntv = 1; dataIntv = 1;
} }
return 0; // Commit the key return 0;
} }
// Keylock function // Keylock function
@@ -149,7 +137,8 @@ public:
return key; return key;
} }
void displayNew(PageData &pageData) { virtual void displayNew(PageData& pageData)
{
#ifdef BOARD_OBP60S3 #ifdef BOARD_OBP60S3
// Clear optical warning // Clear optical warning
if (flashLED == "Limit Violation") { if (flashLED == "Limit Violation") {
@@ -158,344 +147,99 @@ public:
} }
#endif #endif
#ifdef BOARD_OBP40S3 #ifdef BOARD_OBP40S3
String wndSrc; // Wind source true/apparant wind - preselection for OBP40 // we can only initialize user defined wind source here, because "pageData" is not available at object instantiation
wndSrc = commonData->config->getString("page" + String(pageData.pageNumber) + "wndsrc"); wndSrc = commonData->config->getString("page" + String(pageData.pageNumber) + "wndsrc");
if (wndSrc =="True wind") { if (wndSrc == "True wind") {
showTruW = true; showTruW = true;
} else { } else {
showTruW = false; // Wind source is apparant wind showTruW = false; // Wind source is apparent wind
} }
commonData->logger->logDebug(GwLog::LOG,"New PageWindPlot: wind source=%s", wndSrc); oldShowTruW = !showTruW; // Force chart update in displayPage
#endif #endif
oldShowTruW = !showTruW; // makes wind source being initialized at initial page call
if (!twdChart) { // Create true wind charts if they don't exist
twdHstry = pageData.hstryBuffers->getBuffer("TWD");
twsHstry = pageData.hstryBuffers->getBuffer("TWS");
if (twdHstry) {
twdChart.reset(new Chart(*twdHstry, Chart::dfltChrtDta["formatCourse"].range, *commonData, useSimuData));
}
if (twsHstry) {
twsChart.reset(new Chart(*twsHstry, Chart::dfltChrtDta["formatKnots"].range, *commonData, useSimuData));
}
} }
int displayPage(PageData& pageData) { if (!awdChart) { // Create apparent wind charts if they don't exist
awdHstry = pageData.hstryBuffers->getBuffer("AWD");
awsHstry = pageData.hstryBuffers->getBuffer("AWS");
static RingBuffer<int16_t>* wdHstry; // Wind direction data buffer if (awdHstry) {
static RingBuffer<uint16_t>* wsHstry; // Wind speed data buffer awdChart.reset(new Chart(*awdHstry, Chart::dfltChrtDta["formatCourse"].range, *commonData, useSimuData));
static String wdName, wdFormat; // Wind direction name and format }
static String wsName, wsFormat; // Wind speed name and format if (awsHstry) {
static int16_t wdMAX_VAL; // Max. value of wd history buffer, indicating invalid values awsChart.reset(new Chart(*awsHstry, Chart::dfltChrtDta["formatKnots"].range, *commonData, useSimuData));
float wsValue; // Wind speed value in chart area }
String wsUnit; // Wind speed unit in chart area if (twdHstry && twsHstry && awdHstry && awsHstry) {
static GwApi::BoatValue* wsBVal = new GwApi::BoatValue("TWS"); // temp BoatValue for wind speed unit identification; required by OBP60Formater LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Created wind charts");
} else {
// current boat data values; TWD/AWD only for validation test LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: Some/all chart objects for wind data missing");
const int numBoatData = 2; }
GwApi::BoatValue* bvalue; }
bool BDataValid[numBoatData]; }
static bool isInitialized = false; // Flag to indicate that page is initialized
static bool wndDataValid = false; // Flag to indicate if wind data is valid
static int numNoData; // Counter for multiple invalid data values in a row
static int width; // Screen width
static int height; // Screen height
static int xCenter; // Center of screen in x direction
static const int yOffset = 48; // Offset for y coordinates of chart area
static int cHeight; // height of chart area
static int bufSize; // History buffer size: 960 values for appox. 16 min. history chart
static int intvBufSize; // Buffer size used for currently selected time interval
int count; // current size of buffer
static int numWndVals; // number of wind values available for current interval selection
static int bufStart; // 1st data value in buffer to show
int numAddedBufVals; // Number of values added to buffer since last display
size_t currIdx; // Current index in TWD history buffer
static size_t lastIdx; // Last index of TWD history buffer
static size_t lastAddedIdx = 0; // Last index of TWD history buffer when new data was added
static int oldDataIntv; // remember recent user selection of data interval
static int wndCenter; // chart wind center value position
static int wndLeft; // chart wind left value position
static int wndRight; // chart wind right value position
static int chrtRng; // Range of wind values from mid wind value to min/max wind value in degrees
int diffRng; // Difference between mid and current wind value
static const int dfltRng = 60; // Default range for chart
int midWndDir; // New value for wndCenter after chart start / shift
int x, y; // x and y coordinates for drawing
static int prevX, prevY; // Last x and y coordinates for drawing
static float chrtScl; // Scale for wind values in pixels per degree
int chrtVal; // Current wind value
static int chrtPrevVal; // Last wind value in chart area for check if value crosses 180 degree line
int displayPage(PageData& pageData)
{
logger->logDebug(GwLog::LOG, "Display PageWindPlot"); logger->logDebug(GwLog::LOG, "Display PageWindPlot");
ulong timer = millis(); // ulong pageTime = millis();
if (!isInitialized) {
width = epd->width();
height = epd->height();
xCenter = width / 2;
cHeight = height - yOffset - 22;
numNoData = 0;
bufStart = 0;
oldDataIntv = 0;
wsValue = 0;
numAddedBufVals, currIdx, lastIdx = 0;
wndCenter = INT_MAX;
midWndDir = 0;
diffRng = dfltRng;
chrtRng = dfltRng;
isInitialized = true; // Set flag to indicate that page is now initialized
}
// read boat data values; TWD only for validation test, TWS for display of current value
for (int i = 0; i < numBoatData; i++) {
bvalue = pageData.values[i];
BDataValid[i] = bvalue->valid;
}
// Optical warning by limit violation (unused)
if (String(flashLED) == "Limit Violation") {
setBlinkingLED(false);
setFlashLED(false);
}
if (showTruW != oldShowTruW) { if (showTruW != oldShowTruW) {
// Switch active charts based on showTruW
if (showTruW) { if (showTruW) {
wdHstry = pageData.boatHstry->hstryBufList.twdHstry; wdChart = twdChart.get();
wsHstry = pageData.boatHstry->hstryBufList.twsHstry; wsChart = twsChart.get();
wdBVal = pageData.values[0];
wsBVal = pageData.values[1];
} else { } else {
wdHstry = pageData.boatHstry->hstryBufList.awdHstry; wdChart = awdChart.get();
wsHstry = pageData.boatHstry->hstryBufList.awsHstry; wsChart = awsChart.get();
wdBVal = pageData.values[2];
wsBVal = pageData.values[3];
} }
wdHstry->getMetaData(wdName, wdFormat);
wsHstry->getMetaData(wsName, wsFormat);
wdMAX_VAL = wdHstry->getMaxVal();
bufSize = wdHstry->getCapacity();
wsBVal->setFormat(wsHstry->getFormat());
lastAddedIdx = wdHstry->getLastIdx();
oldShowTruW = showTruW; oldShowTruW = showTruW;
} }
logger->logDebug(GwLog::DEBUG, "PageWindPlot: draw with data %s: %.2f, %s: %.2f", wdBVal->getName().c_str(), wdBVal->value, wsBVal->getName().c_str(), wsBVal->value);
// Identify buffer size and buffer start position for chart
count = wdHstry->getCurrentSize();
currIdx = wdHstry->getLastIdx();
numAddedBufVals = (currIdx - lastAddedIdx + bufSize) % bufSize; // Number of values added to buffer since last display
if (dataIntv != oldDataIntv || count == 1) {
// new data interval selected by user
intvBufSize = cHeight * dataIntv;
numWndVals = min(count, (cHeight - 60) * dataIntv);
bufStart = max(0, count - numWndVals);
lastAddedIdx = currIdx;
oldDataIntv = dataIntv;
} else {
numWndVals = numWndVals + numAddedBufVals;
lastAddedIdx = currIdx;
if (count == bufSize) {
bufStart = max(0, bufStart - numAddedBufVals);
}
}
logger->logDebug(GwLog::DEBUG, "PageWindPlot Dataset: count: %d, xWD: %.1f, xWS: %.2f, xWD_valid? %d, intvBufSize: %d, numWndVals: %d, bufStart: %d, numAddedBufVals: %d, lastIdx: %d, wind source: %s",
count, wdHstry->getLast() / 1000.0 * radToDeg, wsHstry->getLast() / 1000.0 * 1.94384, BDataValid[0], intvBufSize, numWndVals, bufStart, numAddedBufVals, wdHstry->getLastIdx(),
showTruW ? "True" : "App");
// Set wndCenter from 1st real buffer value
if (wndCenter == INT_MAX || (wndCenter == 0 && count == 1)) {
wndCenter = getCntr(*wdHstry, numWndVals);
logger->logDebug(GwLog::DEBUG, "PageWindPlot Range Init: count: %d, xWD: %.1f, wndCenter: %d, diffRng: %d, chrtRng: %d, Min: %.0f, Max: %.0f", count, wdHstry->getLast() / 1000.0 * radToDeg,
wndCenter, diffRng, chrtRng, wdHstry->getMin(numWndVals) / 1000.0 * radToDeg, wdHstry->getMax(numWndVals) / 1000.0 * radToDeg);
} else {
// check and adjust range between left, center, and right chart limit
diffRng = getRng(*wdHstry, wndCenter, numWndVals);
diffRng = (diffRng == wdMAX_VAL ? 0 : diffRng);
if (diffRng > chrtRng) {
chrtRng = int((diffRng + (diffRng >= 0 ? 9 : -1)) / 10) * 10; // Round up to next 10 degree value
} else if (diffRng + 10 < chrtRng) { // Reduce chart range for higher resolution if possible
chrtRng = max(dfltRng, int((diffRng + (diffRng >= 0 ? 9 : -1)) / 10) * 10);
logger->logDebug(GwLog::DEBUG, "PageWindPlot Range adjust: wndCenter: %d, diffRng: %d, chrtRng: %d, Min: %.0f, Max: %.0f", wndCenter, diffRng, chrtRng,
wdHstry->getMin(numWndVals) / 1000.0 * radToDeg, wdHstry->getMax(numWndVals) / 1000.0 * radToDeg);
}
}
chrtScl = float(width) / float(chrtRng) / 2.0; // Chart scale: pixels per degree
wndLeft = wndCenter - chrtRng;
if (wndLeft < 0)
wndLeft += 360;
wndRight = (chrtRng < 180 ? wndCenter + chrtRng : wndCenter + chrtRng - 1);
if (wndRight >= 360)
wndRight -= 360;
// Draw page // Draw page
//*********************************************************************** //***********************************************************
// Set display in partial refresh mode // Set display in partial refresh mode
epd->setPartialWindow(0, 0, width, height); // Set partial update epd->setPartialWindow(0, 0, epd->width(), epd->height());
epd->setTextColor(commonData->fgcolor); epd->setTextColor(commonData->fgcolor);
// chart lines if (chrtMode == DIRECTION) {
epd->fillRect(0, yOffset, width, 2, commonData->fgcolor); if (wdChart) {
epd->fillRect(xCenter, yOffset, 1, cHeight, commonData->fgcolor); wdChart->showChrt(VERTICAL, FULL_SIZE, dataIntv, PRNT_NAME, PRNT_VALUE, *wdBVal);
// chart labels
char sWndLbl[4]; // char buffer for Wind angle label
epd->setFont(&Ubuntu_Bold12pt8b);
epd->setCursor(xCenter - 88, yOffset - 3);
epd->print(wdName); // Wind data name
snprintf(sWndLbl, 4, "%03d", (wndCenter < 0) ? (wndCenter + 360) : wndCenter);
drawTextCenter(xCenter, yOffset - 11, sWndLbl);
epd->drawCircle(xCenter + 25, yOffset - 17, 2, commonData->fgcolor); // <degree> symbol
epd->drawCircle(xCenter + 25, yOffset - 17, 3, commonData->fgcolor); // <degree> symbol
epd->setCursor(1, yOffset - 3);
snprintf(sWndLbl, 4, "%03d", (wndLeft < 0) ? (wndLeft + 360) : wndLeft);
epd->print(sWndLbl); // Wind left value
epd->drawCircle(46, yOffset - 17, 2, commonData->fgcolor); // <degree> symbol
epd->drawCircle(46, yOffset - 17, 3, commonData->fgcolor); // <degree> symbol
epd->setCursor(width - 50, yOffset - 3);
snprintf(sWndLbl, 4, "%03d", (wndRight < 0) ? (wndRight + 360) : wndRight);
epd->print(sWndLbl); // Wind right value
epd->drawCircle(width - 5, yOffset - 17, 2, commonData->fgcolor); // <degree> symbol
epd->drawCircle(width - 5, yOffset - 17, 3, commonData->fgcolor); // <degree> symbol
if (wdHstry->getMax() == wdMAX_VAL) {
// only <MAX_VAL> values in buffer -> no valid wind data available
wndDataValid = false;
} else if (!BDataValid[0] && !simulation) {
// currently no valid xWD data available and no simulation mode
numNoData++;
wndDataValid = true;
if (numNoData > 3) {
// If more than 4 invalid values in a row, send message
wndDataValid = false;
} }
} else {
numNoData = 0; // reset data error counter } else if (chrtMode == SPEED) {
wndDataValid = true; // At least some wind data available if (wsChart) {
wsChart->showChrt(HORIZONTAL, FULL_SIZE, dataIntv, PRNT_NAME, PRNT_VALUE, *wsBVal);
} }
// Draw wind values in chart
//***********************************************************************
if (wndDataValid) {
for (int i = 0; i < (numWndVals / dataIntv); i++) {
chrtVal = static_cast<int>(wdHstry->get(bufStart + (i * dataIntv))); // show the latest wind values in buffer; keep 1st value constant in a rolling buffer
if (chrtVal == wdMAX_VAL) {
chrtPrevVal = wdMAX_VAL;
} else {
chrtVal = static_cast<int>((chrtVal / 1000.0 * radToDeg) + 0.5); // Convert to degrees and round
x = ((chrtVal - wndLeft + 360) % 360) * chrtScl;
y = yOffset + cHeight - i; // Position in chart area
if (i >= (numWndVals / dataIntv) - 1) // log chart data of 1 line (adjust for test purposes) } else if (chrtMode == BOTH) {
logger->logDebug(GwLog::DEBUG, "PageWindPlot Chart: i: %d, chrtVal: %d, bufStart: %d, count: %d, linesToShow: %d", i, chrtVal, bufStart, count, (numWndVals / dataIntv)); if (wdChart) {
wdChart->showChrt(VERTICAL, HALF_SIZE_LEFT, dataIntv, PRNT_NAME, PRNT_VALUE, *wdBVal);
if ((i == 0) || (chrtPrevVal == wdMAX_VAL)) { }
// just a dot for 1st chart point or after some invalid values if (wsChart) {
prevX = x; wsChart->showChrt(VERTICAL, HALF_SIZE_RIGHT, dataIntv, PRNT_NAME, PRNT_VALUE, *wsBVal);
prevY = y;
} else {
// cross borders check; shift values to [-180..0..180]; when crossing borders, range is 2x 180 degrees
int wndLeftDlt = -180 - ((wndLeft >= 180) ? (wndLeft - 360) : wndLeft);
int chrtVal180 = ((chrtVal + wndLeftDlt + 180) % 360 + 360) % 360 - 180;
int chrtPrevVal180 = ((chrtPrevVal + wndLeftDlt + 180) % 360 + 360) % 360 - 180;
if (((chrtPrevVal180 >= -180) && (chrtPrevVal180 < -90) && (chrtVal180 > 90)) || ((chrtPrevVal180 <= 179) && (chrtPrevVal180 > 90) && chrtVal180 <= -90)) {
// If current value crosses chart borders compared to previous value, split line
int xSplit = (((chrtPrevVal180 > 0 ? wndRight : wndLeft) - wndLeft + 360) % 360) * chrtScl;
epd->drawLine(prevX, prevY, xSplit, y, commonData->fgcolor);
epd->drawLine(prevX, prevY - 1, ((xSplit != prevX) ? xSplit : xSplit - 1), ((xSplit != prevX) ? y - 1 : y), commonData->fgcolor);
prevX = (((chrtVal180 > 0 ? wndRight : wndLeft) - wndLeft + 360) % 360) * chrtScl;
} }
} }
// Draw line with 2 pixels width + make sure vertical line are drawn correctly // logger->logDebug(GwLog::DEBUG, "PageWindPlot: page time %ldms", millis() - pageTime);
epd->drawLine(prevX, prevY, x, y, commonData->fgcolor);
epd->drawLine(prevX, prevY - 1, ((x != prevX) ? x : x - 1), ((x != prevX) ? y - 1 : y), commonData->fgcolor);
chrtPrevVal = chrtVal;
prevX = x;
prevY = y;
}
// Reaching chart area top end
if (i >= (cHeight - 1)) {
oldDataIntv = 0; // force reset of buffer start and number of values to show in next display loop
int minWndDir = wdHstry->getMin(numWndVals) / 1000.0 * radToDeg;
int maxWndDir = wdHstry->getMax(numWndVals) / 1000.0 * radToDeg;
logger->logDebug(GwLog::DEBUG, "PageWindPlot FreeTop: Minimum: %d, Maximum: %d, OldwndCenter: %d", minWndDir, maxWndDir, wndCenter);
// if (((minWndDir - wndCenter >= 0) && (minWndDir - wndCenter < 180)) || ((maxWndDir - wndCenter <= 0) && (maxWndDir - wndCenter >=180))) {
if ((wndRight > wndCenter && (minWndDir >= wndCenter && minWndDir <= wndRight)) || (wndRight <= wndCenter && (minWndDir >= wndCenter || minWndDir <= wndRight)) || (wndLeft < wndCenter && (maxWndDir <= wndCenter && maxWndDir >= wndLeft)) || (wndLeft >= wndCenter && (maxWndDir <= wndCenter || maxWndDir >= wndLeft))) {
// Check if all wind value are left or right of center value -> optimize chart center
wndCenter = getCntr(*wdHstry, numWndVals);
}
logger->logDebug(GwLog::DEBUG, "PageWindPlot FreeTop: cHeight: %d, bufStart: %d, numWndVals: %d, wndCenter: %d", cHeight, bufStart, numWndVals, wndCenter);
break;
}
}
// Print wind speed value
int currentZone;
static int lastZone = 0;
static bool flipTws = false;
int xPosTws;
static const int yPosTws = yOffset + 40;
xPosTws = flipTws ? 20 : width - 145;
currentZone = (y >= yPosTws - 38) && (y <= yPosTws + 6) && (x >= xPosTws - 4) && (x <= xPosTws + 146) ? 1 : 0; // Define current zone for TWS value
if (currentZone != lastZone) {
// Only flip when x moves to a different zone
if ((y >= yPosTws - 38) && (y <= yPosTws + 6) && (x >= xPosTws - 4) && (x <= xPosTws + 146)) {
flipTws = !flipTws;
xPosTws = flipTws ? 20 : width - 145;
}
}
lastZone = currentZone;
wsValue = wsHstry->getLast();
wsBVal->value = wsValue / 1000.0; // temp variable to retreive data unit from OBP60Formater
wsBVal->valid = (static_cast<uint16_t>(wsValue) != wsHstry->getMinVal());
String swsValue = commonData->fmt->formatValue(wsBVal, *commonData).svalue; // value (string)
wsUnit = commonData->fmt->formatValue(wsBVal, *commonData).unit; // Unit of value
epd->fillRect(xPosTws - 4, yPosTws - 38, 142, 44, commonData->bgcolor); // Clear area for TWS value
epd->setFont(&DSEG7Classic_BoldItalic16pt7b);
epd->setCursor(xPosTws, yPosTws);
epd->print(swsValue); // Value
/* if (!wsBVal->valid) {
epd->print("--.-");
} else {
wsValue = wsValue / 10.0 * 1.94384; // Wind speed value in knots
if (wsValue < 10.0) {
epd->printf("!%3.1f", wsValue); // Value, round to 1 decimal
} else {
epd->printf("%4.1f", wsValue); // Value, round to 1 decimal
}
} */
epd->setFont(&Ubuntu_Bold12pt8b);
epd->setCursor(xPosTws + 82, yPosTws - 14);
epd->print(wsName); // Name
epd->setFont(&Ubuntu_Bold8pt8b);
epd->setCursor(xPosTws + 82, yPosTws + 1);
epd->print(wsUnit); // Unit
} else {
// No valid data available
LOG_DEBUG(GwLog::LOG, "PageWindPlot: No valid data available");
epd->setFont(&Ubuntu_Bold10pt8b);
epd->fillRect(xCenter - 33, height / 2 - 20, 66, 24, commonData->bgcolor); // Clear area for message
drawTextCenter(xCenter, height / 2 - 10, "No data");
}
// chart Y axis labels; print at last to overwrite potential chart lines in label area
int yPos;
int chrtLbl;
epd->setFont(&Ubuntu_Bold8pt8b);
for (int i = 1; i <= 3; i++) {
yPos = yOffset + (i * 60);
epd->fillRect(0, yPos, width, 1, commonData->fgcolor);
epd->fillRect(0, yPos - 8, 24, 16, commonData->bgcolor); // Clear small area to remove potential chart lines
epd->setCursor(1, yPos + 4);
if (count >= intvBufSize) {
// Calculate minute value for label
chrtLbl = ((i - 1 + (prevY < yOffset + 30)) * dataIntv) * -1; // change label if last data point is more than 30 lines (= seconds) from chart line
} else {
int j = 3 - i;
chrtLbl = (int((((numWndVals / dataIntv) - 50) * dataIntv / 60) + 1) - (j * dataIntv)) * -1; // 50 lines left below last chart line
}
epd->printf("%3d", chrtLbl); // Wind value label
}
logger->logDebug(GwLog::DEBUG, "PageWindPlot time: %ld", millis() - timer);
return PAGE_UPDATE; return PAGE_UPDATE;
}; }
}; };
static Page* createPage(CommonData& common) static Page* createPage(CommonData& common)
@@ -512,7 +256,7 @@ PageDescription registerPageWindPlot(
"WindPlot", // Page name "WindPlot", // Page name
createPage, // Action createPage, // Action
0, // Number of bus values depends on selection in Web configuration 0, // Number of bus values depends on selection in Web configuration
{ "TWD", "AWD"}, // Bus values we need in the page { "TWD", "TWS", "AWD", "AWS" }, // Bus values we need in the page
true // Show display header on/off true // Show display header on/off
); );

View File

@@ -4,10 +4,6 @@
#include "Pagedata.h" #include "Pagedata.h"
#include "OBP60Extensions.h" #include "OBP60Extensions.h"
#ifdef ENABLE_CALIBRATION
#include "BoatDataCalibration.h"
#endif
class PageWindRose : public Page class PageWindRose : public Page
{ {
private: private:
@@ -45,20 +41,18 @@ public:
int displayPage(PageData &pageData) { int displayPage(PageData &pageData) {
// storage for hold valued // storage for hold values
static FormattedData bvf_awa_old; static FormattedData bvf_awa_old;
static FormattedData bvf_aws_old; static FormattedData bvf_aws_old;
static FormattedData bvf_twd_old; static FormattedData bvf_twd_old;
static FormattedData bvf_tws_old; static FormattedData bvf_tws_old;
static FormattedData bvf_dbt_old; static FormattedData bvf_dbt_old;
static FormattedData bvf_stw_old; static FormattedData bvf_stw_old;
// units? why?
// Get boat value for AWA // Get boat value for AWA
GwApi::BoatValue *bv_awa = pageData.values[0]; // First element in list GwApi::BoatValue *bv_awa = pageData.values[0]; // First element in list
String name_awa = xdrDelete(bv_awa->getName(), 6); // get name without prefix and limit length String name_awa = xdrDelete(bv_awa->getName(), 6); // get name without prefix and limit length
#ifdef ENABLE_CALIBRATION
calibrationData.calibrateInstance(bv_awa, logger); // Check if boat data value is to be calibrated
#endif
FormattedData bvf_awa = commonData->fmt->formatValue(bv_awa, *commonData); FormattedData bvf_awa = commonData->fmt->formatValue(bv_awa, *commonData);
if (bv_awa->valid) { // Save formatted data for hold feature if (bv_awa->valid) { // Save formatted data for hold feature
bvf_awa_old = bvf_awa; bvf_awa_old = bvf_awa;
@@ -67,9 +61,6 @@ public:
// Get boat value for AWS // Get boat value for AWS
GwApi::BoatValue *bv_aws = pageData.values[1]; // Second element in list GwApi::BoatValue *bv_aws = pageData.values[1]; // Second element in list
String name_aws = xdrDelete(bv_aws->getName(), 6); // get name without prefix and limit length String name_aws = xdrDelete(bv_aws->getName(), 6); // get name without prefix and limit length
#ifdef ENABLE_CALIBRATION
calibrationData.calibrateInstance(bv_aws, logger); // Check if boat data value is to be calibrated
#endif
FormattedData bvf_aws = commonData->fmt->formatValue(bv_aws, *commonData); FormattedData bvf_aws = commonData->fmt->formatValue(bv_aws, *commonData);
if (bv_aws->valid) { // Save formatted data for hold feature if (bv_aws->valid) { // Save formatted data for hold feature
bvf_aws_old = bvf_aws; bvf_aws_old = bvf_aws;

View File

@@ -6,19 +6,21 @@
#include <functional> #include <functional>
#include <vector> #include <vector>
#include "LedSpiTask.h" #include "LedSpiTask.h"
#include "OBPDataOperations.h" // #include "OBPDataOperations.h"
#define MAX_PAGE_NUMBER 10 // Max number of pages for show data #define MAX_PAGE_NUMBER 10 // Max number of pages for show data
typedef std::vector<GwApi::BoatValue *> ValueList; typedef std::vector<GwApi::BoatValue *> ValueList;
class HstryBuffers;
typedef struct{ typedef struct{
GwApi *api; GwApi *api;
String pageName; String pageName;
uint8_t pageNumber; // page number in sequence of visible pages uint8_t pageNumber; // page number in sequence of visible pages
//the values will always contain the user defined values first //the values will always contain the user defined values first
ValueList values; ValueList values;
HstryBuf* boatHstry; HstryBuffers* hstryBuffers; // list of all boat history buffers
} PageData; } PageData;
// Sensor data structure (only for extended sensors, not for NMEA bus sensors) // Sensor data structure (only for extended sensors, not for NMEA bus sensors)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,29 @@
# PlatformIO extra script for obp60task # PlatformIO extra script for obp60task
import subprocess
def cleanup_patches(source, target, env):
for p in patchfiles:
patch = os.path.join(patchdir, p)
print(f"removing {patch}")
res = subprocess.run(["git", "apply", "-R", patch], capture_output=True, text=True)
if res.returncode != 0:
print(res.stderr)
patching = False
epdtype = "unknown" epdtype = "unknown"
pcbvers = "unknown" pcbvers = "unknown"
for x in env["BUILD_FLAGS"]: for x in env["BUILD_FLAGS"]:
if x.startswith("-D HARDWARE_"): if not x.startswith('-D'):
continue
opt = x[2:].strip()
if opt.startswith("HARDWARE_"):
pcbvers = x.split('_')[1] pcbvers = x.split('_')[1]
if x.startswith("-D DISPLAY_"): elif opt.startswith("DISPLAY_"):
epdtype = x.split('_')[1] epdtype = x.split('_')[1]
elif opt == 'ENABLE_PATCHES':
patching = True
propfilename = os.path.join(env["PROJECT_LIBDEPS_DIR"], env["PIOENV"], "GxEPD2/library.properties") propfilename = os.path.join(env["PROJECT_LIBDEPS_DIR"], env["PIOENV"], "GxEPD2/library.properties")
properties = {} properties = {}
@@ -29,3 +46,22 @@ env["CPPDEFINES"].extend([("BOARD", env["BOARD"]), ("EPDTYPE", epdtype), ("PCBVE
print("added hardware info to CPPDEFINES") print("added hardware info to CPPDEFINES")
print("friendly board name is '{}'".format(env.GetProjectOption("board_name"))) print("friendly board name is '{}'".format(env.GetProjectOption("board_name")))
if patching:
# apply patches to gateway code
print("applying gateway patches")
patchdir = os.path.join(os.path.dirname(script), "patches")
if not os.path.isdir(patchdir):
print("patchdir not found, no patches applied")
else:
patchfiles = [f for f in os.listdir(patchdir)]
if len(patchfiles) > 0:
for p in patchfiles:
patch = os.path.join(patchdir, p)
print(f"applying {patch}")
res = subprocess.run(["git", "apply", patch], capture_output=True, text=True)
if res.returncode != 0:
print(res.stderr)
env.AddPostAction("$PROGPATH", cleanup_patches)
else:
print("no patches found")

View File

@@ -0,0 +1,202 @@
const uint8_t IBM8x8pxBitmaps[] PROGMEM = {
0x00, /* 0x20 space */
0x6F, 0xF6, 0x60, 0x60, /* 0x21 exclam */
0xDE, 0xF6, /* 0x22 quotedbl */
0x6C, 0xDB, 0xFB, 0x6F, 0xED, 0x9B, 0x00, /* 0x23 numbersign */
0x31, 0xFC, 0x1E, 0x0F, 0xE3, 0x00, /* 0x24 dollar */
0xC7, 0x98, 0x61, 0x86, 0x78, 0xC0, /* 0x25 percent */
0x38, 0xD8, 0xE3, 0xBD, 0xD9, 0x9D, 0x80, /* 0x26 ampersand */
0x6F, 0x00, /* 0x27 quotesingle */
0x36, 0xCC, 0xC6, 0x30, /* 0x28 parenleft */
0xC6, 0x33, 0x36, 0xC0, /* 0x29 parenright */
0x66, 0x3C, 0xFF, 0x3C, 0x66, /* 0x2A asterisk */
0x30, 0xCF, 0xCC, 0x30, /* 0x2B plus */
0x6F, 0x00, /* 0x2C comma */
0xFC, /* 0x2D hyphen */
0xF0, /* 0x2E period */
0x06, 0x18, 0x61, 0x86, 0x18, 0x20, 0x00, /* 0x2F slash */
0x7D, 0x8F, 0x3E, 0xFF, 0x7C, 0xDF, 0x00, /* 0x30 zero */
0x31, 0xC3, 0x0C, 0x30, 0xCF, 0xC0, /* 0x31 one */
0x7B, 0x30, 0xCE, 0x63, 0x1F, 0xC0, /* 0x32 two */
0x7B, 0x30, 0xCE, 0x0F, 0x37, 0x80, /* 0x33 three */
0x1C, 0x79, 0xB6, 0x6F, 0xE1, 0x87, 0x80, /* 0x34 four */
0xFF, 0x0F, 0x83, 0x0F, 0x37, 0x80, /* 0x35 five */
0x39, 0x8C, 0x3E, 0xCF, 0x37, 0x80, /* 0x36 six */
0xFF, 0x30, 0xC6, 0x30, 0xC3, 0x00, /* 0x37 seven */
0x7B, 0x3C, 0xDE, 0xCF, 0x37, 0x80, /* 0x38 eight */
0x7B, 0x3C, 0xDF, 0x0C, 0x67, 0x00, /* 0x39 nine */
0xF0, 0xF0, /* 0x3A colon */
0x6C, 0x37, 0x80, /* 0x3B semicolon */
0x19, 0x99, 0x86, 0x18, 0x60, /* 0x3C less */
0xFC, 0x00, 0x3F, /* 0x3D equal */
0xC3, 0x0C, 0x33, 0x33, 0x00, /* 0x3E greater */
0x7B, 0x30, 0xC6, 0x30, 0x03, 0x00, /* 0x3F question */
0x7D, 0x8F, 0x7E, 0xFD, 0xF8, 0x1E, 0x00, /* 0x40 at */
0x31, 0xEC, 0xF3, 0xFF, 0x3C, 0xC0, /* 0x41 A */
0xFC, 0xCD, 0x9B, 0xE6, 0x6C, 0xFF, 0x00, /* 0x42 B */
0x3C, 0xCF, 0x06, 0x0C, 0x0C, 0xCF, 0x00, /* 0x43 C */
0xF8, 0xD9, 0x9B, 0x36, 0x6D, 0xBE, 0x00, /* 0x44 D */
0xFE, 0xC5, 0xA3, 0xC6, 0x8C, 0x7F, 0x80, /* 0x45 E */
0xFE, 0xC5, 0xA3, 0xC6, 0x8C, 0x3C, 0x00, /* 0x46 F */
0x3C, 0xCF, 0x06, 0x0C, 0xEC, 0xCF, 0x80, /* 0x47 G */
0xCF, 0x3C, 0xFF, 0xCF, 0x3C, 0xC0, /* 0x48 H */
0xF6, 0x66, 0x66, 0xF0, /* 0x49 I */
0x1E, 0x18, 0x30, 0x6C, 0xD9, 0x9E, 0x00, /* 0x4A J */
0xE6, 0xCD, 0xB3, 0xC6, 0xCC, 0xF9, 0x80, /* 0x4B K */
0xF0, 0xC1, 0x83, 0x06, 0x2C, 0xFF, 0x80, /* 0x4C L */
0xC7, 0xDF, 0xFF, 0xFD, 0x78, 0xF1, 0x80, /* 0x4D M */
0xC7, 0xCF, 0xDE, 0xFC, 0xF8, 0xF1, 0x80, /* 0x4E N */
0x38, 0xDB, 0x1E, 0x3C, 0x6D, 0x8E, 0x00, /* 0x4F O */
0xFC, 0xCD, 0x9B, 0xE6, 0x0C, 0x3C, 0x00, /* 0x50 P */
0x7B, 0x3C, 0xF3, 0xDD, 0xE1, 0xC0, /* 0x51 Q */
0xFC, 0xCD, 0x9B, 0xE6, 0xCC, 0xF9, 0x80, /* 0x52 R */
0x7B, 0x3E, 0x1C, 0x1F, 0x37, 0x80, /* 0x53 S */
0xFE, 0xD3, 0x0C, 0x30, 0xC7, 0x80, /* 0x54 T */
0xCF, 0x3C, 0xF3, 0xCF, 0x3F, 0xC0, /* 0x55 U */
0xCF, 0x3C, 0xF3, 0xCD, 0xE3, 0x00, /* 0x56 V */
0xC7, 0x8F, 0x1E, 0xBF, 0xFD, 0xF1, 0x80, /* 0x57 W */
0xC7, 0x8D, 0xB1, 0xC3, 0x8D, 0xB1, 0x80, /* 0x58 X */
0xCF, 0x3C, 0xDE, 0x30, 0xC7, 0x80, /* 0x59 Y */
0xFF, 0x8E, 0x30, 0xC3, 0x2C, 0xFF, 0x80, /* 0x5A Z */
0xFC, 0xCC, 0xCC, 0xF0, /* 0x5B bracketleft */
0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0x80, /* 0x5C backslash */
0xF3, 0x33, 0x33, 0xF0, /* 0x5D bracketright */
0x10, 0x71, 0xB6, 0x30, /* 0x5E asciicircum */
0xFF, /* 0x5F underscore */
0xD9, 0x80, /* 0x60 grave */
0x78, 0x19, 0xF6, 0x67, 0x60, /* 0x61 a */
0xE0, 0xC1, 0x83, 0xE6, 0x6C, 0xF7, 0x00, /* 0x62 b */
0x7B, 0x3C, 0x33, 0x78, /* 0x63 c */
0x1C, 0x18, 0x33, 0xEC, 0xD9, 0x9D, 0x80, /* 0x64 d */
0x7B, 0x3F, 0xF0, 0x78, /* 0x65 e */
0x39, 0xB6, 0x3C, 0x61, 0x8F, 0x00, /* 0x66 f */
0x77, 0x9B, 0x33, 0xE0, 0xDF, 0x00, /* 0x67 g */
0xE0, 0xC1, 0xB3, 0xB6, 0x6C, 0xF9, 0x80, /* 0x68 h */
0x60, 0xE6, 0x66, 0xF0, /* 0x69 i */
0x0C, 0x00, 0xC3, 0x0F, 0x3C, 0xDE, /* 0x6A j */
0xE0, 0xC1, 0x9B, 0x67, 0x8D, 0xB9, 0x80, /* 0x6B k */
0xE6, 0x66, 0x66, 0xF0, /* 0x6C l */
0xCD, 0xFF, 0xFE, 0xBC, 0x60, /* 0x6D m */
0xFB, 0x3C, 0xF3, 0xCC, /* 0x6E n */
0x7B, 0x3C, 0xF3, 0x78, /* 0x6F o */
0xDC, 0xCD, 0x9B, 0xE6, 0x1E, 0x00, /* 0x70 p */
0x77, 0x9B, 0x33, 0xE0, 0xC3, 0xC0, /* 0x71 q */
0xDC, 0xED, 0x9B, 0x0F, 0x00, /* 0x72 r */
0x7F, 0x07, 0x83, 0xF8, /* 0x73 s */
0x23, 0x3E, 0xC6, 0x34, 0xC0, /* 0x74 t */
0xCD, 0x9B, 0x36, 0x67, 0x60, /* 0x75 u */
0xCF, 0x3C, 0xDE, 0x30, /* 0x76 v */
0xC7, 0xAF, 0xFF, 0xF6, 0xC0, /* 0x77 w */
0xC6, 0xD8, 0xE3, 0x6C, 0x60, /* 0x78 x */
0xCF, 0x3C, 0xDF, 0x0F, 0xE0, /* 0x79 y */
0xFE, 0x63, 0x19, 0xFC, /* 0x7A z */
0x1C, 0xC3, 0x38, 0x30, 0xC1, 0xC0, /* 0x7B braceleft */
0xFC, 0xFC, /* 0x7C bar */
0xE0, 0xC3, 0x07, 0x30, 0xCE, 0x00, /* 0x7D braceright */
0x77, 0xB8, /* 0x7E asciitilde */
0x10, 0x71, 0xB6, 0x3C, 0x7F, 0xC0 /* 0x7F uni007F */
};
const GFXglyph IBM8x8pxGlyphs[] PROGMEM = {
{ 0, 1, 1, 2, 0, -1 }, /* 0x20 space */
{ 1, 4, 7, 5, 0, -7 }, /* 0x21 exclam */
{ 5, 5, 3, 6, 0, -7 }, /* 0x22 quotedbl */
{ 7, 7, 7, 8, 0, -7 }, /* 0x23 numbersign */
{ 14, 6, 7, 7, 0, -7 }, /* 0x24 dollar */
{ 20, 7, 6, 8, 0, -6 }, /* 0x25 percent */
{ 26, 7, 7, 8, 0, -7 }, /* 0x26 ampersand */
{ 33, 3, 3, 4, 0, -7 }, /* 0x27 quotesingle */
{ 35, 4, 7, 5, 0, -7 }, /* 0x28 parenleft */
{ 39, 4, 7, 5, 0, -7 }, /* 0x29 parenright */
{ 43, 8, 5, 9, 0, -6 }, /* 0x2A asterisk */
{ 48, 6, 5, 7, 0, -6 }, /* 0x2B plus */
{ 52, 3, 3, 4, 0, -2 }, /* 0x2C comma */
{ 54, 6, 1, 7, 0, -4 }, /* 0x2D hyphen */
{ 55, 2, 2, 3, 0, -2 }, /* 0x2E period */
{ 56, 7, 7, 8, 0, -7 }, /* 0x2F slash */
{ 63, 7, 7, 8, 0, -7 }, /* 0x30 zero */
{ 70, 6, 7, 7, 0, -7 }, /* 0x31 one */
{ 76, 6, 7, 7, 0, -7 }, /* 0x32 two */
{ 82, 6, 7, 7, 0, -7 }, /* 0x33 three */
{ 88, 7, 7, 8, 0, -7 }, /* 0x34 four */
{ 95, 6, 7, 7, 0, -7 }, /* 0x35 five */
{ 101, 6, 7, 7, 0, -7 }, /* 0x36 six */
{ 107, 6, 7, 7, 0, -7 }, /* 0x37 seven */
{ 113, 6, 7, 7, 0, -7 }, /* 0x38 eight */
{ 119, 6, 7, 7, 0, -7 }, /* 0x39 nine */
{ 125, 2, 6, 3, 0, -6 }, /* 0x3A colon */
{ 127, 3, 6, 4, 0, -6 }, /* 0x3B semicolon */
{ 130, 5, 7, 6, 0, -7 }, /* 0x3C less */
{ 135, 6, 4, 7, 0, -5 }, /* 0x3D equal */
{ 138, 5, 7, 6, 0, -7 }, /* 0x3E greater */
{ 143, 6, 7, 7, 0, -7 }, /* 0x3F question */
{ 149, 7, 7, 8, 0, -7 }, /* 0x40 at */
{ 156, 6, 7, 7, 0, -7 }, /* 0x41 A */
{ 162, 7, 7, 8, 0, -7 }, /* 0x42 B */
{ 169, 7, 7, 8, 0, -7 }, /* 0x43 C */
{ 176, 7, 7, 8, 0, -7 }, /* 0x44 D */
{ 183, 7, 7, 8, 0, -7 }, /* 0x45 E */
{ 190, 7, 7, 8, 0, -7 }, /* 0x46 F */
{ 197, 7, 7, 8, 0, -7 }, /* 0x47 G */
{ 204, 6, 7, 7, 0, -7 }, /* 0x48 H */
{ 210, 4, 7, 5, 0, -7 }, /* 0x49 I */
{ 214, 7, 7, 8, 0, -7 }, /* 0x4A J */
{ 221, 7, 7, 8, 0, -7 }, /* 0x4B K */
{ 228, 7, 7, 8, 0, -7 }, /* 0x4C L */
{ 235, 7, 7, 8, 0, -7 }, /* 0x4D M */
{ 242, 7, 7, 8, 0, -7 }, /* 0x4E N */
{ 249, 7, 7, 8, 0, -7 }, /* 0x4F O */
{ 256, 7, 7, 8, 0, -7 }, /* 0x50 P */
{ 263, 6, 7, 7, 0, -7 }, /* 0x51 Q */
{ 269, 7, 7, 8, 0, -7 }, /* 0x52 R */
{ 276, 6, 7, 7, 0, -7 }, /* 0x53 S */
{ 282, 6, 7, 7, 0, -7 }, /* 0x54 T */
{ 288, 6, 7, 7, 0, -7 }, /* 0x55 U */
{ 294, 6, 7, 7, 0, -7 }, /* 0x56 V */
{ 300, 7, 7, 8, 0, -7 }, /* 0x57 W */
{ 307, 7, 7, 8, 0, -7 }, /* 0x58 X */
{ 314, 6, 7, 7, 0, -7 }, /* 0x59 Y */
{ 320, 7, 7, 8, 0, -7 }, /* 0x5A Z */
{ 327, 4, 7, 5, 0, -7 }, /* 0x5B bracketleft */
{ 331, 7, 7, 8, 0, -7 }, /* 0x5C backslash */
{ 338, 4, 7, 5, 0, -7 }, /* 0x5D bracketright */
{ 342, 7, 4, 8, 0, -7 }, /* 0x5E asciicircum */
{ 346, 8, 1, 9, 0, 0 }, /* 0x5F underscore */
{ 347, 3, 3, 4, 0, -7 }, /* 0x60 grave */
{ 349, 7, 5, 8, 0, -5 }, /* 0x61 a */
{ 354, 7, 7, 8, 0, -7 }, /* 0x62 b */
{ 361, 6, 5, 7, 0, -5 }, /* 0x63 c */
{ 365, 7, 7, 8, 0, -7 }, /* 0x64 d */
{ 372, 6, 5, 7, 0, -5 }, /* 0x65 e */
{ 376, 6, 7, 7, 0, -7 }, /* 0x66 f */
{ 382, 7, 6, 8, 0, -5 }, /* 0x67 g */
{ 388, 7, 7, 8, 0, -7 }, /* 0x68 h */
{ 395, 4, 7, 5, 0, -7 }, /* 0x69 i */
{ 399, 6, 8, 7, 0, -7 }, /* 0x6A j */
{ 405, 7, 7, 8, 0, -7 }, /* 0x6B k */
{ 412, 4, 7, 5, 0, -7 }, /* 0x6C l */
{ 416, 7, 5, 8, 0, -5 }, /* 0x6D m */
{ 421, 6, 5, 7, 0, -5 }, /* 0x6E n */
{ 425, 6, 5, 7, 0, -5 }, /* 0x6F o */
{ 429, 7, 6, 8, 0, -5 }, /* 0x70 p */
{ 435, 7, 6, 8, 0, -5 }, /* 0x71 q */
{ 441, 7, 5, 8, 0, -5 }, /* 0x72 r */
{ 446, 6, 5, 7, 0, -5 }, /* 0x73 s */
{ 450, 5, 7, 6, 0, -7 }, /* 0x74 t */
{ 455, 7, 5, 8, 0, -5 }, /* 0x75 u */
{ 460, 6, 5, 7, 0, -5 }, /* 0x76 v */
{ 464, 7, 5, 8, 0, -5 }, /* 0x77 w */
{ 469, 7, 5, 8, 0, -5 }, /* 0x78 x */
{ 474, 6, 6, 7, 0, -5 }, /* 0x79 y */
{ 479, 6, 5, 7, 0, -5 }, /* 0x7A z */
{ 483, 6, 7, 7, 0, -7 }, /* 0x7B braceleft */
{ 489, 2, 7, 3, 0, -7 }, /* 0x7C bar */
{ 491, 6, 7, 7, 0, -7 }, /* 0x7D braceright */
{ 497, 7, 2, 8, 0, -7 }, /* 0x7E asciitilde */
{ 499, 7, 6, 8, 0, -6 } /* 0x7F uni007F */
};
const GFXfont IBM8x8px PROGMEM = {
(uint8_t *)IBM8x8pxBitmaps,
(GFXglyph *)IBM8x8pxGlyphs,
0x20, 0x7F, 8 };

View File

@@ -20,7 +20,7 @@ import getopt
import re import re
import json import json
__version__ = "1.2" __version__ = "1.3"
def detect_pages(filename): def detect_pages(filename):
# returns a dictionary with page name and the number of gui fields # returns a dictionary with page name and the number of gui fields
@@ -87,6 +87,11 @@ def create_json(device, no_of_pages, pagedata):
output = [] output = []
for page_no in range(1, no_of_pages + 1): for page_no in range(1, no_of_pages + 1):
category = f"{device.upper()} Page {page_no}"
capabilities = {device.lower(): "true"}
visiblepages = [vp for vp in range(page_no, no_of_pages + 1)]
page_data = { page_data = {
"name": f"page{page_no}type", "name": f"page{page_no}type",
"label": "Type", "label": "Type",
@@ -94,9 +99,11 @@ def create_json(device, no_of_pages, pagedata):
"default": get_default_page(page_no), "default": get_default_page(page_no),
"description": f"Type of page for page {page_no}", "description": f"Type of page for page {page_no}",
"list": pages, "list": pages,
"category": f"{device.upper()} Page {page_no}", "category": category,
"capabilities": {device.lower(): "true"}, "capabilities": {device.lower(): "true"},
"condition": [{"visiblePages": vp} for vp in range(page_no, no_of_pages + 1)], "condition": {
"visiblePages": visiblepages
},
#"fields": [], #"fields": [],
} }
output.append(page_data) output.append(page_data)
@@ -108,16 +115,16 @@ def create_json(device, no_of_pages, pagedata):
"type": "boatData", "type": "boatData",
"default": "", "default": "",
"description": "The display for field {}".format(number_to_text(field_no)), "description": "The display for field {}".format(number_to_text(field_no)),
"category": f"{device.upper()} Page {page_no}", "category": category,
"capabilities": {device.lower(): "true"}, "capabilities": capabilities,
"condition": { "condition": {
f"page{page_no}type": [page for page in pages if pagedata[page] >= field_no], f"page{page_no}type": [page for page in pages if pagedata[page] >= field_no],
"visiblePages": [vp for vp in range(page_no, no_of_pages + 1)] "visiblePages": visiblepages
}, }
} }
output.append(field_data) output.append(field_data)
fluid_data ={ fluid_data = {
"name": f"page{page_no}fluid", "name": f"page{page_no}fluid",
"label": "Fluid type", "label": "Fluid type",
"type": "list", "type": "list",
@@ -132,14 +139,32 @@ def create_json(device, no_of_pages, pagedata):
{"l":"Fuel Gasoline (6)","v":"6"} {"l":"Fuel Gasoline (6)","v":"6"}
], ],
"description": "Fluid type in tank", "description": "Fluid type in tank",
"category": f"{device.upper()} Page {page_no}", "category": category,
"capabilities": { "capabilities": capabilities,
device.lower(): "true"
},
"condition":[{f"page{page_no}type":"Fluid"}] "condition":[{f"page{page_no}type":"Fluid"}]
} }
output.append(fluid_data) output.append(fluid_data)
if device.upper() == 'OBP40':
windsource = {
"name": f"page{page_no}wndsrc",
"label": "Wind source",
"type": "list",
"default": "True wind",
"description": f"Wind source for page {page_no}: [true|apparent]",
"list": [
"True wind",
"Apparent wind"
],
"category": category,
"capabilities": capabilities,
"condition": {
f"page{page_no}type": "WindPlot",
"visiblePages": visiblepages
}
}
output.append(windsource)
return json.dumps(output, indent=4) return json.dumps(output, indent=4)
def usage(): def usage():

View File

@@ -0,0 +1,67 @@
#define foxtrot_width 96
#define foxtrot_height 64
static unsigned char foxtrot_bits[] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0xa8, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0xaa, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0x55, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xa0, 0xaa, 0xaa, 0x02, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x50, 0x55, 0x55, 0x05, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xa8, 0xaa, 0xaa, 0x2a, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x54, 0x55, 0x55, 0x55, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x80, 0xaa, 0xaa, 0xaa, 0xaa, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x40, 0x55, 0x55, 0x55, 0x55, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xa0, 0xaa, 0xaa, 0xaa, 0xaa, 0x0a, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x50, 0x55, 0x55, 0x55, 0x55, 0x15, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0x2a, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x00, 0x00, 0x00,
0x00, 0x00, 0x80, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0x02, 0x00, 0x00,
0x00, 0x00, 0x40, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x05, 0x00, 0x00,
0x00, 0x00, 0xa8, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0x0a, 0x00, 0x00,
0x00, 0x00, 0x54, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x15, 0x00, 0x00,
0x00, 0x00, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0x00, 0x00,
0x00, 0x00, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x01, 0x00,
0x00, 0xa0, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0x02, 0x00,
0x00, 0x50, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x05, 0x00,
0x00, 0xa8, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0x2a, 0x00,
0x00, 0x54, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x00,
0x80, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0x00,
0x40, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x01,
0xa0, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0x0a,
0x50, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x15,
0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0x2a,
0x54, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
0xa8, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0x0a,
0x50, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x05,
0x80, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0x02,
0x00, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x01,
0x00, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0x2a, 0x00,
0x00, 0x54, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x15, 0x00,
0x00, 0xa0, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0x0a, 0x00,
0x00, 0x40, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x05, 0x00,
0x00, 0x80, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0x00, 0x00,
0x00, 0x00, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x00, 0x00,
0x00, 0x00, 0xa8, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0x2a, 0x00, 0x00,
0x00, 0x00, 0x50, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x15, 0x00, 0x00,
0x00, 0x00, 0xa0, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0x02, 0x00, 0x00,
0x00, 0x00, 0x40, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x54, 0x55, 0x55, 0x55, 0x55, 0x55, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0xa8, 0xaa, 0xaa, 0xaa, 0xaa, 0x0a, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x50, 0x55, 0x55, 0x55, 0x55, 0x05, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x80, 0xaa, 0xaa, 0xaa, 0xaa, 0x02, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x55, 0x55, 0x55, 0x55, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xaa, 0xaa, 0xaa, 0x2a, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x54, 0x55, 0x55, 0x15, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xa0, 0xaa, 0xaa, 0x0a, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x40, 0x55, 0x55, 0x05, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x80, 0xaa, 0xaa, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0x55, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0xa8, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x15, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };

View File

@@ -14,7 +14,6 @@
#include <GxEPD2_BW.h> // GxEPD2 lib for b/w E-Ink displays #include <GxEPD2_BW.h> // GxEPD2 lib for b/w E-Ink displays
#include "OBP60Extensions.h" // Functions lib for extension board #include "OBP60Extensions.h" // Functions lib for extension board
#include "OBPKeyboardTask.h" // Functions lib for keyboard handling #include "OBPKeyboardTask.h" // Functions lib for keyboard handling
#include "BoatDataCalibration.h" // Functions lib for data instance calibration
#include "OBPDataOperations.h" // Functions lib for data operations such as true wind calculation #include "OBPDataOperations.h" // Functions lib for data operations such as true wind calculation
#include "OBP60QRWiFi.h" // Functions lib for WiFi QR code #include "OBP60QRWiFi.h" // Functions lib for WiFi QR code
#include "OBPSensorTask.h" // Functions lib for sensor data #include "OBPSensorTask.h" // Functions lib for sensor data
@@ -253,6 +252,10 @@ void registerAllPages(GwLog *logger, PageList &list){
list.add(&registerPageFluid); list.add(&registerPageFluid);
extern PageDescription registerPageSkyView; extern PageDescription registerPageSkyView;
list.add(&registerPageSkyView); list.add(&registerPageSkyView);
extern PageDescription registerPageNavigation;
list.add(&registerPageNavigation);
extern PageDescription registerPageDigitalOut;
list.add(&registerPageDigitalOut);
extern PageDescription registerPageAnchor; extern PageDescription registerPageAnchor;
list.add(&registerPageAnchor); list.add(&registerPageAnchor);
extern PageDescription registerPageAIS; extern PageDescription registerPageAIS;
@@ -432,13 +435,12 @@ void OBP60Task(GwApi *api){
#endif #endif
LOG_DEBUG(GwLog::LOG,"...done"); LOG_DEBUG(GwLog::LOG,"...done");
int lastPage=pageNumber; int lastPage=-1; // initialize with an impossible value, so we can detect wether we are during startup and no page has been displayed yet
BoatValueList boatValues; //all the boat values for the api query BoatValueList boatValues; //all the boat values for the api query
HstryBuf hstryBufList(960); // Create ring buffers for history storage of some boat data HstryBuffers hstryBufferList(1920, &boatValues, logger); // Create empty list of boat data history buffers (1.920 values = seconds = 32 min.)
WindUtils trueWind(&boatValues); // Create helper object for true wind calculation WindUtils trueWind(&boatValues, logger); // Create helper object for true wind calculation
//commonData.distanceformat=config->getString(xxx); CalibrationData calibrationDataList(logger); // all boat data types which are supposed to be calibrated
//add all necessary data to common data
//fill the page data from config //fill the page data from config
numPages=config->getInt(config->visiblePages,1); numPages=config->getInt(config->visiblePages,1);
@@ -479,21 +481,26 @@ void OBP60Task(GwApi *api){
LOG_DEBUG(GwLog::DEBUG,"added fixed value %s to page %d",value->getName().c_str(),i); LOG_DEBUG(GwLog::DEBUG,"added fixed value %s to page %d",value->getName().c_str(),i);
pages[i].parameters.values.push_back(value); pages[i].parameters.values.push_back(value);
} }
// Add boat history data to page parameters
pages[i].parameters.boatHstry = &hstryBufList; // Read the specified boat data types of relevant pages and create a history buffer for each type
if (pages[i].parameters.pageName == "OneValue" || pages[i].parameters.pageName == "TwoValues" || pages[i].parameters.pageName == "WindPlot") {
for (auto pVal : pages[i].parameters.values) {
hstryBufferList.addBuffer(pVal->getName());
} }
}
// Add list of history buffers to page parameters
pages[i].parameters.hstryBuffers = &hstryBufferList;
}
// add out of band system page (always available) // add out of band system page (always available)
Page *syspage = allPages.pages[0]->creator(commonData); Page *syspage = allPages.pages[0]->creator(commonData);
// Read all calibration data settings from config // Read user settings from config file
calibrationData.readConfig(config, logger);
// Check user settings for true wind calculation
bool calcTrueWnds = api->getConfig()->getBool(api->getConfig()->calcTrueWnds, false); bool calcTrueWnds = api->getConfig()->getBool(api->getConfig()->calcTrueWnds, false);
bool useSimuData = api->getConfig()->getBool(api->getConfig()->useSimuData, false); bool useSimuData = api->getConfig()->getBool(api->getConfig()->useSimuData, false);
// Read user calibration data settings from config file
// Initialize history buffer for certain boat data calibrationDataList.readConfig(config);
hstryBufList.init(&boatValues, logger);
// Display screenshot handler for HTTP request // Display screenshot handler for HTTP request
// http://192.168.15.1/api/user/OBP60Task/screenshot // http://192.168.15.1/api/user/OBP60Task/screenshot
@@ -654,7 +661,9 @@ void OBP60Task(GwApi *api){
// if(String(backlight) == "Control by Key"){ // if(String(backlight) == "Control by Key"){
if(keyboardMessage == 6){ if(keyboardMessage == 6){
LOG_DEBUG(GwLog::LOG,"Toggle Backlight LED"); LOG_DEBUG(GwLog::LOG,"Toggle Backlight LED");
toggleBacklightLED(commonData.backlight.brightness, commonData.backlight.color); // TODO config: toogle vs steps
// toggleBacklightLED(commonData.backlight.brightness, commonData.backlight.color);
stepsBacklightLED(commonData.backlight.brightness, commonData.backlight.color);
} }
} }
#ifdef BOARD_OBP40S3 #ifdef BOARD_OBP40S3
@@ -716,8 +725,8 @@ void OBP60Task(GwApi *api){
} }
} }
// Full display update afer a new selected page and 4s wait time // Full display update afer a new selected page and 8s wait time
if(millis() > starttime4 + 4000 && delayedDisplayUpdate == true){ if (millis() > starttime4 + 8000 && delayedDisplayUpdate == true) {
starttime1 = millis(); starttime1 = millis();
starttime2 = millis(); starttime2 = millis();
epd->setFullWindow(); // Set full update epd->setFullWindow(); // Set full update
@@ -807,10 +816,10 @@ void OBP60Task(GwApi *api){
api->getStatus(commonData.status); api->getStatus(commonData.status);
if (calcTrueWnds) { if (calcTrueWnds) {
trueWind.addTrueWind(api, &boatValues, logger); trueWind.addWinds(); // calculate true wind data from apparent wind values
} }
// Handle history buffers for TWD, TWS for wind plot page and other usage calibrationDataList.handleCalibration(&boatValues); // Process calibration for all boat data in <calibrationDataList>
hstryBufList.handleHstryBuf(useSimuData); hstryBufferList.handleHstryBufs(useSimuData, commonData); // Handle history buffers for certain boat data for windplot page and other usage
// Clear display // Clear display
// epd->fillRect(0, 0, epd->width(), epd->height(), commonData.bgcolor); // epd->fillRect(0, 0, epd->width(), epd->height(), commonData.bgcolor);
@@ -846,9 +855,11 @@ void OBP60Task(GwApi *api){
epd->nextPage(); // Partial update (fast) epd->nextPage(); // Partial update (fast)
} }
else { else {
if (lastPage != pageNumber){ if (lastPage != pageNumber) {
if (lastPage != -1) { // skip cleanup if we are during startup, and no page has been displayed yet
pages[lastPage].page->leavePage(pages[lastPage].parameters); // call page cleanup code pages[lastPage].page->leavePage(pages[lastPage].parameters); // call page cleanup code
if (hasFRAM) fram.write(FRAM_PAGE_NO, pageNumber); // remember new page for device restart if (hasFRAM) fram.write(FRAM_PAGE_NO, pageNumber); // remember new page for device restart
}
currentPage->setupKeys(); currentPage->setupKeys();
currentPage->displayNew(pages[pageNumber].parameters); currentPage->displayNew(pages[pageNumber].parameters);
lastPage = pageNumber; lastPage = pageNumber;

View File

@@ -15,6 +15,8 @@ board_build.variants_dir = variants
board = obp60_s3_n16r8 #ESP32-S3 N16R8, 16MB flash, 8MB PSRAM, production series board = obp60_s3_n16r8 #ESP32-S3 N16R8, 16MB flash, 8MB PSRAM, production series
#board_build.partitions = default_8MB.csv #ESP32-S3 N8, 8MB flash #board_build.partitions = default_8MB.csv #ESP32-S3 N8, 8MB flash
board_build.partitions = default_16MB.csv #ESP32-S3 N16, 16MB flash board_build.partitions = default_16MB.csv #ESP32-S3 N16, 16MB flash
custom_config = lib/obp60task/config_obp60.json
custom_script = lib/obp60task/extra_task.py
board_name = OBP60 board_name = OBP60
framework = arduino framework = arduino
lib_deps = lib_deps =
@@ -22,6 +24,8 @@ lib_deps =
Wire Wire
SPI SPI
ESP32time ESP32time
HTTPClient
WiFiClientSecure
robtillaart/PCF8574@0.3.9 robtillaart/PCF8574@0.3.9
adafruit/Adafruit Unified Sensor @ 1.1.13 adafruit/Adafruit Unified Sensor @ 1.1.13
blemasle/MCP23017@2.0.0 blemasle/MCP23017@2.0.0
@@ -51,8 +55,6 @@ build_flags=
# -D DISPLAY_GYE042A87 #alternativ E-Ink display from Genyo Optical, R10 2.2 ohm - medium # -D DISPLAY_GYE042A87 #alternativ E-Ink display from Genyo Optical, R10 2.2 ohm - medium
# -D DISPLAY_SE0420NQ04 #alternativ E-Ink display from SID Technology, R10 2.2 ohm - bad (burn in effects) # -D DISPLAY_SE0420NQ04 #alternativ E-Ink display from SID Technology, R10 2.2 ohm - bad (burn in effects)
# -D DISPLAY_ZJY400300-042CAAMFGN #alternativ E-Ink display from ZZE Technology, R10 2.2 ohm - very good # -D DISPLAY_ZJY400300-042CAAMFGN #alternativ E-Ink display from ZZE Technology, R10 2.2 ohm - very good
# -D ENABLE_TRUEWIND # calculate true wind data (default off)
# -D ENABLE_CALIBRATION # boat data calibration (default off)
${env.build_flags} ${env.build_flags}
#CONFIG_ESP_TASK_WDT_TIMEOUT_S = 10 #Task Watchdog timeout period (seconds) [1...60] 5 default #CONFIG_ESP_TASK_WDT_TIMEOUT_S = 10 #Task Watchdog timeout period (seconds) [1...60] 5 default
upload_port = /dev/ttyACM0 #OBP60 download via USB-C direct upload_port = /dev/ttyACM0 #OBP60 download via USB-C direct
@@ -64,14 +66,17 @@ monitor_speed = 115200
board_build.variants_dir = variants board_build.variants_dir = variants
board = obp40_s3_n8r8 #ESP32-S3 N8R8, 8MB flash, 8MB PSRAM, OBP60 clone (CrowPanel 4.2) board = obp40_s3_n8r8 #ESP32-S3 N8R8, 8MB flash, 8MB PSRAM, OBP60 clone (CrowPanel 4.2)
board_build.partitions = default_8MB.csv #ESP32-S3 N8, 8MB flash board_build.partitions = default_8MB.csv #ESP32-S3 N8, 8MB flash
custom_config = lib/obp60task/config_obp40.json
custom_script = lib/obp60task/extra_task.py
board_name = OBP40 board_name = OBP40
custom_config = config_obp40.json
framework = arduino framework = arduino
lib_deps = lib_deps =
${basedeps.lib_deps} ${basedeps.lib_deps}
Wire Wire
SPI SPI
ESP32time ESP32time
HTTPClient
WiFiClientSecure
robtillaart/PCF8574@0.3.9 robtillaart/PCF8574@0.3.9
adafruit/Adafruit Unified Sensor @ 1.1.13 adafruit/Adafruit Unified Sensor @ 1.1.13
blemasle/MCP23017@2.0.0 blemasle/MCP23017@2.0.0
@@ -96,8 +101,6 @@ build_flags=
#-D DISPLAY_ZJY400300-042CAAMFGN #alternativ E-Ink display from ZZE Technology, R10 2.2 ohm - very good #-D DISPLAY_ZJY400300-042CAAMFGN #alternativ E-Ink display from ZZE Technology, R10 2.2 ohm - very good
# -D LIPO_ACCU_1200 #Hardware extension, LiPo accu 3,7V 1200mAh # -D LIPO_ACCU_1200 #Hardware extension, LiPo accu 3,7V 1200mAh
# -D VOLTAGE_SENSOR #Hardware extension, LiPo voltage sensor with two resistors # -D VOLTAGE_SENSOR #Hardware extension, LiPo voltage sensor with two resistors
# -D ENABLE_TRUEWIND # calculate true wind data (default off)
# -D ENABLE_CALIBRATION # boat data calibration (default off)
${env.build_flags} ${env.build_flags}
upload_port = /dev/ttyUSB0 #OBP40 download via external USB/Serail converter upload_port = /dev/ttyUSB0 #OBP40 download via external USB/Serail converter
upload_protocol = esptool #firmware upload via USB OTG seriell, by first upload need to set the ESP32-S3 in the upload mode with shortcut GND to Pin27 upload_protocol = esptool #firmware upload via USB OTG seriell, by first upload need to set the ESP32-S3 in the upload mode with shortcut GND to Pin27

840
lib/obp60task/puff.c Normal file
View File

@@ -0,0 +1,840 @@
/*
* puff.c
* Copyright (C) 2002-2013 Mark Adler
* For conditions of distribution and use, see copyright notice in puff.h
* version 2.3, 21 Jan 2013
*
* puff.c is a simple inflate written to be an unambiguous way to specify the
* deflate format. It is not written for speed but rather simplicity. As a
* side benefit, this code might actually be useful when small code is more
* important than speed, such as bootstrap applications. For typical deflate
* data, zlib's inflate() is about four times as fast as puff(). zlib's
* inflate compiles to around 20K on my machine, whereas puff.c compiles to
* around 4K on my machine (a PowerPC using GNU cc). If the faster decode()
* function here is used, then puff() is only twice as slow as zlib's
* inflate().
*
* All dynamically allocated memory comes from the stack. The stack required
* is less than 2K bytes. This code is compatible with 16-bit int's and
* assumes that long's are at least 32 bits. puff.c uses the short data type,
* assumed to be 16 bits, for arrays in order to conserve memory. The code
* works whether integers are stored big endian or little endian.
*
* In the comments below are "Format notes" that describe the inflate process
* and document some of the less obvious aspects of the format. This source
* code is meant to supplement RFC 1951, which formally describes the deflate
* format:
*
* http://www.zlib.org/rfc-deflate.html
*/
/*
* Change history:
*
* 1.0 10 Feb 2002 - First version
* 1.1 17 Feb 2002 - Clarifications of some comments and notes
* - Update puff() dest and source pointers on negative
* errors to facilitate debugging deflators
* - Remove longest from struct huffman -- not needed
* - Simplify offs[] index in construct()
* - Add input size and checking, using longjmp() to
* maintain easy readability
* - Use short data type for large arrays
* - Use pointers instead of long to specify source and
* destination sizes to avoid arbitrary 4 GB limits
* 1.2 17 Mar 2002 - Add faster version of decode(), doubles speed (!),
* but leave simple version for readability
* - Make sure invalid distances detected if pointers
* are 16 bits
* - Fix fixed codes table error
* - Provide a scanning mode for determining size of
* uncompressed data
* 1.3 20 Mar 2002 - Go back to lengths for puff() parameters [Gailly]
* - Add a puff.h file for the interface
* - Add braces in puff() for else do [Gailly]
* - Use indexes instead of pointers for readability
* 1.4 31 Mar 2002 - Simplify construct() code set check
* - Fix some comments
* - Add FIXLCODES #define
* 1.5 6 Apr 2002 - Minor comment fixes
* 1.6 7 Aug 2002 - Minor format changes
* 1.7 3 Mar 2003 - Added test code for distribution
* - Added zlib-like license
* 1.8 9 Jan 2004 - Added some comments on no distance codes case
* 1.9 21 Feb 2008 - Fix bug on 16-bit integer architectures [Pohland]
* - Catch missing end-of-block symbol error
* 2.0 25 Jul 2008 - Add #define to permit distance too far back
* - Add option in TEST code for puff to write the data
* - Add option in TEST code to skip input bytes
* - Allow TEST code to read from piped stdin
* 2.1 4 Apr 2010 - Avoid variable initialization for happier compilers
* - Avoid unsigned comparisons for even happier compilers
* 2.2 25 Apr 2010 - Fix bug in variable initializations [Oberhumer]
* - Add const where appropriate [Oberhumer]
* - Split if's and ?'s for coverage testing
* - Break out test code to separate file
* - Move NIL to puff.h
* - Allow incomplete code only if single code length is 1
* - Add full code coverage test to Makefile
* 2.3 21 Jan 2013 - Check for invalid code length codes in dynamic blocks
*/
#include <setjmp.h> /* for setjmp(), longjmp(), and jmp_buf */
#include "puff.h" /* prototype for puff() */
#define local static /* for local function definitions */
/*
* Maximums for allocations and loops. It is not useful to change these --
* they are fixed by the deflate format.
*/
#define MAXBITS 15 /* maximum bits in a code */
#define MAXLCODES 286 /* maximum number of literal/length codes */
#define MAXDCODES 30 /* maximum number of distance codes */
#define MAXCODES (MAXLCODES+MAXDCODES) /* maximum codes lengths to read */
#define FIXLCODES 288 /* number of fixed literal/length codes */
/* input and output state */
struct state {
/* output state */
unsigned char *out; /* output buffer */
unsigned long outlen; /* available space at out */
unsigned long outcnt; /* bytes written to out so far */
/* input state */
const unsigned char *in; /* input buffer */
unsigned long inlen; /* available input at in */
unsigned long incnt; /* bytes read so far */
int bitbuf; /* bit buffer */
int bitcnt; /* number of bits in bit buffer */
/* input limit error return state for bits() and decode() */
jmp_buf env;
};
/*
* Return need bits from the input stream. This always leaves less than
* eight bits in the buffer. bits() works properly for need == 0.
*
* Format notes:
*
* - Bits are stored in bytes from the least significant bit to the most
* significant bit. Therefore bits are dropped from the bottom of the bit
* buffer, using shift right, and new bytes are appended to the top of the
* bit buffer, using shift left.
*/
local int bits(struct state *s, int need)
{
long val; /* bit accumulator (can use up to 20 bits) */
/* load at least need bits into val */
val = s->bitbuf;
while (s->bitcnt < need) {
if (s->incnt == s->inlen)
longjmp(s->env, 1); /* out of input */
val |= (long)(s->in[s->incnt++]) << s->bitcnt; /* load eight bits */
s->bitcnt += 8;
}
/* drop need bits and update buffer, always zero to seven bits left */
s->bitbuf = (int)(val >> need);
s->bitcnt -= need;
/* return need bits, zeroing the bits above that */
return (int)(val & ((1L << need) - 1));
}
/*
* Process a stored block.
*
* Format notes:
*
* - After the two-bit stored block type (00), the stored block length and
* stored bytes are byte-aligned for fast copying. Therefore any leftover
* bits in the byte that has the last bit of the type, as many as seven, are
* discarded. The value of the discarded bits are not defined and should not
* be checked against any expectation.
*
* - The second inverted copy of the stored block length does not have to be
* checked, but it's probably a good idea to do so anyway.
*
* - A stored block can have zero length. This is sometimes used to byte-align
* subsets of the compressed data for random access or partial recovery.
*/
local int stored(struct state *s)
{
unsigned len; /* length of stored block */
/* discard leftover bits from current byte (assumes s->bitcnt < 8) */
s->bitbuf = 0;
s->bitcnt = 0;
/* get length and check against its one's complement */
if (s->incnt + 4 > s->inlen)
return 2; /* not enough input */
len = s->in[s->incnt++];
len |= s->in[s->incnt++] << 8;
if (s->in[s->incnt++] != (~len & 0xff) ||
s->in[s->incnt++] != ((~len >> 8) & 0xff))
return -2; /* didn't match complement! */
/* copy len bytes from in to out */
if (s->incnt + len > s->inlen)
return 2; /* not enough input */
if (s->out != NIL) {
if (s->outcnt + len > s->outlen)
return 1; /* not enough output space */
while (len--)
s->out[s->outcnt++] = s->in[s->incnt++];
}
else { /* just scanning */
s->outcnt += len;
s->incnt += len;
}
/* done with a valid stored block */
return 0;
}
/*
* Huffman code decoding tables. count[1..MAXBITS] is the number of symbols of
* each length, which for a canonical code are stepped through in order.
* symbol[] are the symbol values in canonical order, where the number of
* entries is the sum of the counts in count[]. The decoding process can be
* seen in the function decode() below.
*/
struct huffman {
short *count; /* number of symbols of each length */
short *symbol; /* canonically ordered symbols */
};
/*
* Decode a code from the stream s using huffman table h. Return the symbol or
* a negative value if there is an error. If all of the lengths are zero, i.e.
* an empty code, or if the code is incomplete and an invalid code is received,
* then -10 is returned after reading MAXBITS bits.
*
* Format notes:
*
* - The codes as stored in the compressed data are bit-reversed relative to
* a simple integer ordering of codes of the same lengths. Hence below the
* bits are pulled from the compressed data one at a time and used to
* build the code value reversed from what is in the stream in order to
* permit simple integer comparisons for decoding. A table-based decoding
* scheme (as used in zlib) does not need to do this reversal.
*
* - The first code for the shortest length is all zeros. Subsequent codes of
* the same length are simply integer increments of the previous code. When
* moving up a length, a zero bit is appended to the code. For a complete
* code, the last code of the longest length will be all ones.
*
* - Incomplete codes are handled by this decoder, since they are permitted
* in the deflate format. See the format notes for fixed() and dynamic().
*/
#ifdef SLOW
local int decode(struct state *s, const struct huffman *h)
{
int len; /* current number of bits in code */
int code; /* len bits being decoded */
int first; /* first code of length len */
int count; /* number of codes of length len */
int index; /* index of first code of length len in symbol table */
code = first = index = 0;
for (len = 1; len <= MAXBITS; len++) {
code |= bits(s, 1); /* get next bit */
count = h->count[len];
if (code - count < first) /* if length len, return symbol */
return h->symbol[index + (code - first)];
index += count; /* else update for next length */
first += count;
first <<= 1;
code <<= 1;
}
return -10; /* ran out of codes */
}
/*
* A faster version of decode() for real applications of this code. It's not
* as readable, but it makes puff() twice as fast. And it only makes the code
* a few percent larger.
*/
#else /* !SLOW */
local int decode(struct state *s, const struct huffman *h)
{
int len; /* current number of bits in code */
int code; /* len bits being decoded */
int first; /* first code of length len */
int count; /* number of codes of length len */
int index; /* index of first code of length len in symbol table */
int bitbuf; /* bits from stream */
int left; /* bits left in next or left to process */
short *next; /* next number of codes */
bitbuf = s->bitbuf;
left = s->bitcnt;
code = first = index = 0;
len = 1;
next = h->count + 1;
while (1) {
while (left--) {
code |= bitbuf & 1;
bitbuf >>= 1;
count = *next++;
if (code - count < first) { /* if length len, return symbol */
s->bitbuf = bitbuf;
s->bitcnt = (s->bitcnt - len) & 7;
return h->symbol[index + (code - first)];
}
index += count; /* else update for next length */
first += count;
first <<= 1;
code <<= 1;
len++;
}
left = (MAXBITS+1) - len;
if (left == 0)
break;
if (s->incnt == s->inlen)
longjmp(s->env, 1); /* out of input */
bitbuf = s->in[s->incnt++];
if (left > 8)
left = 8;
}
return -10; /* ran out of codes */
}
#endif /* SLOW */
/*
* Given the list of code lengths length[0..n-1] representing a canonical
* Huffman code for n symbols, construct the tables required to decode those
* codes. Those tables are the number of codes of each length, and the symbols
* sorted by length, retaining their original order within each length. The
* return value is zero for a complete code set, negative for an over-
* subscribed code set, and positive for an incomplete code set. The tables
* can be used if the return value is zero or positive, but they cannot be used
* if the return value is negative. If the return value is zero, it is not
* possible for decode() using that table to return an error--any stream of
* enough bits will resolve to a symbol. If the return value is positive, then
* it is possible for decode() using that table to return an error for received
* codes past the end of the incomplete lengths.
*
* Not used by decode(), but used for error checking, h->count[0] is the number
* of the n symbols not in the code. So n - h->count[0] is the number of
* codes. This is useful for checking for incomplete codes that have more than
* one symbol, which is an error in a dynamic block.
*
* Assumption: for all i in 0..n-1, 0 <= length[i] <= MAXBITS
* This is assured by the construction of the length arrays in dynamic() and
* fixed() and is not verified by construct().
*
* Format notes:
*
* - Permitted and expected examples of incomplete codes are one of the fixed
* codes and any code with a single symbol which in deflate is coded as one
* bit instead of zero bits. See the format notes for fixed() and dynamic().
*
* - Within a given code length, the symbols are kept in ascending order for
* the code bits definition.
*/
local int construct(struct huffman *h, const short *length, int n)
{
int symbol; /* current symbol when stepping through length[] */
int len; /* current length when stepping through h->count[] */
int left; /* number of possible codes left of current length */
short offs[MAXBITS+1]; /* offsets in symbol table for each length */
/* count number of codes of each length */
for (len = 0; len <= MAXBITS; len++)
h->count[len] = 0;
for (symbol = 0; symbol < n; symbol++)
(h->count[length[symbol]])++; /* assumes lengths are within bounds */
if (h->count[0] == n) /* no codes! */
return 0; /* complete, but decode() will fail */
/* check for an over-subscribed or incomplete set of lengths */
left = 1; /* one possible code of zero length */
for (len = 1; len <= MAXBITS; len++) {
left <<= 1; /* one more bit, double codes left */
left -= h->count[len]; /* deduct count from possible codes */
if (left < 0)
return left; /* over-subscribed--return negative */
} /* left > 0 means incomplete */
/* generate offsets into symbol table for each length for sorting */
offs[1] = 0;
for (len = 1; len < MAXBITS; len++)
offs[len + 1] = offs[len] + h->count[len];
/*
* put symbols in table sorted by length, by symbol order within each
* length
*/
for (symbol = 0; symbol < n; symbol++)
if (length[symbol] != 0)
h->symbol[offs[length[symbol]]++] = symbol;
/* return zero for complete set, positive for incomplete set */
return left;
}
/*
* Decode literal/length and distance codes until an end-of-block code.
*
* Format notes:
*
* - Compressed data that is after the block type if fixed or after the code
* description if dynamic is a combination of literals and length/distance
* pairs terminated by and end-of-block code. Literals are simply Huffman
* coded bytes. A length/distance pair is a coded length followed by a
* coded distance to represent a string that occurs earlier in the
* uncompressed data that occurs again at the current location.
*
* - Literals, lengths, and the end-of-block code are combined into a single
* code of up to 286 symbols. They are 256 literals (0..255), 29 length
* symbols (257..285), and the end-of-block symbol (256).
*
* - There are 256 possible lengths (3..258), and so 29 symbols are not enough
* to represent all of those. Lengths 3..10 and 258 are in fact represented
* by just a length symbol. Lengths 11..257 are represented as a symbol and
* some number of extra bits that are added as an integer to the base length
* of the length symbol. The number of extra bits is determined by the base
* length symbol. These are in the static arrays below, lens[] for the base
* lengths and lext[] for the corresponding number of extra bits.
*
* - The reason that 258 gets its own symbol is that the longest length is used
* often in highly redundant files. Note that 258 can also be coded as the
* base value 227 plus the maximum extra value of 31. While a good deflate
* should never do this, it is not an error, and should be decoded properly.
*
* - If a length is decoded, including its extra bits if any, then it is
* followed a distance code. There are up to 30 distance symbols. Again
* there are many more possible distances (1..32768), so extra bits are added
* to a base value represented by the symbol. The distances 1..4 get their
* own symbol, but the rest require extra bits. The base distances and
* corresponding number of extra bits are below in the static arrays dist[]
* and dext[].
*
* - Literal bytes are simply written to the output. A length/distance pair is
* an instruction to copy previously uncompressed bytes to the output. The
* copy is from distance bytes back in the output stream, copying for length
* bytes.
*
* - Distances pointing before the beginning of the output data are not
* permitted.
*
* - Overlapped copies, where the length is greater than the distance, are
* allowed and common. For example, a distance of one and a length of 258
* simply copies the last byte 258 times. A distance of four and a length of
* twelve copies the last four bytes three times. A simple forward copy
* ignoring whether the length is greater than the distance or not implements
* this correctly. You should not use memcpy() since its behavior is not
* defined for overlapped arrays. You should not use memmove() or bcopy()
* since though their behavior -is- defined for overlapping arrays, it is
* defined to do the wrong thing in this case.
*/
local int codes(struct state *s,
const struct huffman *lencode,
const struct huffman *distcode)
{
int symbol; /* decoded symbol */
int len; /* length for copy */
unsigned dist; /* distance for copy */
static const short lens[29] = { /* Size base for length codes 257..285 */
3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31,
35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258};
static const short lext[29] = { /* Extra bits for length codes 257..285 */
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2,
3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0};
static const short dists[30] = { /* Offset base for distance codes 0..29 */
1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193,
257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145,
8193, 12289, 16385, 24577};
static const short dext[30] = { /* Extra bits for distance codes 0..29 */
0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6,
7, 7, 8, 8, 9, 9, 10, 10, 11, 11,
12, 12, 13, 13};
/* decode literals and length/distance pairs */
do {
symbol = decode(s, lencode);
if (symbol < 0)
return symbol; /* invalid symbol */
if (symbol < 256) { /* literal: symbol is the byte */
/* write out the literal */
if (s->out != NIL) {
if (s->outcnt == s->outlen)
return 1;
s->out[s->outcnt] = symbol;
}
s->outcnt++;
}
else if (symbol > 256) { /* length */
/* get and compute length */
symbol -= 257;
if (symbol >= 29)
return -10; /* invalid fixed code */
len = lens[symbol] + bits(s, lext[symbol]);
/* get and check distance */
symbol = decode(s, distcode);
if (symbol < 0)
return symbol; /* invalid symbol */
dist = dists[symbol] + bits(s, dext[symbol]);
#ifndef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR
if (dist > s->outcnt)
return -11; /* distance too far back */
#endif
/* copy length bytes from distance bytes back */
if (s->out != NIL) {
if (s->outcnt + len > s->outlen)
return 1;
while (len--) {
s->out[s->outcnt] =
#ifdef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR
dist > s->outcnt ?
0 :
#endif
s->out[s->outcnt - dist];
s->outcnt++;
}
}
else
s->outcnt += len;
}
} while (symbol != 256); /* end of block symbol */
/* done with a valid fixed or dynamic block */
return 0;
}
/*
* Process a fixed codes block.
*
* Format notes:
*
* - This block type can be useful for compressing small amounts of data for
* which the size of the code descriptions in a dynamic block exceeds the
* benefit of custom codes for that block. For fixed codes, no bits are
* spent on code descriptions. Instead the code lengths for literal/length
* codes and distance codes are fixed. The specific lengths for each symbol
* can be seen in the "for" loops below.
*
* - The literal/length code is complete, but has two symbols that are invalid
* and should result in an error if received. This cannot be implemented
* simply as an incomplete code since those two symbols are in the "middle"
* of the code. They are eight bits long and the longest literal/length\
* code is nine bits. Therefore the code must be constructed with those
* symbols, and the invalid symbols must be detected after decoding.
*
* - The fixed distance codes also have two invalid symbols that should result
* in an error if received. Since all of the distance codes are the same
* length, this can be implemented as an incomplete code. Then the invalid
* codes are detected while decoding.
*/
local int fixed(struct state *s)
{
static int virgin = 1;
static short lencnt[MAXBITS+1], lensym[FIXLCODES];
static short distcnt[MAXBITS+1], distsym[MAXDCODES];
static struct huffman lencode, distcode;
/* build fixed huffman tables if first call (may not be thread safe) */
if (virgin) {
int symbol;
short lengths[FIXLCODES];
/* construct lencode and distcode */
lencode.count = lencnt;
lencode.symbol = lensym;
distcode.count = distcnt;
distcode.symbol = distsym;
/* literal/length table */
for (symbol = 0; symbol < 144; symbol++)
lengths[symbol] = 8;
for (; symbol < 256; symbol++)
lengths[symbol] = 9;
for (; symbol < 280; symbol++)
lengths[symbol] = 7;
for (; symbol < FIXLCODES; symbol++)
lengths[symbol] = 8;
construct(&lencode, lengths, FIXLCODES);
/* distance table */
for (symbol = 0; symbol < MAXDCODES; symbol++)
lengths[symbol] = 5;
construct(&distcode, lengths, MAXDCODES);
/* do this just once */
virgin = 0;
}
/* decode data until end-of-block code */
return codes(s, &lencode, &distcode);
}
/*
* Process a dynamic codes block.
*
* Format notes:
*
* - A dynamic block starts with a description of the literal/length and
* distance codes for that block. New dynamic blocks allow the compressor to
* rapidly adapt to changing data with new codes optimized for that data.
*
* - The codes used by the deflate format are "canonical", which means that
* the actual bits of the codes are generated in an unambiguous way simply
* from the number of bits in each code. Therefore the code descriptions
* are simply a list of code lengths for each symbol.
*
* - The code lengths are stored in order for the symbols, so lengths are
* provided for each of the literal/length symbols, and for each of the
* distance symbols.
*
* - If a symbol is not used in the block, this is represented by a zero as the
* code length. This does not mean a zero-length code, but rather that no
* code should be created for this symbol. There is no way in the deflate
* format to represent a zero-length code.
*
* - The maximum number of bits in a code is 15, so the possible lengths for
* any code are 1..15.
*
* - The fact that a length of zero is not permitted for a code has an
* interesting consequence. Normally if only one symbol is used for a given
* code, then in fact that code could be represented with zero bits. However
* in deflate, that code has to be at least one bit. So for example, if
* only a single distance base symbol appears in a block, then it will be
* represented by a single code of length one, in particular one 0 bit. This
* is an incomplete code, since if a 1 bit is received, it has no meaning,
* and should result in an error. So incomplete distance codes of one symbol
* should be permitted, and the receipt of invalid codes should be handled.
*
* - It is also possible to have a single literal/length code, but that code
* must be the end-of-block code, since every dynamic block has one. This
* is not the most efficient way to create an empty block (an empty fixed
* block is fewer bits), but it is allowed by the format. So incomplete
* literal/length codes of one symbol should also be permitted.
*
* - If there are only literal codes and no lengths, then there are no distance
* codes. This is represented by one distance code with zero bits.
*
* - The list of up to 286 length/literal lengths and up to 30 distance lengths
* are themselves compressed using Huffman codes and run-length encoding. In
* the list of code lengths, a 0 symbol means no code, a 1..15 symbol means
* that length, and the symbols 16, 17, and 18 are run-length instructions.
* Each of 16, 17, and 18 are followed by extra bits to define the length of
* the run. 16 copies the last length 3 to 6 times. 17 represents 3 to 10
* zero lengths, and 18 represents 11 to 138 zero lengths. Unused symbols
* are common, hence the special coding for zero lengths.
*
* - The symbols for 0..18 are Huffman coded, and so that code must be
* described first. This is simply a sequence of up to 19 three-bit values
* representing no code (0) or the code length for that symbol (1..7).
*
* - A dynamic block starts with three fixed-size counts from which is computed
* the number of literal/length code lengths, the number of distance code
* lengths, and the number of code length code lengths (ok, you come up with
* a better name!) in the code descriptions. For the literal/length and
* distance codes, lengths after those provided are considered zero, i.e. no
* code. The code length code lengths are received in a permuted order (see
* the order[] array below) to make a short code length code length list more
* likely. As it turns out, very short and very long codes are less likely
* to be seen in a dynamic code description, hence what may appear initially
* to be a peculiar ordering.
*
* - Given the number of literal/length code lengths (nlen) and distance code
* lengths (ndist), then they are treated as one long list of nlen + ndist
* code lengths. Therefore run-length coding can and often does cross the
* boundary between the two sets of lengths.
*
* - So to summarize, the code description at the start of a dynamic block is
* three counts for the number of code lengths for the literal/length codes,
* the distance codes, and the code length codes. This is followed by the
* code length code lengths, three bits each. This is used to construct the
* code length code which is used to read the remainder of the lengths. Then
* the literal/length code lengths and distance lengths are read as a single
* set of lengths using the code length codes. Codes are constructed from
* the resulting two sets of lengths, and then finally you can start
* decoding actual compressed data in the block.
*
* - For reference, a "typical" size for the code description in a dynamic
* block is around 80 bytes.
*/
local int dynamic(struct state *s)
{
int nlen, ndist, ncode; /* number of lengths in descriptor */
int index; /* index of lengths[] */
int err; /* construct() return value */
short lengths[MAXCODES]; /* descriptor code lengths */
short lencnt[MAXBITS+1], lensym[MAXLCODES]; /* lencode memory */
short distcnt[MAXBITS+1], distsym[MAXDCODES]; /* distcode memory */
struct huffman lencode, distcode; /* length and distance codes */
static const short order[19] = /* permutation of code length codes */
{16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15};
/* construct lencode and distcode */
lencode.count = lencnt;
lencode.symbol = lensym;
distcode.count = distcnt;
distcode.symbol = distsym;
/* get number of lengths in each table, check lengths */
nlen = bits(s, 5) + 257;
ndist = bits(s, 5) + 1;
ncode = bits(s, 4) + 4;
if (nlen > MAXLCODES || ndist > MAXDCODES)
return -3; /* bad counts */
/* read code length code lengths (really), missing lengths are zero */
for (index = 0; index < ncode; index++)
lengths[order[index]] = bits(s, 3);
for (; index < 19; index++)
lengths[order[index]] = 0;
/* build huffman table for code lengths codes (use lencode temporarily) */
err = construct(&lencode, lengths, 19);
if (err != 0) /* require complete code set here */
return -4;
/* read length/literal and distance code length tables */
index = 0;
while (index < nlen + ndist) {
int symbol; /* decoded value */
int len; /* last length to repeat */
symbol = decode(s, &lencode);
if (symbol < 0)
return symbol; /* invalid symbol */
if (symbol < 16) /* length in 0..15 */
lengths[index++] = symbol;
else { /* repeat instruction */
len = 0; /* assume repeating zeros */
if (symbol == 16) { /* repeat last length 3..6 times */
if (index == 0)
return -5; /* no last length! */
len = lengths[index - 1]; /* last length */
symbol = 3 + bits(s, 2);
}
else if (symbol == 17) /* repeat zero 3..10 times */
symbol = 3 + bits(s, 3);
else /* == 18, repeat zero 11..138 times */
symbol = 11 + bits(s, 7);
if (index + symbol > nlen + ndist)
return -6; /* too many lengths! */
while (symbol--) /* repeat last or zero symbol times */
lengths[index++] = len;
}
}
/* check for end-of-block code -- there better be one! */
if (lengths[256] == 0)
return -9;
/* build huffman table for literal/length codes */
err = construct(&lencode, lengths, nlen);
if (err && (err < 0 || nlen != lencode.count[0] + lencode.count[1]))
return -7; /* incomplete code ok only for single length 1 code */
/* build huffman table for distance codes */
err = construct(&distcode, lengths + nlen, ndist);
if (err && (err < 0 || ndist != distcode.count[0] + distcode.count[1]))
return -8; /* incomplete code ok only for single length 1 code */
/* decode data until end-of-block code */
return codes(s, &lencode, &distcode);
}
/*
* Inflate source to dest. On return, destlen and sourcelen are updated to the
* size of the uncompressed data and the size of the deflate data respectively.
* On success, the return value of puff() is zero. If there is an error in the
* source data, i.e. it is not in the deflate format, then a negative value is
* returned. If there is not enough input available or there is not enough
* output space, then a positive error is returned. In that case, destlen and
* sourcelen are not updated to facilitate retrying from the beginning with the
* provision of more input data or more output space. In the case of invalid
* inflate data (a negative error), the dest and source pointers are updated to
* facilitate the debugging of deflators.
*
* puff() also has a mode to determine the size of the uncompressed output with
* no output written. For this dest must be (unsigned char *)0. In this case,
* the input value of *destlen is ignored, and on return *destlen is set to the
* size of the uncompressed output.
*
* The return codes are:
*
* 2: available inflate data did not terminate
* 1: output space exhausted before completing inflate
* 0: successful inflate
* -1: invalid block type (type == 3)
* -2: stored block length did not match one's complement
* -3: dynamic block code description: too many length or distance codes
* -4: dynamic block code description: code lengths codes incomplete
* -5: dynamic block code description: repeat lengths with no first length
* -6: dynamic block code description: repeat more than specified lengths
* -7: dynamic block code description: invalid literal/length code lengths
* -8: dynamic block code description: invalid distance code lengths
* -9: dynamic block code description: missing end-of-block code
* -10: invalid literal/length or distance code in fixed or dynamic block
* -11: distance is too far back in fixed or dynamic block
*
* Format notes:
*
* - Three bits are read for each block to determine the kind of block and
* whether or not it is the last block. Then the block is decoded and the
* process repeated if it was not the last block.
*
* - The leftover bits in the last byte of the deflate data after the last
* block (if it was a fixed or dynamic block) are undefined and have no
* expected values to check.
*/
int puff(unsigned char *dest, /* pointer to destination pointer */
unsigned long *destlen, /* amount of output space */
const unsigned char *source, /* pointer to source data pointer */
unsigned long *sourcelen) /* amount of input available */
{
struct state s; /* input/output state */
int last, type; /* block information */
int err; /* return value */
/* initialize output state */
s.out = dest;
s.outlen = *destlen; /* ignored if dest is NIL */
s.outcnt = 0;
/* initialize input state */
s.in = source;
s.inlen = *sourcelen;
s.incnt = 0;
s.bitbuf = 0;
s.bitcnt = 0;
/* return if bits() or decode() tries to read past available input */
if (setjmp(s.env) != 0) /* if came back here via longjmp() */
err = 2; /* then skip do-loop, return error */
else {
/* process blocks until last block or error */
do {
last = bits(&s, 1); /* one if last block */
type = bits(&s, 2); /* block type 0..3 */
err = type == 0 ?
stored(&s) :
(type == 1 ?
fixed(&s) :
(type == 2 ?
dynamic(&s) :
-1)); /* type == 3, invalid */
if (err != 0)
break; /* return with error */
} while (!last);
}
/* update the lengths and return */
if (err <= 0) {
*destlen = s.outcnt;
*sourcelen = s.incnt;
}
return err;
}

35
lib/obp60task/puff.h Normal file
View File

@@ -0,0 +1,35 @@
/* puff.h
Copyright (C) 2002-2013 Mark Adler, all rights reserved
version 2.3, 21 Jan 2013
This software is provided 'as-is', without any express or implied
warranty. In no event will the author be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
Mark Adler madler@alumni.caltech.edu
*/
/*
* See puff.c for purpose and usage.
*/
#ifndef NIL
# define NIL ((unsigned char *)0) /* for no output option */
#endif
int puff(unsigned char *dest, /* pointer to destination pointer */
unsigned long *destlen, /* amount of output space */
const unsigned char *source, /* pointer to source data pointer */
unsigned long *sourcelen); /* amount of input available */

View File

@@ -8,6 +8,6 @@
# Install tools # Install tools
echo "Installing tools" echo "Installing tools"
cd /workspace/esp32-nmea2000 cd /workspaces/esp32-nmea2000
pip3 install -U esptool pip3 install -U esptool
pip3 install platformio pip3 install platformio

View File

@@ -3,7 +3,7 @@
This code is free software; you can redistribute it and/or This code is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version. version 2 of the License, or (at your option) any later version.
This code is distributed in the hope that it will be useful, This code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU

View File

@@ -3,7 +3,7 @@
This code is free software; you can redistribute it and/or This code is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version. version 2 of the License, or (at your option) any later version.
This code is distributed in the hope that it will be useful, This code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
@@ -93,6 +93,7 @@ class GwSensorConfig{
} }
bool readConfig(T* s,GwConfigHandler *cfg){ bool readConfig(T* s,GwConfigHandler *cfg){
if (s == nullptr) return false; if (s == nullptr) return false;
if (prefix != s->prefix) return false;
configReader(s,cfg); configReader(s,cfg);
return s->ok; return s->ok;
} }

View File

@@ -66,18 +66,8 @@ GwSerial::~GwSerial()
if (lock != nullptr) vSemaphoreDelete(lock); if (lock != nullptr) vSemaphoreDelete(lock);
} }
String GwSerial::getMode(){ int GwSerial::getType() {
switch (type){ return 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; } bool GwSerial::isInitialized() { return initialized; }

View File

@@ -42,7 +42,7 @@ class GwSerial : public GwChannelInterface{
virtual Stream *getStream(bool partialWrites); virtual Stream *getStream(bool partialWrites);
bool getAvailableWrite(){return availableWrite;} 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 void begin(unsigned long baud, uint32_t config=SERIAL_8N1, int8_t rxPin=-1, int8_t txPin=-1)=0;
virtual String getMode() override; virtual int getType() override;
friend GwSerialStream; friend GwSerialStream;
}; };
@@ -122,6 +122,7 @@ template<typename T>
setError(serial,logger); setError(serial,logger);
}; };
}; };

View File

@@ -6,7 +6,6 @@
#include "GwSocketHelper.h" #include "GwSocketHelper.h"
#include "GWWifi.h" #include "GWWifi.h"
GwUdpReader::GwUdpReader(const GwConfigHandler *config, GwLog *logger, int minId) GwUdpReader::GwUdpReader(const GwConfigHandler *config, GwLog *logger, int minId)
{ {
this->config = config; this->config = config;

View File

@@ -3,7 +3,7 @@
This code is free software; you can redistribute it and/or This code is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version. version 2 of the License, or (at your option) any later version.
This code is distributed in the hope that it will be useful, This code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU

View File

@@ -3,7 +3,7 @@
This code is free software; you can redistribute it and/or This code is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version. version 2 of the License, or (at your option) any later version.
This code is distributed in the hope that it will be useful, This code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU

View File

@@ -3,7 +3,7 @@
This code is free software; you can redistribute it and/or This code is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version. version 2 of the License, or (at your option) any later version.
This code is distributed in the hope that it will be useful, This code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU

View File

@@ -3,7 +3,7 @@
This code is free software; you can redistribute it and/or This code is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version. version 2 of the License, or (at your option) any later version.
This code is distributed in the hope that it will be useful, This code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU

View File

@@ -3,7 +3,7 @@
This code is free software; you can redistribute it and/or This code is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version. version 2 of the License, or (at your option) any later version.
This code is distributed in the hope that it will be useful, This code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU

View File

@@ -1,5 +1,13 @@
#pragma once #pragma once
#include <Arduino.h> #include <Arduino.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
static inline int64_t gwMonotonicUs(){
TickType_t ticks=xTaskGetTickCount();
return ((int64_t)ticks) * ((int64_t)portTICK_PERIOD_MS) * 1000;
}
class TimeAverage{ class TimeAverage{
double factor=0.3; double factor=0.3;
double current=0; double current=0;
@@ -70,7 +78,7 @@ class TimeMonitor{
} }
void reset(){ void reset(){
if (last != 0 && start != 0) loop->add(last-start); if (last != 0 && start != 0) loop->add(last-start);
start=esp_timer_get_time(); start=gwMonotonicUs();
for (size_t i=0;i<len;i++) current[i]=0; for (size_t i=0;i<len;i++) current[i]=0;
count++; count++;
@@ -114,7 +122,7 @@ class TimeMonitor{
for (size_t i=0;i<index;i++){ for (size_t i=0;i<index;i++){
if (current[i] != 0) sv=current[i]; if (current[i] != 0) sv=current[i];
} }
int64_t now=esp_timer_get_time(); int64_t now=gwMonotonicUs();
last=now; last=now;
current[index]=now; current[index]=now;
int64_t currentv=now-sv; int64_t currentv=now-sv;

View File

@@ -220,6 +220,10 @@ public:
{ {
return api->getNMEA2000(); return api->getNMEA2000();
} }
virtual tN2kDeviceList *getN2kDeviceList()
{
return api->getN2kDeviceList();
}
virtual GwBoatData *getBoatData() virtual GwBoatData *getBoatData()
{ {
return api->getBoatData(); return api->getBoatData();

View File

@@ -431,7 +431,8 @@ GwXDRFoundMapping GwXDRMappings::getMapping(String xName,String xType,String xUn
} }
return selectMapping(&(it->second),instance,n183Key.c_str()); return selectMapping(&(it->second),instance,n183Key.c_str());
} }
GwXDRFoundMapping GwXDRMappings::getMapping(GwXDRCategory category,int selector,int field,int instance){ GwXDRFoundMapping GwXDRMappings::getMapping(double value,GwXDRCategory category,int selector,int field,int instance){
if (value == N2kDoubleNA) return GwXDRFoundMapping(); //do not add to unknown mappings
unsigned long n2kKey=GwXDRMappingDef::n2kKey(category,selector,field); unsigned long n2kKey=GwXDRMappingDef::n2kKey(category,selector,field);
auto it=n2kMap.find(n2kKey); auto it=n2kMap.find(n2kKey);
if (it == n2kMap.end()){ if (it == n2kMap.end()){

View File

@@ -244,7 +244,7 @@ class GwXDRMappings{
//get the mappings //get the mappings
//the returned mapping will exactly contain one mapping def //the returned mapping will exactly contain one mapping def
GwXDRFoundMapping getMapping(String xName,String xType,String xUnit); GwXDRFoundMapping getMapping(String xName,String xType,String xUnit);
GwXDRFoundMapping getMapping(GwXDRCategory category,int selector,int field=0,int instance=-1); GwXDRFoundMapping getMapping(double value,GwXDRCategory category,int selector,int field=0,int instance=-1);
String getXdrEntry(String mapping, double value,int instance=0); String getXdrEntry(String mapping, double value,int instance=0);
const char * getUnMapped(); const char * getUnMapped();
const GwXDRType * findType(const String &typeString, const String &unitString) const; const GwXDRType * findType(const String &typeString, const String &unitString) const;

View File

@@ -190,3 +190,14 @@ build_flags =
${env.build_flags} ${env.build_flags}
upload_port = /dev/esp32 upload_port = /dev/esp32
upload_protocol = esptool upload_protocol = esptool
[env:s3devkitm-generic]
extends = sensors
board = esp32-s3-devkitm-1
lib_deps =
${env.lib_deps}
${sensors.lib_deps}
build_flags =
${env.build_flags}
upload_port = /dev/esp32
upload_protocol = esptool

View File

@@ -2,6 +2,7 @@ Import("env", "projenv")
import os import os
import glob import glob
import shutil import shutil
import re
print("##post script running") print("##post script running")
HDROFFSET=288 HDROFFSET=288
@@ -39,6 +40,7 @@ def post(source,target,env):
appoffset=env.subst("$ESP32_APP_OFFSET") appoffset=env.subst("$ESP32_APP_OFFSET")
firmware=env.subst("$BUILD_DIR/${PROGNAME}.bin") firmware=env.subst("$BUILD_DIR/${PROGNAME}.bin")
(fwname,version)=getFirmwareInfo(firmware) (fwname,version)=getFirmwareInfo(firmware)
fwname=re.sub(r"[^0-9A-Za-z_.-]*","",fwname)
print("found fwname=%s, fwversion=%s"%(fwname,version)) print("found fwname=%s, fwversion=%s"%(fwname,version))
python=env.subst("$PYTHONEXE") python=env.subst("$PYTHONEXE")
print("base=%s,esptool=%s,appoffset=%s,uploaderflags=%s"%(base,esptool,appoffset,uploaderflags)) print("base=%s,esptool=%s,appoffset=%s,uploaderflags=%s"%(base,esptool,appoffset,uploaderflags))
@@ -70,10 +72,12 @@ def post(source,target,env):
print("running %s"%" ".join(cmd)) print("running %s"%" ".join(cmd))
env.Execute(" ".join(cmd),"#testpost") env.Execute(" ".join(cmd),"#testpost")
ofversion="-"+version ofversion="-"+version
versionedFile=os.path.join(outdir,"%s%s-update.bin"%(base,ofversion)) versionedFile=os.path.join(outdir,"%s%s-update.bin"%(fwname,ofversion))
shutil.copyfile(firmware,versionedFile) shutil.copyfile(firmware,versionedFile)
versioneOutFile=os.path.join(outdir,"%s%s-all.bin"%(base,ofversion)) print(f"wrote {versionedFile}")
versioneOutFile=os.path.join(outdir,"%s%s-all.bin"%(fwname,ofversion))
shutil.copyfile(outfile,versioneOutFile) shutil.copyfile(outfile,versioneOutFile)
print(f"wrote {versioneOutFile}")
env.AddPostAction( env.AddPostAction(
"$BUILD_DIR/${PROGNAME}.bin", "$BUILD_DIR/${PROGNAME}.bin",
post post

View File

@@ -3,7 +3,7 @@
This code is free software; you can redistribute it and/or This code is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version. version 2 of the License, or (at your option) any later version.
This code is distributed in the hope that it will be useful, This code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
@@ -72,9 +72,9 @@ const unsigned long HEAP_REPORT_TIME=2000; //set to 0 to disable heap reporting
#define MAX_NMEA2000_MESSAGE_SEASMART_SIZE 500 #define MAX_NMEA2000_MESSAGE_SEASMART_SIZE 500
#define MAX_NMEA0183_MESSAGE_SIZE MAX_NMEA2000_MESSAGE_SEASMART_SIZE #define MAX_NMEA0183_MESSAGE_SIZE MAX_NMEA2000_MESSAGE_SEASMART_SIZE
//assert length of firmware name and version //assert length of firmware name and version
CASSERT(strlen(FIRMWARE_TYPE) <= 32, "environment name (FIRMWARE_TYPE) must not exceed 32 chars"); CASSERT(strlen(FIRMWARE_TYPE) <= 31, "environment name (FIRMWARE_TYPE) must not exceed 32 chars");
CASSERT(strlen(VERSION) <= 32, "VERSION must not exceed 32 chars"); CASSERT(strlen(VERSION) <= 31, "VERSION must not exceed 32 chars");
CASSERT(strlen(IDF_VERSION) <= 32,"IDF_VERSION must not exceed 32 chars"); CASSERT(strlen(IDF_VERSION) <= 31,"IDF_VERSION must not exceed 32 chars");
//https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/app_image_format.html //https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/app_image_format.html
//and removed the bugs in the doc... //and removed the bugs in the doc...
__attribute__((section(".rodata_custom_desc"))) esp_app_desc_t custom_app_desc = { __attribute__((section(".rodata_custom_desc"))) esp_app_desc_t custom_app_desc = {
@@ -100,6 +100,7 @@ GwLog logger(LOGLEVEL,NULL);
GwConfigHandler config(&logger); GwConfigHandler config(&logger);
#include "Nmea2kTwai.h" #include "Nmea2kTwai.h"
#include <N2kDeviceList.h>
static const unsigned long CAN_RECOVERY_PERIOD=3000; //ms static const unsigned long CAN_RECOVERY_PERIOD=3000; //ms
static const unsigned long NMEA2000_HEARTBEAT_INTERVAL=5000; static const unsigned long NMEA2000_HEARTBEAT_INTERVAL=5000;
class Nmea2kTwaiLog : public Nmea2kTwai{ class Nmea2kTwaiLog : public Nmea2kTwai{
@@ -126,6 +127,7 @@ class Nmea2kTwaiLog : public Nmea2kTwai{
#endif #endif
Nmea2kTwai &NMEA2000=*(new Nmea2kTwaiLog((gpio_num_t)ESP32_CAN_TX_PIN,(gpio_num_t)ESP32_CAN_RX_PIN,CAN_RECOVERY_PERIOD,&logger)); Nmea2kTwai &NMEA2000=*(new Nmea2kTwaiLog((gpio_num_t)ESP32_CAN_TX_PIN,(gpio_num_t)ESP32_CAN_RX_PIN,CAN_RECOVERY_PERIOD,&logger));
tN2kDeviceList *pN2kDeviceList;
#ifdef GWBUTTON_PIN #ifdef GWBUTTON_PIN
bool fixedApPass=false; bool fixedApPass=false;
@@ -342,6 +344,9 @@ public:
virtual Nmea2kTwai *getNMEA2000(){ virtual Nmea2kTwai *getNMEA2000(){
return &NMEA2000; return &NMEA2000;
} }
virtual tN2kDeviceList *getN2kDeviceList(){
return pN2kDeviceList;
}
virtual GwBoatData *getBoatData(){ virtual GwBoatData *getBoatData(){
return &boatData; return &boatData;
} }
@@ -776,7 +781,7 @@ void loopFunction(void *){
//if(Serial1.available()) {} //if(Serial1.available()) {}
//if(Serial.available()) {} //if(Serial.available()) {}
//if(Serial2.available()) {} //if(Serial2.available()) {}
//delay(1); vTaskDelay(1);
} }
} }
const String USERPREFIX="/api/user/"; const String USERPREFIX="/api/user/";
@@ -944,6 +949,7 @@ void setup() {
NMEA2000.SetMsgHandler([](const tN2kMsg &n2kMsg){ NMEA2000.SetMsgHandler([](const tN2kMsg &n2kMsg){
handleN2kMessage(n2kMsg,N2K_CHANNEL_ID); handleN2kMessage(n2kMsg,N2K_CHANNEL_ID);
}); });
pN2kDeviceList = new tN2kDeviceList(&NMEA2000);
NMEA2000.Open(); NMEA2000.Open();
logger.logDebug(GwLog::LOG,"starting addon tasks"); logger.logDebug(GwLog::LOG,"starting addon tasks");
logger.flush(); logger.flush();

Some files were not shown because too many files have changed in this diff Show More