Merge remote-tracking branch 'upstream/master'

This commit is contained in:
wt 2023-12-18 15:40:33 +01:00
commit 3241ef2e20
106 changed files with 7834 additions and 1736 deletions

77
.circleci/config.yml Normal file
View File

@ -0,0 +1,77 @@
# Use the latest 2.1 version of CircleCI pipeline process engine.
# See: https://circleci.com/docs/configuration-reference
version: 2.1
parameters:
run_build:
type: boolean
default: false
environment:
type: string
default: "m5stack-atom"
build_flags:
type: string
default: ""
config:
type: string
default: "{}"
suffix:
type: string
default: ""
orbs:
python: circleci/python@1.4.0
# Define a job to be invoked later in a workflow.
# See: https://circleci.com/docs/configuration-reference/#jobs
jobs:
pio-build:
executor: python/default
# Add steps to the job
# See: https://circleci.com/docs/configuration-reference/#steps
environment:
PLATFORMIO_BUILD_FLAGS: << pipeline.parameters.build_flags >>
steps:
- checkout
- run:
name: "platformio install"
command: "pip install --upgrade platformio"
- run:
name: "build"
command: "pio run -e << pipeline.parameters.environment >>"
- run:
name: "save config"
working_directory: ".pio/build/<< pipeline.parameters.environment >>"
command: "echo '<< pipeline.parameters.config >>' > config.json"
- run:
name: "save build config"
working_directory: ".pio/build/<< pipeline.parameters.environment >>"
command: "echo 'GIT_SHA=\"<< pipeline.git.revision >>\" PLATFORMIO_BUILD_FLAGS=\"<< pipeline.parameters.build_flags >>\" pio run -e << pipeline.parameters.environment >> ' > buildconfig.txt"
- run:
name: "rename"
working_directory: ".pio/build/<< pipeline.parameters.environment >>"
command: "mv firmware.bin << pipeline.parameters.environment >><< pipeline.parameters.suffix >>-update.bin"
- when:
condition:
not:
equal: [ << pipeline.parameters.suffix >> ,""]
steps:
- run:
name: "rename2"
working_directory: ".pio/build/<< pipeline.parameters.environment >>"
command: "mv << pipeline.parameters.environment >>-all.bin << pipeline.parameters.environment >><< pipeline.parameters.suffix >>-all.bin"
- run:
name: "compress"
working_directory: ".pio/build/<< pipeline.parameters.environment >>"
command: "zip << pipeline.parameters.environment >><< pipeline.parameters.suffix >>.zip << pipeline.parameters.environment >>*.bin config.json buildconfig.txt"
- store_artifacts:
path: .pio/build/<< pipeline.parameters.environment >>/<< pipeline.parameters.environment >><< pipeline.parameters.suffix >>.zip
destination: << pipeline.parameters.environment >><< pipeline.parameters.suffix >>.zip
# Orchestrate jobs using workflows
# See: https://circleci.com/docs/configuration-reference/#workflows
workflows:
build-workflow:
when: << pipeline.parameters.run_build >>
jobs:
- pio-build:
filters:
tags:
only: /.*/

View File

@ -7,7 +7,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-20.04]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:

3
.gitignore vendored
View File

@ -4,4 +4,5 @@
.vscode/launch.json
.vscode/ipch
generated/*
lib/generated
lib/generated
webinstall/token.php

View File

@ -38,6 +38,7 @@ What is included
* a WEB UI to configure the gateway and to show the data that has been received
* a USB Actisense to NMEA2000 gateway
* a NMEA2000 to USB Actisense gateway
* starting with 201311xx some I2C Sensors
For the details of the mapped PGNs and NMEA sentences refer to [Conversions](doc/Conversions.pdf).
@ -46,11 +47,15 @@ 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.
For the list of hardware set ups refer to [Hardware](doc/Hardware.md).
There is a couple of prebuild binaries that you can directly flash to your device. For other combinations of hardware there is an [online build service](doc/BuildService.md) that will allow you to select your hardware and trigger a build.
Installation
------------
In the [release section](../../releases) you can find a couple of pre-build binaries.<br>
They are devided into binaries for an initial flash (xxx-all.bin) and binaries for updating an existing device (xxx-update.bin).
They are devided into binaries for an initial flash (xxx-all.bin) and binaries for updating an existing device (xxx-update.bin).
For other Hardware refer to the [online build service](https://circleci.com/).
Initial Flash
*************
@ -160,6 +165,17 @@ For details refer to the [example description](lib/exampletask/Readme.md).
Changelog
---------
[20231105](../../releases/tag/20231105)
**********
* support for ESP32-S3
* own [TWAI](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/twai.html) based driver for the NMEA2000 bus
* add NMEA2000 node address and status to the status tab
* ability to change the AP ip address
* [online build service ](doc/BuildService.md) to select the components you need
* restructuring of the lib_deps handling (much shorter compile time <br>__Hint__: if this introduces problems for your build, revert back the [lib_ldf_mode](https://github.com/wellenvogel/esp32-nmea2000/blob/9b955d135d74937a60f2926e8bfb9395585ff8cd/platformio.ini#L50) to chain+
* integration of a couple of I2C sensors (e.g. M5 ENVIII, BME280)
* More functionality for user tasks (counter, interface between tasks, dynamic registration, adding fixed XDR mappings) - refer to the [example description](lib/exampletask/Readme.md)
[20230317](../../releases/tag/20230317)
**********
* correctly convert bar to Pascal in XDR records

49
doc/BuildService.md Normal file
View File

@ -0,0 +1,49 @@
Online Build Service
--------------------
As the supported hardware combinations are already a lot and to support the suage of the converter also in your own designs there is an online build service available that will allow you to select the hardware combinations you would like to have and to start a build.
__Access__
To access the build service go to [wellenvogel.de](https://www.wellenvogel.net/software/esp32/cibuild.html).
To prevent the build service from being misused you need to authenticate with a username and password.
* User: ESP32NMEA2K
* Pass: esp32ci
Just consider that the user and password could change in the future - so when bookmarking just always start here and check for the current username and password.
__Workflow__
On the page you select the Hardware combination that fits your needs.
![buildservice](CiBuild1.png).
The selected board type and the build flags will be show at the bottom.
Whenever an existing build with the same parameters is found it will be shown and you will be able to download the build results - or directly go to the [WebInstaller](https://www.wellenvogel.net/software/esp32/install.html).
If there is no existing build the "Start" button is active and you can trigger a build.
At the bottom you see the status of the build and you get a link to the [used build service](https://app.circleci.com/pipelines/github/wellenvogel/esp32-nmea2000).
![buildservice](CiBuild2.png).
Once the build is complete you will get the buttons to download the results or directly use them for the web installer.
__Build Results__
The build result is always a zip file that contains a flash for the initial installation, an flash for the update installation and the used configuration for the build.
This zip file should be stored on your side as the build results will be deleted after 30 days.
If you later on would like to update with a new release you can upload your configuration using the "LOADCFG" button (either the zip file or the json file from within the zip).
You can also separately store the configuration of your build using the "SAVECFG" button.
__Hardware__
Currently a couple of different hardware configurations are supported. For M5 devices you typically select the processor unit, the "base" and some groove units.
For other node mcu boards you can freely configure the peripherals that you would like to connect.
__Local Build__
As the selection of hardware is basically controlled by defines (-D flags on the command line) you can also use the build GUI to create a build command that you run locally (assumimng you have the build environment installed).
Just click the "?" button beside "Build Flags". The PopUp will give you a "Copy" button that will copy the complete command line you can use for the build.
__Remarks__
Although the online build service tries to make a couple of checks to prevent impossible hardware configurations you still need to check your design (especially if you are using the "free" nodemcu settings).
The free build service at [CircleCi](https://circleci.com/) has a couple of limitation - so please be careful and avoid too many bauilds that you potentially don't need.

BIN
doc/CiBuild1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

BIN
doc/CiBuild2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

View File

@ -78,6 +78,13 @@ Can be used e.g. as an NMEA2000 Adapter for a laptop running e.g. OpenCPN with t
![OpenCPN on Laptop via USB and MFD on Android via WiFi](in_action1.jpg)
![OpenCPN on Laptop via USB and AvNav on Android via WiFi](in_action2.jpg)
M5 Stack AtomS3Lite Canunit (experimental since dev20230826)
---------------------
* Hardware: [M5_ATOMS3 Lite](http://docs.m5stack.com/en/core/AtomS3%20Lite) + [CAN Unit](http://docs.m5stack.com/en/unit/can)
* Prebuild Binary: m5stack-atoms3-canunit-all.bin
* Build Define: BOARD_M5ATOMS3_CANUNIT
* Power: Via USB
M5 Stick C Canunit
------------------
* Hardware: [M5_StickC+](http://docs.m5stack.com/en/core/m5stickc_plus) + [CAN Unit](http://docs.m5stack.com/en/unit/can)

View File

@ -7,6 +7,11 @@ import inspect
import json
import glob
from datetime import datetime
import re
import pprint
from platformio.project.config import ProjectConfig
Import("env")
#print(env.Dump())
OWN_FILE="extra_script.py"
@ -14,6 +19,7 @@ GEN_DIR='lib/generated'
CFG_FILE='web/config.json'
XDR_FILE='web/xdrconfig.json'
CFG_INCLUDE='GwConfigDefinitions.h'
CFG_INCLUDE_IMPL='GwConfigDefImpl.h'
XDR_INCLUDE='GwXdrTypeMappings.h'
TASK_INCLUDE='GwUserTasks.h'
EMBEDDED_INCLUDE="GwEmbeddedFiles.h"
@ -102,6 +108,44 @@ def mergeConfig(base,other):
base=base+merge
return base
def replaceTexts(data,replacements):
if replacements is None:
return data
if isinstance(data,str):
for k,v in replacements.items():
data=data.replace("$"+k,str(v))
return data
if isinstance(data,list):
rt=[]
for e in data:
rt.append(replaceTexts(e,replacements))
return rt
if isinstance(data,dict):
rt={}
for k,v in data.items():
rt[replaceTexts(k,replacements)]=replaceTexts(v,replacements)
return rt
return data
def expandConfig(config):
rt=[]
for item in config:
type=item.get('type')
if type != 'array':
rt.append(item)
continue
replacements=item.get('replace')
children=item.get('children')
name=item.get('name')
if name is None:
name="#unknown#"
if not isinstance(replacements,list):
raise Exception("missing replacements at array %s"%name)
for replace in replacements:
if children is not None:
for c in children:
rt.append(replaceTexts(c,replace))
return rt
def generateMergedConfig(inFile,outFile,addDirs=[]):
if not os.path.exists(inFile):
raise Exception("unable to read cfg file %s"%inFile)
@ -109,45 +153,70 @@ def generateMergedConfig(inFile,outFile,addDirs=[]):
with open(inFile,'rb') as ch:
config=json.load(ch)
config=mergeConfig(config,addDirs)
config=expandConfig(config)
data=json.dumps(config,indent=2)
writeFileIfChanged(outFile,data)
def generateCfg(inFile,outFile,addDirs=[]):
def generateCfg(inFile,outFile,impl):
if not os.path.exists(inFile):
raise Exception("unable to read cfg file %s"%inFile)
data=""
with open(inFile,'rb') as ch:
config=json.load(ch)
config=mergeConfig(config,addDirs)
config=json.load(ch)
data+="//generated from %s\n"%inFile
data+='#include "GwConfigItem.h"\n'
l=len(config)
data+='class GwConfigDefinitions{\n'
data+=' public:\n'
data+=' int getNumConfig() const{return %d;}\n'%(l)
for item in config:
n=item.get('name')
if n is None:
continue
if len(n) > 15:
raise Exception("%s: config names must be max 15 caracters"%n)
data+=' static constexpr const __FlashStringHelper* %s=F("%s");\n'%(n,n)
data+=' protected:\n'
data+=' GwConfigInterface *configs[%d]={\n'%(l)
first=True
for item in config:
if not first:
data+=',\n'
first=False
secret="false";
if item.get('type') == 'password':
secret="true"
data+=" new GwConfigInterface(%s,\"%s\",%s)"%(item.get('name'),item.get('default'),secret)
data+='};\n'
data+='};\n'
idx=0
if not impl:
data+='#include "GwConfigItem.h"\n'
data+='class GwConfigDefinitions{\n'
data+=' public:\n'
data+=' int getNumConfig() const{return %d;}\n'%(l)
for item in config:
n=item.get('name')
if n is None:
continue
if len(n) > 15:
raise Exception("%s: config names must be max 15 caracters"%n)
data+=' static constexpr const char* %s="%s";\n'%(n,n)
data+="};\n"
else:
data+='void GwConfigHandler::populateConfigs(GwConfigInterface **config){\n'
for item in config:
name=item.get('name')
if name is None:
continue
data+=' configs[%d]=\n'%(idx)
idx+=1
secret="false";
if item.get('type') == 'password':
secret="true"
data+=" #undef __CFGMODE\n"
data+=" #ifdef CFGMODE_%s\n"%(name)
data+=" #define __CFGMODE CFGMODE_%s\n"%(name)
data+=" #else\n"
data+=" #define __CFGMODE GwConfigInterface::NORMAL\n"
data+=" #endif\n"
data+=" #ifdef CFGDEFAULT_%s\n"%(name)
data+=" new GwConfigInterface(%s,CFGDEFAULT_%s,%s,__CFGMODE)\n"%(name,name,secret)
data+=" #else\n"
data+=" new GwConfigInterface(%s,\"%s\",%s,__CFGMODE)\n"%(name,item.get('default'),secret)
data+=" #endif\n"
data+=";\n"
data+='}\n'
for item in config:
name=item.get('name')
if name is None:
continue
data+="#ifdef CFGMODE_%s\n"%(name)
data+=" __MSG(\"CFGMODE_%s=\" __XSTR(CFGMODE_%s))\n"%(name,name)
data+="#endif\n"
data+="#ifdef CFGDEFAULT_%s\n"%(name)
data+=" __MSG(\"CFGDEFAULT_%s=\" CFGDEFAULT_%s)\n"%(name,name)
data+="#endif\n"
writeFileIfChanged(outFile,data)
def labelFilter(label):
return re.sub("[^a-zA-Z0-9]","",re.sub("\([0-9]*\)","",label))
def generateXdrMappings(fp,oh,inFile=''):
jdoc=json.load(fp)
oh.write("static GwXDRTypeMapping* typeMappings[]={\n")
@ -186,15 +255,56 @@ def generateXdrMappings(fp,oh,inFile=''):
oh.write(" new GwXDRTypeMapping(%d,%d,%d) /*%s:%s*/"%(cid,id,tc,cat,l))
oh.write("\n")
oh.write("};\n")
for cat in jdoc:
item=jdoc[cat]
cid=item.get('id')
if cid is None:
continue
selectors=item.get('selector')
if selectors is not None:
for selector in selectors:
label=selector.get('l')
value=selector.get('v')
if label is not None and value is not None:
label=labelFilter(label)
define=("GWXDRSEL_%s_%s"%(cat,label)).upper()
oh.write(" #define %s %s\n"%(define,value))
fields=item.get('fields')
if fields is not None:
idx=0
for field in fields:
v=field.get('v')
if v is None:
v=idx
else:
v=int(v)
label=field.get('l')
if v is not None and label is not None:
define=("GWXDRFIELD_%s_%s"%(cat,labelFilter(label))).upper();
oh.write(" #define %s %s\n"%(define,str(v)))
idx+=1
userTaskDirs=[]
def getUserTaskDirs():
rt=[]
taskdirs=glob.glob(os.path.join('lib','*task*'))
taskdirs=glob.glob(os.path.join( basePath(),'lib','*task*'))
for task in taskdirs:
rt.append(task)
return rt
def checkAndAdd(file,names,ilist):
if not file.endswith('.h'):
return
match=False
for cmp in names:
#print("##check %s<->%s"%(f.lower(),cmp))
if file.lower() == cmp:
match=True
if not match:
return
ilist.append(file)
def genereateUserTasks(outfile):
includes=[]
for task in userTaskDirs:
@ -202,16 +312,7 @@ def genereateUserTasks(outfile):
base=os.path.basename(task)
includeNames=[base.lower()+".h",'gw'+base.lower()+'.h']
for f in os.listdir(task):
if not f.endswith('.h'):
continue
match=False
for cmp in includeNames:
#print("##check %s<->%s"%(f.lower(),cmp))
if f.lower() == cmp:
match=True
if not match:
continue
includes.append(f)
checkAndAdd(f,includeNames,includes)
includeData=""
for i in includes:
print("#task include %s"%i)
@ -237,16 +338,92 @@ def getContentType(fn):
return "text/css"
return "application/octet-stream"
def getLibs():
base=os.path.join(basePath(),"lib")
rt=[]
for sd in os.listdir(base):
if sd == '..':
continue
if sd == '.':
continue
fn=os.path.join(base,sd)
if os.path.isdir(fn):
rt.append(sd)
EXTRAS=['generated']
for e in EXTRAS:
if not e in rt:
rt.append(e)
return rt
OWNLIBS=getLibs()+["FS","WiFi"]
GLOBAL_INCLUDES=[]
def handleDeps(env):
#overwrite the GetProjectConfig
#to inject all our libs
oldGetProjectConfig=env.GetProjectConfig
def GetProjectConfigX(env):
rt=oldGetProjectConfig()
cenv="env:"+env['PIOENV']
libs=[]
for section,options in rt.as_tuple():
if section == cenv:
for key,values in options:
if key == 'lib_deps':
libs=values
mustUpdate=False
for lib in OWNLIBS:
if not lib in libs:
libs.append(lib)
mustUpdate=True
if mustUpdate:
update=[(cenv,[('lib_deps',libs)])]
rt.update(update)
return rt
env.AddMethod(GetProjectConfigX,"GetProjectConfig")
#store the list of all includes after we resolved
#the dependencies for our main project
#we will use them for all compilations afterwards
oldLibBuilder=env.ConfigureProjectLibBuilder
def ConfigureProjectLibBuilderX(env):
global GLOBAL_INCLUDES
project=oldLibBuilder()
#print("##ConfigureProjectLibBuilderX")
#pprint.pprint(project)
if project.depbuilders:
#print("##depbuilders %s"%",".join(map(lambda x: x.path,project.depbuilders)))
for db in project.depbuilders:
idirs=db.get_include_dirs()
for id in idirs:
if not id in GLOBAL_INCLUDES:
GLOBAL_INCLUDES.append(id)
return project
env.AddMethod(ConfigureProjectLibBuilderX,"ConfigureProjectLibBuilder")
def injectIncludes(env,node):
return env.Object(
node,
CPPPATH=env["CPPPATH"]+GLOBAL_INCLUDES
)
env.AddBuildMiddleware(injectIncludes)
def prebuild(env):
global userTaskDirs
print("#prebuild running")
if not checkDir():
sys.exit(1)
ldf_mode=env.GetProjectOption("lib_ldf_mode")
if ldf_mode == 'off':
print("##ldf off - own dependency handling")
handleDeps(env)
userTaskDirs=getUserTaskDirs()
mergedConfig=os.path.join(outPath(),os.path.basename(CFG_FILE))
generateMergedConfig(os.path.join(basePath(),CFG_FILE),mergedConfig,userTaskDirs)
compressFile(mergedConfig,mergedConfig+".gz")
generateCfg(mergedConfig,os.path.join(outPath(),CFG_INCLUDE))
generateCfg(mergedConfig,os.path.join(outPath(),CFG_INCLUDE),False)
generateCfg(mergedConfig,os.path.join(outPath(),CFG_INCLUDE_IMPL),True)
embedded=getEmbeddedFiles(env)
filedefs=[]
for ef in embedded:
@ -280,10 +457,15 @@ def cleangenerated(source, target, env):
fn=os.path.join(od,f)
os.unlink(f)
print("#prescript...")
prebuild(env)
board="PLATFORM_BOARD_%s"%env["BOARD"].replace("-","_").upper()
print("Board=#%s#"%board)
print("BuildFlags=%s"%(" ".join(env["BUILD_FLAGS"])))
env.Append(
LINKFLAGS=[ "-u", "custom_app_desc" ]
LINKFLAGS=[ "-u", "custom_app_desc" ],
CPPDEFINES=[(board,"1")]
)
#script does not run on clean yet - maybe in the future
env.AddPostAction("clean",cleangenerated)

View File

@ -5,6 +5,10 @@
#include "NMEA0183Msg.h"
#include "GWConfig.h"
#include "GwBoatData.h"
#include "GwXDRMappings.h"
#include <map>
class GwApi;
typedef void (*GwUserTaskFunction)(GwApi *);
//API to be used for additional tasks
class GwApi{
public:
@ -32,7 +36,47 @@ class GwApi{
return format;
}
};
/**
* an interface for the data exchange between tasks
* the core part will not handle this data at all but
* this interface ensures that there is a correct locking of the
* data to correctly handle multi threading
* The user code should not use this intterface directly
* but instead it should use the static functions
* apiGetXXX(GwApi *,...)
* apiSetXXX(GwApi *,...)
* that will be created by the macro DECLARE_TASK_INTERFACE
*/
class TaskInterfaces
{
public:
class Base
{
public:
virtual ~Base()
{
}
};
using Ptr = std::shared_ptr<Base>;
protected:
virtual bool iset(const String &file, const String &name, Ptr v) = 0;
virtual Ptr iget(const String &name, int &result) = 0;
virtual bool iclaim(const String &name, const String &task)=0;
public:
template <typename T>
bool set(const T &v){
return false;
}
template <typename T>
T get(int &res){
res=-1;
return T();
}
template <typename T>
bool claim(const String &task){
return false;
}
};
class Status{
public:
bool wifiApOn=false;
@ -47,6 +91,8 @@ class GwApi{
unsigned long usbTx=0;
unsigned long serRx=0;
unsigned long serTx=0;
unsigned long ser2Rx=0;
unsigned long ser2Tx=0;
unsigned long tcpSerRx=0;
unsigned long tcpSerTx=0;
int tcpClients=0;
@ -68,6 +114,8 @@ class GwApi{
usbTx=0;
serRx=0;
serTx=0;
ser2Rx=0;
ser2Tx=0;
tcpSerRx=0;
tcpSerTx=0;
tcpClients=0;
@ -75,7 +123,7 @@ class GwApi{
tcpClTx=0;
tcpClientConnected=false;
n2kRx=0;
n2kTx=0;
n2kTx=0;
}
};
/**
@ -109,7 +157,32 @@ class GwApi{
/**
* fill the status information
*/
virtual void getStatus(Status &status);
virtual void getStatus(Status &status)=0;
/**
* access to counters for a task
* thread safe
* use the value returned from addCounter for the other operations
*/
virtual int addCounter(const String &){return -1;}
virtual void increment(int idx,const String &name,bool failed=false){}
virtual void reset(int idx){}
virtual void remove(int idx){}
virtual TaskInterfaces * taskInterfaces()=0;
/**
* only allowed during init methods
*/
virtual bool addXdrMapping(const GwXDRMappingDef &)=0;
virtual void addCapability(const String &name, const String &value)=0;
/**
* add a user task
* this allows you decide based on defines/config if a user task really should be added
* so this is the preferred solution over DECLARE_USERTASK
* The name should be similar to the function name of the user task (although not mandatory)
*/
virtual bool addUserTask(GwUserTaskFunction task,const String Name, int stackSize=2000)=0;
/**
* not thread safe methods
* accessing boat data must only be executed from within the main thread
@ -118,6 +191,14 @@ class GwApi{
virtual GwBoatData *getBoatData()=0;
virtual ~GwApi(){}
};
/**
* a simple generic function to create runtime errors if some necessary values are not defined
*/
template<typename... T>
static void checkDef(T... args){};
#ifndef DECLARE_USERTASK
#define DECLARE_USERTASK(task)
#endif
@ -133,4 +214,60 @@ class GwApi{
#ifndef DECLARE_STRING_CAPABILITY
#define DECLARE_STRING_CAPABILITY(name,value)
#endif
/**
* macro to declare an interface that a task provides to other tasks
* this macro must be used in an File I<taskname>.h or GwI<taskname>.h
* the first parameter must be the name of the task that should be able
* to write this data (the same name as being used in DECLARE_TASK).
* The second parameter must be a type (class) that inherits from
* GwApi::TaskInterfaces::Base.
* This class must provide copy constructors and empty constructors.
* The attributes should only be simple values or structs but no pointers.
* example:
* class TestTaskApi: public GwApi::TaskInterfaces::Base{
* public:
* int ival1=0;
* int ival2=99;
* String sval="unset";
* };
* DECLARE_TASKIF(testTask,TestTaskApi);
* The macro will generate 2 static funtions:
*
* bool apiSetTestTaskApi(GwApi *api, const TestTaskApi &v);
* TestTaskApi apiGetTestTaskApi(GwApi *api, int &result);
*
* The setter will return true on success.
* It is intended to be used by the task that did declare the api.
* The getter will set the result to -1 if no data is available, otherwise
* it will return the update count (so you can check if there was a change
* compared to the last call).
* It is intended to be used by any task.
* Be aware that all the apis share a common namespace - so be sure to somehow
* make your API names unique.
*
*
*/
#define DECLARE_TASKIF_IMPL(type) \
template<> \
inline bool GwApi::TaskInterfaces::set(const type & v) {\
return iset(__FILE__,#type,GwApi::TaskInterfaces::Ptr(new type(v))); \
}\
template<> \
inline type GwApi::TaskInterfaces::get<type>(int &result) {\
auto ptr=iget(#type,result); \
if (!ptr) {\
result=-1; \
return type(); \
}\
type *tp=(type*)ptr.get(); \
return type(*tp); \
}\
template<> \
inline bool GwApi::TaskInterfaces::claim<type>(const String &task) {\
return iclaim(#type,task);\
}\
#ifndef DECLARE_TASKIF
#define DECLARE_TASKIF(type) DECLARE_TASKIF_IMPL(type)
#endif
#endif

View File

@ -15,4 +15,5 @@
#endif
#endif
#define FIRMWARE_TYPE GWSTRINGIFY(PIO_ENV_BUILD)
#define FIRMWARE_TYPE GWSTRINGIFY(PIO_ENV_BUILD)
#define IDF_VERSION GWSTRINGIFY(ESP_IDF_VERSION_MAJOR) "." GWSTRINGIFY(ESP_IDF_VERSION_MINOR) "." GWSTRINGIFY(ESP_IDF_VERSION_PATCH)

View File

@ -4,8 +4,9 @@
#include "GwLog.h"
#include <Arduino.h>
#include <map>
#include <vector>
#define GW_BOAT_VALUE_LEN 32
#define GWSC(name) static constexpr const __FlashStringHelper* name=F(#name)
#define GWSC(name) static constexpr const char* name=#name
//see https://github.com/wellenvogel/esp32-nmea2000/issues/44
//factor to convert from N2k/SI rad/s to current NMEA rad/min
@ -164,10 +165,10 @@ public:
virtual ~GwBoatItemNameProvider() {}
};
#define GWBOATDATA(type,name,time,fmt) \
static constexpr const __FlashStringHelper* _##name=F(#name); \
GwBoatItem<type> *name=new GwBoatItem<type>(F(#name),GwBoatItemBase::fmt,time,&values) ;
static constexpr const char* _##name=#name; \
GwBoatItem<type> *name=new GwBoatItem<type>(#name,GwBoatItemBase::fmt,time,&values) ;
#define GWSPECBOATDATA(clazz,name,time,fmt) \
clazz *name=new clazz(F(#name),GwBoatItemBase::fmt,time,&values) ;
clazz *name=new clazz(#name,GwBoatItemBase::fmt,time,&values) ;
class GwBoatData{
private:
GwLog *logger;

View File

@ -1,5 +0,0 @@
#ifndef _GWBUTTONS_H
#define _GWBUTTONS_H
//task function
void handleButtons(void *param);
#endif

View File

@ -1,7 +1,7 @@
#include "GwButtons.h"
#include "GwButtonTask.h"
#include "GwHardware.h"
#include "GwApi.h"
#include "GwLeds.h"
#include "GwLedTask.h"
class FactoryResetRequest: public GwMessage{
private:
@ -22,9 +22,13 @@ class FactoryResetRequest: public GwMessage{
},"reset",1000,NULL,0,NULL);
};
};
void handleButtons(void *param){
GwApi *api=(GwApi*)param;
void handleButtons(GwApi *api){
GwLog *logger=api->getLogger();
GwApi::TaskInterfaces *interfaces=api->taskInterfaces();
IButtonTask state;
if (!interfaces->set(state)){
LOG_DEBUG(GwLog::ERROR,"unable to set button state");
}
#ifndef GWBUTTON_PIN
LOG_DEBUG(GwLog::LOG,"no button pin defined, do not watch");
vTaskDelete(NULL);
@ -50,46 +54,51 @@ void handleButtons(void *param){
unsigned long lastReport=0;
const unsigned long OFF_TIME=20;
const unsigned long REPORT_TIME=1000;
const unsigned long HARD_REST_TIME=10000;
GwLedMode ledMode=LED_OFF;
const unsigned long PRESS_5_TIME=5000;
const unsigned long PRESS_10_TIME=10000;
const unsigned long PRESS_RESET_TIME=12000;
LOG_DEBUG(GwLog::LOG,"button task started");
while(true){
delay(10);
int current=digitalRead(GWBUTTON_PIN);
unsigned long now=millis();
IButtonTask::ButtonState lastState=state.state;
if (current != activeState){
if (lastPressed != 0 && (lastPressed+OFF_TIME) < now){
lastPressed=0; //finally off
firstPressed=0;
if (ledMode != LED_OFF){
setLedMode(LED_GREEN); //TODO: better "go back"
ledMode=LED_OFF;
}
state.state=IButtonTask::OFF;
LOG_DEBUG(GwLog::LOG,"Button press stopped");
}
if (state.state != lastState){
interfaces->set(state);
}
continue;
}
lastPressed=now;
if (firstPressed == 0) {
firstPressed=now;
LOG_DEBUG(GwLog::LOG,"Button press started");
state.pressCount++;
state.state=IButtonTask::PRESSED;
interfaces->set(state);
lastReport=now;
continue;
}
if (lastReport != 0 && (lastReport + REPORT_TIME) < now ){
LOG_DEBUG(GwLog::LOG,"Button active for %ld",(now-firstPressed));
lastReport=now;
}
GwLedMode nextMode=ledMode;
if (now > (firstPressed+HARD_REST_TIME/2)){
nextMode=LED_BLUE;
if (now > (firstPressed+PRESS_5_TIME)){
state.state=IButtonTask::PRESSED_5;
}
if (now > (firstPressed+HARD_REST_TIME*0.9)){
nextMode=LED_RED;
if (now > (firstPressed+PRESS_10_TIME)){
state.state=IButtonTask::PRESSED_10;
}
if (ledMode != nextMode){
setLedMode(nextMode);
ledMode=nextMode;
if (lastState != state.state){
interfaces->set(state);
}
if (now > (firstPressed+HARD_REST_TIME)){
if (now > (firstPressed+PRESS_RESET_TIME)){
LOG_DEBUG(GwLog::ERROR,"Factory reset by button");
GwMessage *r=new FactoryResetRequest(api);
api->getQueue()->sendAndForget(r);
@ -100,4 +109,14 @@ void handleButtons(void *param){
}
vTaskDelete(NULL);
#endif
}
void initButtons(GwApi *api){
#ifndef GWBUTTON_PIN
api->getLogger()->logDebug(GwLog::LOG,"no buttons defined, no button task");
return;
#endif
const String taskname("buttonTask");
api->addUserTask(handleButtons,taskname);
api->taskInterfaces()->claim<IButtonTask>(taskname);
}

View File

@ -0,0 +1,22 @@
#ifndef _GWBUTTONTASK_H
#define _GWBUTTONTASK_H
#include "GwApi.h"
//task function
void initButtons(GwApi *param);
DECLARE_INITFUNCTION(initButtons);
class IButtonTask : public GwApi::TaskInterfaces::Base
{
public:
typedef enum
{
OFF,
PRESSED,
PRESSED_5, // 5...10s
PRESSED_10 //>10s
} ButtonState;
ButtonState state=OFF;
long pressCount=0;
};
DECLARE_TASKIF(IButtonTask);
#endif

View File

@ -57,7 +57,7 @@ GwChannel::GwChannel(GwLog *logger,
this->logger = logger;
this->name=name;
this->sourceId=sourceId;
this->maxSourceId=sourceId;
this->maxSourceId=maxSourceId;
this->countIn=new GwCounter<String>(String("count")+name+String("in"));
this->countOut=new GwCounter<String>(String("count")+name+String("out"));
this->impl=NULL;
@ -146,12 +146,15 @@ bool GwChannel::canReceive(const char *buffer){
}
int GwChannel::getJsonSize(){
int rt=2;
int rt=JSON_OBJECT_SIZE(6);
if (countIn) rt+=countIn->getJsonSize();
if (countOut) rt+=countOut->getJsonSize();
return rt;
}
void GwChannel::toJson(GwJsonDocument &doc){
JsonObject jo=doc.createNestedObject("ch"+name);
jo["id"]=sourceId;
jo["max"]=maxSourceId;
if (countOut) countOut->toJson(doc);
if (countIn) countIn->toJson(doc);
}

View File

@ -58,13 +58,135 @@ void GwChannelList::allChannels(ChannelAction action){
action(*it);
}
}
typedef struct {
int id;
const char *baud;
const char *receive;
const char *send;
const char *direction;
const char *toN2K;
const char *readF;
const char *writeF;
const char *name;
} SerialParam;
static SerialParam serialParameters[]={
{
.id=SERIAL1_CHANNEL_ID,
.baud=GwConfigDefinitions::serialBaud,
.receive=GwConfigDefinitions::receiveSerial,
.send=GwConfigDefinitions::sendSerial,
.direction=GwConfigDefinitions::serialDirection,
.toN2K=GwConfigDefinitions::serialToN2k,
.readF=GwConfigDefinitions::serialReadF,
.writeF=GwConfigDefinitions::serialWriteF,
.name="Serial"
},
{
.id=SERIAL2_CHANNEL_ID,
.baud=GwConfigDefinitions::serial2Baud,
.receive=GwConfigDefinitions::receiveSerial2,
.send=GwConfigDefinitions::sendSerial2,
.direction=GwConfigDefinitions::serial2Dir,
.toN2K=GwConfigDefinitions::serial2ToN2k,
.readF=GwConfigDefinitions::serial2ReadF,
.writeF=GwConfigDefinitions::serial2WriteF,
.name="Serial2"
}
};
static SerialParam *getSerialParam(int id){
for (size_t idx=0;idx<sizeof(serialParameters)/sizeof(SerialParam*);idx++){
if (serialParameters[idx].id == id) return &serialParameters[idx];
}
return nullptr;
}
void GwChannelList:: addSerial(HardwareSerial *stream,int id,int type,int rx,int tx){
const char *mode=nullptr;
switch (type)
{
case GWSERIAL_TYPE_UNI:
mode="UNI";
break;
case GWSERIAL_TYPE_BI:
mode="BI";
break;
case GWSERIAL_TYPE_RX:
mode="RX";
break;
case GWSERIAL_TYPE_TX:
mode="TX";
break;
}
if (mode == nullptr) {
LOG_DEBUG(GwLog::ERROR,"unknown serial type %d",type);
return;
}
addSerial(stream,id,mode,rx,tx);
}
void GwChannelList::addSerial(HardwareSerial *serialStream,int id,const String &mode,int rx,int tx){
SerialParam *param=getSerialParam(id);
if (param == nullptr){
logger->logDebug(GwLog::ERROR,"trying to set up an unknown serial channel: %d",id);
return;
}
if (rx < 0 && tx < 0){
logger->logDebug(GwLog::ERROR,"useless config for serial %d: both rx/tx undefined");
return;
}
modes[id]=String(mode);
bool canRead=false;
bool canWrite=false;
if (mode == "BI"){
canRead=config->getBool(param->receive);
canWrite=config->getBool(param->send);
}
if (mode == "TX"){
canWrite=true;
}
if (mode == "RX"){
canRead=true;
}
if (mode == "UNI"){
String cfgMode=config->getString(param->direction);
if (cfgMode == "receive"){
canRead=true;
}
if (cfgMode == "send"){
canWrite=true;
}
}
if (rx < 0) canRead=false;
if (tx < 0) canWrite=false;
LOG_DEBUG(GwLog::DEBUG,"serial set up: mode=%s,rx=%d,canRead=%d,tx=%d,canWrite=%d",
mode.c_str(),rx,(int)canRead,tx,(int)canWrite);
serialStream->begin(config->getInt(param->baud,115200),SERIAL_8N1,rx,tx);
GwSerial *serial = new GwSerial(logger, serialStream, id, canRead);
LOG_DEBUG(GwLog::LOG, "starting serial %d ", id);
GwChannel *channel = new GwChannel(logger, param->name, id);
channel->setImpl(serial);
channel->begin(
canRead || canWrite,
canWrite,
canRead,
config->getString(param->readF),
config->getString(param->writeF),
false,
config->getBool(param->toN2K),
false,
false);
LOG_DEBUG(GwLog::LOG, "%s", channel->toString().c_str());
theChannels.push_back(channel);
}
void GwChannelList::begin(bool fallbackSerial){
LOG_DEBUG(GwLog::DEBUG,"GwChannelList::begin");
GwChannel *channel=NULL;
//usb
if (! fallbackSerial){
GwSerial *usb=new GwSerial(NULL,0,USB_CHANNEL_ID);
usb->setup(config->getInt(config->usbBaud),3,1);
GwSerial *usb=new GwSerial(NULL,&USBSerial,USB_CHANNEL_ID);
USBSerial.begin(config->getInt(config->usbBaud));
logger->setWriter(new GwSerialLog(usb,config->getBool(config->usbActisense)));
logger->prefix="GWSERIAL:";
channel=new GwChannel(logger,"USB",USB_CHANNEL_ID);
@ -85,7 +207,7 @@ void GwChannelList::begin(bool fallbackSerial){
//TCP server
sockets=new GwSocketServer(config,logger,MIN_TCP_CHANNEL_ID);
sockets->begin();
channel=new GwChannel(logger,"TCP",MIN_TCP_CHANNEL_ID,MIN_TCP_CHANNEL_ID+10);
channel=new GwChannel(logger,"TCPserver",MIN_TCP_CHANNEL_ID,MIN_TCP_CHANNEL_ID+10);
channel->setImpl(sockets);
channel->begin(
true,
@ -102,57 +224,33 @@ void GwChannelList::begin(bool fallbackSerial){
theChannels.push_back(channel);
//serial 1
bool serCanRead=true;
bool serCanWrite=true;
int serialrx=-1;
int serialtx=-1;
#ifdef GWSERIAL_MODE
#ifdef GWSERIAL_TX
serialtx=GWSERIAL_TX;
#endif
#ifdef GWSERIAL_RX
serialrx=GWSERIAL_RX;
#endif
if (serialrx != -1 && serialtx != -1){
serialMode=GWSERIAL_MODE;
}
#ifndef GWSERIAL_TX
#define GWSERIAL_TX -1
#endif
#ifndef GWSERIAL_RX
#define GWSERIAL_RX -1
#endif
#ifdef GWSERIAL_TYPE
addSerial(&Serial1,SERIAL1_CHANNEL_ID,GWSERIAL_TYPE,GWSERIAL_RX,GWSERIAL_TX);
#else
#ifdef GWSERIAL_MODE
addSerial(&Serial1,SERIAL1_CHANNEL_ID,GWSERIAL_MODE,GWSERIAL_RX,GWSERIAL_TX);
#endif
#endif
//serial 2
#ifndef GWSERIAL2_TX
#define GWSERIAL2_TX -1
#endif
#ifndef GWSERIAL2_RX
#define GWSERIAL2_RX -1
#endif
#ifdef GWSERIAL2_TYPE
addSerial(&Serial2,SERIAL2_CHANNEL_ID,GWSERIAL2_TYPE,GWSERIAL2_RX,GWSERIAL2_TX);
#else
#ifdef GWSERIAL2_MODE
addSerial(&Serial2,SERIAL2_CHANNEL_ID,GWSERIAL2_MODE,GWSERIAL2_RX,GWSERIAL2_TX);
#endif
#endif
//the serial direction is from the config (only valid for mode UNI)
String serialDirection=config->getString(config->serialDirection);
//we only consider the direction if mode is UNI
if (serialMode != String("UNI")){
serialDirection=String("");
//if mode is UNI it depends on the selection
serCanRead=config->getBool(config->receiveSerial);
serCanWrite=config->getBool(config->sendSerial);
}
if (serialDirection == "receive" || serialDirection == "off" || serialMode == "RX") serCanWrite=false;
if (serialDirection == "send" || serialDirection == "off" || serialMode == "TX") serCanRead=false;
LOG_DEBUG(GwLog::DEBUG,"serial set up: mode=%s,direction=%s,rx=%d,tx=%d",
serialMode.c_str(),serialDirection.c_str(),serialrx,serialtx
);
if (serialtx != -1 || serialrx != -1 ){
LOG_DEBUG(GwLog::LOG,"creating serial interface rx=%d, tx=%d",serialrx,serialtx);
GwSerial *serial=new GwSerial(logger,1,SERIAL1_CHANNEL_ID,serCanRead);
int rt=serial->setup(config->getInt(config->serialBaud,115200),serialrx,serialtx);
LOG_DEBUG(GwLog::LOG,"starting serial returns %d",rt);
channel=new GwChannel(logger,"SER",SERIAL1_CHANNEL_ID);
channel->setImpl(serial);
channel->begin(
serCanRead || serCanWrite,
serCanWrite,
serCanRead,
config->getString(config->serialReadF),
config->getString(config->serialWriteF),
false,
config->getBool(config->serialToN2k),
false,
false
);
LOG_DEBUG(GwLog::LOG,"%s",channel->toString().c_str());
theChannels.push_back(channel);
}
//tcp client
bool tclEnabled=config->getBool(config->tclEnabled);
channel=new GwChannel(logger,"TCPClient",TCP_CLIENT_CHANNEL_ID);
@ -180,6 +278,11 @@ void GwChannelList::begin(bool fallbackSerial){
LOG_DEBUG(GwLog::LOG,"%s",channel->toString().c_str());
logger->flush();
}
String GwChannelList::getMode(int id){
auto it=modes.find(id);
if (it != modes.end()) return it->second;
return "UNKNOWN";
}
int GwChannelList::getJsonSize(){
int rt=0;
allChannels([&](GwChannel *c){
@ -219,6 +322,11 @@ void GwChannelList::fillStatus(GwApi::Status &status){
status.serRx=channel->countRx();
status.serTx=channel->countTx();
}
channel=getChannelById(SERIAL2_CHANNEL_ID);
if (channel){
status.ser2Rx=channel->countRx();
status.ser2Tx=channel->countTx();
}
channel=getChannelById(MIN_TCP_CHANNEL_ID);
if (channel){
status.tcpSerRx=channel->countRx();

View File

@ -1,19 +1,22 @@
#pragma once
#include <functional>
#include <vector>
#include <map>
#include <WString.h>
#include "GwChannel.h"
#include "GwLog.h"
#include "GWConfig.h"
#include "GwJsonDocument.h"
#include "GwApi.h"
#include <HardwareSerial.h>
//NMEA message channels
#define N2K_CHANNEL_ID 0
#define USB_CHANNEL_ID 1
#define SERIAL1_CHANNEL_ID 2
#define TCP_CLIENT_CHANNEL_ID 3
#define MIN_TCP_CHANNEL_ID 4
#define SERIAL2_CHANNEL_ID 3
#define TCP_CLIENT_CHANNEL_ID 4
#define MIN_TCP_CHANNEL_ID 5
#define MIN_USER_TASK 200
class GwSocketServer;
@ -24,10 +27,11 @@ class GwChannelList{
GwConfigHandler *config;
typedef std::vector<GwChannel *> ChannelList;
ChannelList theChannels;
std::map<int,String> modes;
GwSocketServer *sockets;
GwTcpClient *client;
String serialMode=F("NONE");
void addSerial(HardwareSerial *stream,int id,const String &mode,int rx,int tx);
void addSerial(HardwareSerial *stream,int id,int type,int rx,int tx);
public:
GwChannelList(GwLog *logger, GwConfigHandler *config);
typedef std::function<void(GwChannel *)> ChannelAction;
@ -40,6 +44,6 @@ class GwChannelList{
//single channel
GwChannel *getChannelById(int sourceId);
void fillStatus(GwApi::Status &status);
String getMode(int id);
};

View File

@ -1,7 +1,11 @@
#define CFG_MESSAGES
#include <Preferences.h>
#include "GWConfig.h"
#include <ArduinoJson.h>
#include <string.h>
#include <MD5Builder.h>
#include "GwHardware.h"
#include "GwConfigDefImpl.h"
#define B(v) (v?"true":"false")
@ -55,14 +59,20 @@ GwConfigInterface * GwConfigHandler::getConfigItem(const String name, bool dummy
GwConfigHandler::GwConfigHandler(GwLog *logger): GwConfigDefinitions(){
this->logger=logger;
saltBase=esp_random();
configs=new GwConfigInterface*[getNumConfig()];
populateConfigs(configs);
prefs=new Preferences();
}
GwConfigHandler::~GwConfigHandler(){
delete prefs;
}
bool GwConfigHandler::loadConfig(){
prefs.begin(PREF_NAME,true);
prefs->begin(PREF_NAME,true);
for (int i=0;i<getNumConfig();i++){
String v=prefs.getString(configs[i]->getName().c_str(),configs[i]->getDefault());
String v=prefs->getString(configs[i]->getName().c_str(),configs[i]->getDefault());
configs[i]->value=v;
}
prefs.end();
prefs->end();
return true;
}
@ -77,19 +87,19 @@ bool GwConfigHandler::updateValue(String name, String value){
return false;
}
LOG_DEBUG(GwLog::LOG,"update config %s=>%s",name.c_str(),i->isSecret()?"***":value.c_str());
prefs.begin(PREF_NAME,false);
prefs.putString(i->getName().c_str(),value);
prefs.end();
prefs->begin(PREF_NAME,false);
prefs->putString(i->getName().c_str(),value);
prefs->end();
}
return true;
}
bool GwConfigHandler::reset(){
LOG_DEBUG(GwLog::LOG,"reset config");
prefs.begin(PREF_NAME,false);
prefs->begin(PREF_NAME,false);
for (int i=0;i<getNumConfig();i++){
prefs.putString(configs[i]->getName().c_str(),configs[i]->getDefault());
prefs->putString(configs[i]->getName().c_str(),configs[i]->getDefault());
}
prefs.end();
prefs->end();
return true;
}
String GwConfigHandler::getString(const String name, String defaultv) const{
@ -97,6 +107,11 @@ String GwConfigHandler::getString(const String name, String defaultv) const{
if (!i) return defaultv;
return i->asString();
}
const char * GwConfigHandler::getCString(const String name, const char *defaultv) const{
GwConfigInterface *i=getConfigItem(name,false);
if (!i) return defaultv;
return i->asCString();
}
bool GwConfigHandler::getBool(const String name, bool defaultv) const{
GwConfigInterface *i=getConfigItem(name,false);
if (!i) return defaultv;
@ -110,11 +125,12 @@ int GwConfigHandler::getInt(const String name,int defaultv) const{
void GwConfigHandler::stopChanges(){
allowChanges=false;
}
bool GwConfigHandler::setValue(String name,String value){
bool GwConfigHandler::setValue(String name,String value, bool hide){
if (! allowChanges) return false;
GwConfigInterface *i=getConfigItem(name,false);
if (!i) return false;
i->value=value;
i->type=hide?GwConfigInterface::HIDDEN:GwConfigInterface::READONLY;
return true;
}
@ -159,6 +175,24 @@ void GwConfigHandler::toHex(unsigned long v, char *buffer, size_t bsize)
buffer[2 * i] = 0;
}
std::vector<String> GwConfigHandler::getSpecial() const{
std::vector<String> rt;
rt.reserve(numSpecial());
for (int i=0L;i<getNumConfig();i++){
if (configs[i]->getType() != GwConfigInterface::NORMAL){
rt.push_back(configs[i]->getName());
};
}
return rt;
}
int GwConfigHandler::numSpecial() const{
int rt=0;
for (int i=0L;i<getNumConfig();i++){
if (configs[i]->getType() != GwConfigInterface::NORMAL) rt++;
}
return rt;
}
void GwNmeaFilter::handleToken(String token, int index){
switch(index){
case 0:

View File

@ -1,19 +1,20 @@
#ifndef _GWCONFIG_H
#define _GWCONFIG_H
#include <Arduino.h>
#include <Preferences.h>
#include "GwLog.h"
#include "GwConfigItem.h"
#include "GwConfigDefinitions.h"
#include <map>
#include <vector>
class Preferences;
class GwConfigHandler: public GwConfigDefinitions{
private:
Preferences prefs;
Preferences *prefs;
GwLog *logger;
typedef std::map<String,String> StringMap;
boolean allowChanges=true;
GwConfigInterface **configs;
public:
public:
GwConfigHandler(GwLog *logger);
@ -26,17 +27,69 @@ class GwConfigHandler: public GwConfigDefinitions{
String getString(const String name,const String defaultv="") const;
bool getBool(const String name,bool defaultv=false) const ;
int getInt(const String name,int defaultv=0) const;
const char * getCString(const String name, const char *defaultv="") const;
GwConfigInterface * getConfigItem(const String name, bool dummy=false) const;
bool checkPass(String hash);
std::vector<String> getSpecial() const;
int numSpecial() const;
/**
* change the value of a config item
* will become a noop after stopChanges has been called
* !use with care! no checks of the value
*/
bool setValue(String name, String value);
bool setValue(String name, String value, bool hide=false);
static void toHex(unsigned long v,char *buffer,size_t bsize);
unsigned long getSaltBase(){return saltBase;}
~GwConfigHandler();
bool userChangesAllowed(){return allowChanges;}
template <typename T>
bool getValue(T & target, const String &name, int defaultv=0){
GwConfigInterface *i=getConfigItem(name);
if (!i){
target=(T)defaultv;
return false;
}
target=(T)(i->asInt());
return true;
}
bool getValue(int &target, const String &name, int defaultv=0){
GwConfigInterface *i=getConfigItem(name);
if (!i){
target=defaultv;
return false;
}
target=i->asInt();
return true;
}
bool getValue(long &target, const String &name, long defaultv=0){
GwConfigInterface *i=getConfigItem(name);
if (!i){
target=defaultv;
return false;
}
target=i->asInt();
return true;
}
bool getValue(bool &target, const String name, bool defaultv=false){
GwConfigInterface *i=getConfigItem(name);
if (!i){
target=defaultv;
return false;
}
target=i->asBoolean();
return true;
}
bool getValue(String &target, const String name, const String &defaultv=""){
GwConfigInterface *i=getConfigItem(name);
if (!i){
target=defaultv;
return false;
}
target=i->asString();
return true;
}
private:
unsigned long saltBase=0;
void populateConfigs(GwConfigInterface **);
};
#endif

View File

@ -5,17 +5,25 @@
class GwConfigHandler;
class GwConfigInterface{
public:
typedef enum {
NORMAL=0,
HIDDEN=1,
READONLY=2
} ConfigType;
private:
String name;
const char * initialValue;
String value;
bool secret=false;
ConfigType type=NORMAL;
public:
GwConfigInterface(const String &name, const char * initialValue, bool secret=false){
GwConfigInterface(const String &name, const char * initialValue, bool secret=false,ConfigType type=NORMAL){
this->name=name;
this->initialValue=initialValue;
this->value=initialValue;
this->secret=secret;
this->type=type;
}
virtual String asString() const{
return value;
@ -41,6 +49,9 @@ class GwConfigInterface{
String getDefault() const {
return initialValue;
}
ConfigType getType() const {
return type;
}
friend class GwConfigHandler;
};
@ -62,5 +73,7 @@ class GwNmeaFilter{
String toString();
};
#define __XSTR(x) __STR(x)
#define __STR(x) #x
#define __MSG(x) _Pragma (__STR(message (x)))
#endif

View File

@ -11,9 +11,12 @@ template<class T> class GwCounter{
unsigned long globalFail=0;
String name;
public:
GwCounter(String name){
GwCounter(const String &name){
this->name=name;
};
void setName(const String &name){
this->name=name;
}
void reset(){
okCounter.clear();
failCounter.clear();

View File

@ -3,20 +3,86 @@
#ifdef BOARD_TEST
#include "GwExampleTask.h"
#include "GwApi.h"
#include "GWConfig.h"
#include <vector>
#include "N2kMessages.h"
#include "GwXdrTypeMappings.h"
/**
* INVALID!!! - the next interface declaration will not work
* as it is not in the correct header file
* it is just included here to show you how errors
* could be created.
* if you call the apiSetExampleNotWorkingIf method
* it will always return false
*/
class ExampleNotWorkingIf: public GwApi::TaskInterfaces::Base{
public:
int someValue=99;
};
DECLARE_TASKIF(ExampleNotWorkingIf);
void exampleTask(GwApi *param);
/**
* an init function that ist being called before other initializations from the core
*/
void exampleInit(GwApi *api){
api->getLogger()->logDebug(GwLog::LOG,"example init running");
//this example is a more or less useless example how you could set some
//config value to a fixed value
//you can only set config values within the init function
//you could also compute this value from some own configuration
//for this example it would make a lot of sense to declare a capability
//to hide this config item from the UI - see header file
api->getConfig()->setValue(api->getConfig()->minXdrInterval,"50");
// make the task known to the core
// the task function should not return (unless you delete the task - see example code)
const String taskName("exampleTask");
api->addUserTask(exampleTask, taskName, 4000);
// this would create our task with a stack size of 4000 bytes
// we declare some capabilities that we can
// use in config.json to only show some
// elements when this capability is set correctly
api->addCapability("testboard", "true");
api->addCapability("testboard2", "true");
// hide some config value
// and force it's default value
auto current=api->getConfig()->getConfigItem(GwConfigDefinitions::minXdrInterval,false);
String defaultXdrInt="50";
if (current){
defaultXdrInt=current->getDefault();
}
//with the true parameter this config value will be hidden
//if you would like the user to be able to see this item, omit the "true", the config value will be read only
api->getConfig()->setValue(GwConfigDefinitions::minXdrInterval,defaultXdrInt,true);
// example for a user defined help url that will be shown when clicking the help button
api->addCapability("HELP_URL", "https://www.wellenvogel.de");
//we would like to store data with the interfaces that we defined
//the name must match the one we used for addUserTask
api->taskInterfaces()->claim<ExampleTaskIf>(taskName);
//not working interface
if (!api->taskInterfaces()->claim<ExampleNotWorkingIf>(taskName)){
api->getLogger()->logDebug(GwLog::ERROR,"unable to claim ExampleNotWorkingIf");
}
//check if we should simulate some voltage measurements
//add an XDR mapping in this case
String voltageTransducer=api->getConfig()->getString(GwConfigDefinitions::exTransducer);
if (!voltageTransducer.isEmpty()){
int instance=api->getConfig()->getInt(GwConfigDefinitions::exInstanceId);
GwXDRMappingDef xdr;
//we send a battery message - so it is category battery
xdr.category=GwXDRCategory::XDRBAT;
//we only need a conversion from N2K to NMEA0183
xdr.direction=GwXDRMappingDef::Direction::M_FROM2K;
//you can find the XDR field and selector definitions
//in the generated GwXdrTypeMappings.h
xdr.field=GWXDRFIELD_BATTERY_BATTERYVOLTAGE;
//xdr.selector=-1; //refer to xdrconfig.json - there is no selector under Battery, so we can leave it empty
//we just map exactly our instance
xdr.instanceMode=GwXDRMappingDef::IS_SINGLE;
//those are the user configured values
//this instance is the one we use for the battery instance when we set up
//the N2K message
xdr.instanceId=instance;
xdr.xdrName=voltageTransducer;
if (!api->addXdrMapping(xdr)){
api->getLogger()->logDebug(GwLog::ERROR,"unable to set our xdr mapping %s",xdr.toString().c_str());
}
}
}
#define INVALID_COORD -99999
class GetBoatDataRequest: public GwMessage{
@ -97,6 +163,15 @@ void exampleTask(GwApi *api){
GwApi::BoatValue *testValue=new GwApi::BoatValue(boatItemName);
GwApi::BoatValue *valueList[]={longitude,latitude,testValue};
GwApi::Status status;
int counter=api->addCounter("usertest");
int apiResult=0;
ExampleTaskIf e1=api->taskInterfaces()->get<ExampleTaskIf>(apiResult);
LOG_DEBUG(GwLog::LOG,"exampleIf before rs=%d,v=%d,s=%s",apiResult,e1.count,e1.someValue.c_str());
ExampleNotWorkingIf nw1;
bool nwrs=api->taskInterfaces()->set(nw1);
LOG_DEBUG(GwLog::LOG,"exampleNotWorking update returned %d",(int)nwrs);
String voltageTransducer=api->getConfig()->getString(GwConfigDefinitions::exTransducer);
int voltageInstance=api->getConfig()->getInt(GwConfigDefinitions::exInstanceId);
while(true){
delay(1000);
/*
@ -193,8 +268,29 @@ void exampleTask(GwApi *api){
status.tcpClRx,
status.tcpClTx,
status.n2kRx,
status.n2kTx);
status.n2kTx);
//increment some counter
api->increment(counter,"Test");
ExampleTaskIf e2=api->taskInterfaces()->get<ExampleTaskIf>(apiResult);
LOG_DEBUG(GwLog::LOG,"exampleIf before update rs=%d,v=%d,s=%s",apiResult,e2.count,e2.someValue.c_str());
e1.count+=1;
e1.someValue="running";
bool rs=api->taskInterfaces()->set(e1);
LOG_DEBUG(GwLog::LOG,"exampleIf update rs=%d,v=%d,s=%s",(int)rs,e1.count,e1.someValue.c_str());
ExampleTaskIf e3=api->taskInterfaces()->get<ExampleTaskIf>(apiResult);
LOG_DEBUG(GwLog::LOG,"exampleIf after update rs=%d,v=%d,s=%s",apiResult,e3.count,e3.someValue.c_str());
if (!voltageTransducer.isEmpty()){
//simulate some voltage measurements...
double offset=100.0*(double)std::rand()/RAND_MAX - 50.0;
double simVoltage=(1200.0+offset)/100;
LOG_DEBUG(GwLog::LOG,"simulated voltage %f",(float)simVoltage);
tN2kMsg msg;
SetN2kDCBatStatus(msg,voltageInstance,simVoltage);
//we send out an N2K message
//and as we added an XDR mapping, we will see this in the data dashboard
//and on the NMEA0183 stream
api->sendN2kMessage(msg);
}
}
vTaskDelete(NULL);

View File

@ -2,50 +2,30 @@
#include "GwApi.h"
//we only compile for some boards
#ifdef BOARD_TEST
//we could add the following defines also in our local platformio.ini
//CAN base
#define M5_CAN_KIT
//RS485 on groove
#define SERIAL_GROOVE_485
#define ESP32_CAN_TX_PIN GPIO_NUM_22
#define ESP32_CAN_RX_PIN GPIO_NUM_19
//if using tail485
#define GWSERIAL_TX 26
#define GWSERIAL_RX 32
#define GWSERIAL_MODE "UNI"
#define GWBUTTON_PIN GPIO_NUM_39
#define GWBUTTON_ACTIVE LOW
//if GWBUTTON_PULLUPDOWN we enable a pulup/pulldown
#define GWBUTTON_PULLUPDOWN
//led handling
//if we define GWLED_FASTNET the arduino fastnet lib is used
#define GWLED_FASTLED
#define GWLED_TYPE SK6812
//color schema for fastled
#define GWLED_SCHEMA GRB
#define GWLED_PIN GPIO_NUM_27
//brightness 0...255
#define GWLED_BRIGHTNESS 64
void exampleTask(GwApi *param);
void exampleInit(GwApi *param);
//make the task known to the core
//the task function should not return (unless you delete the task - see example code)
//DECLARE_USERTASK(exampleTask)
//if your task is not happy with the default 2000 bytes of stack, replace the DECLARE_USERTASK
DECLARE_USERTASK_PARAM(exampleTask,4000);
//this would create our task with a stack size of 4000 bytes
//let the core call an init function before the
//N2K Stuff and the communication is set up
//normally you should not need this at all
//especially this init function will register the real task at the core
//this gives you some flexibility to decide based on config or defines whether you
//really want to start the task or not
//this function must return when done - otherwise the core will not start up
DECLARE_INITFUNCTION(exampleInit);
//we declare a capability that we can
//use in config.json to only show some
//elements when this capability is set correctly
DECLARE_CAPABILITY(testboard,true);
DECLARE_CAPABILITY(testboard2,true);
//hide some config value
//just set HIDE + the name of the config item to true
DECLARE_CAPABILITY(HIDEminXdrInterval,true);
//example for a user defined help url that will be shown when clicking the help button
DECLARE_STRING_CAPABILITY(HELP_URL,"https://www.wellenvogel.de");
/**
* an interface for the example task
*/
class ExampleTaskIf : public GwApi::TaskInterfaces::Base{
public:
long count=0;
String someValue;
};
DECLARE_TASKIF(ExampleTaskIf);
#endif

View File

@ -15,17 +15,52 @@ Files
This file is completely optional.
You only need this if you want to
extend the base configuration - we add a dummy library here and define one additional build environment (board)
* [GwExampleTask.h](GwExampleTask.h) the name of this include must match the name of the directory (ignoring case) with a "gw" in front. This file includes our special hardware definitions and registers our task at the core (DECLARE_USERTASK in the code). Avoid including headers from other libraries in this file as this could interfere with the main code. Just only include them in your .cpp files (or in other headers).
* [GwExampleTask.h](GwExampleTask.h) the name of this include must match the name of the directory (ignoring case) with a "gw" in front. This file includes our special hardware definitions and registers our task at the core.<br>
This registration can be done statically using [DECLARE_USERTASK](https://github.com/wellenvogel/esp32-nmea2000/blob/9b955d135d74937a60f2926e8bfb9395585ff8cd/lib/api/GwApi.h#L202) in the header file. <br>
As an alternative we just only register an [initialization function](https://github.com/wellenvogel/esp32-nmea2000/blob/9b955d135d74937a60f2926e8bfb9395585ff8cd/lib/exampletask/GwExampleTask.h#L19) using DECLARE_INITFUNCTION and later on register the task function itself via the [API](https://github.com/wellenvogel/esp32-nmea2000/blob/9b955d135d74937a60f2926e8bfb9395585ff8cd/lib/exampletask/GwExampleTask.cpp#L32).<br>
This gives you more flexibility - maybe you only want to start your task if certain config values are set.<br>
The init function itself should not interact with external hardware and should run fast. It needs to return otherwise the main code will not start up.<br>
Avoid including headers from other libraries in this file as this could interfere with the main code. Just only include them in your .cpp files (or in other headers).
Optionally it can define some capabilities (using DECLARE_CAPABILITY) that can be used in the config UI (see below)
There are some special capabilities you can set:
* HIDEsomeName: will hide the configItem "someName"
* HELP_URL: will set the url that is loaded when clicking the HELP tab (user DECLARE_STRING_CAPABILITY)
* HELP_URL: will set the url that is loaded when clicking the HELP tab (user DECLARE_STRING_CAPABILITY)<br>
* [GwExampleTaks.cpp](GwExampleTask.cpp) includes the implementation of our task. This tasks runs in an own thread - see the comments in the code.
We can have as many cpp (and header files) as we need to structure our code.
* [config.json](config.json)<br>
This file allows to add some config definitions that are needed for our task. For the possible options have a look at the global [config.json](../../web/config.json). Be careful not to overwrite config defitions from the global file. A good practice wood be to prefix the names of definitions with parts of the library name. Always put them in a separate category so that they do not interfere with the system ones.
The defined config items can later be accessed in the code (see the example in [GwExampleTask.cpp](GwExampleTask.cpp)).
Interfaces
----------
The task init function and the task function interact with the core using an [API](../api/GwApi.h) that they get when started.
The API has a couple of functions that only can be used inside an init function and others that can only be used inside a task function.
Avoid any other use of core functions other then via the API - otherwise you carefully have to consider thread synchronization!
The API allows you to
* access config data
* write logs
* send NMEA2000 messages
* send NMEA0183 messages
* get the currently available data values (as shown at the data tab)
* get some status information from the core
* send some requests to the core (only for very special functionality)
* add and increment counter (starting from 20231105)
* add some fixed [XDR](../../doc/XdrMappings.md) Mapping - see [example](https://github.com/wellenvogel/esp32-nmea2000/blob/9b955d135d74937a60f2926e8bfb9395585ff8cd/lib/exampletask/GwExampleTask.cpp#L63).
* add capabilities (since 20231105 - as an alternative to a static DECLARE_CAPABILITY )
* add a user task (since 20231105 - as an alternative to a static DECLARE_USERTASK)
* store or read task interface data (see below)
__Interfacing between Task__
Sometimes you may want to exchange data between different user tasks.<br> As this needs thread sychronization (and a place to store this data) there is an interface to handle this in a safe manner (since 20231105).<br>
The task that would like to provide some data for others must declare a [class](https://github.com/wellenvogel/esp32-nmea2000/blob/9b955d135d74937a60f2926e8bfb9395585ff8cd/lib/exampletask/GwExampleTask.h#L24) that stores this data. This must be declared in the task header file and you need to make this known to the code using DECLARE_TASKIF.<br>
Before you can use this interface for writing you need to ["claim"](https://github.com/wellenvogel/esp32-nmea2000/blob/9b955d135d74937a60f2926e8bfb9395585ff8cd/lib/exampletask/GwExampleTask.cpp#L55) it - this prevents other tasks from also writing this data.
Later on you are able to [write](https://github.com/wellenvogel/esp32-nmea2000/blob/9b955d135d74937a60f2926e8bfb9395585ff8cd/lib/exampletask/GwExampleTask.cpp#L278) this data via the api.<br>
Any other task that is interested in your data can read it at any time. The read function will provide an update counter - so the reading side can easily see if the data has been written.
The core uses this concept for the interworking between the [button task](../buttontask/) - writing - and the [led task](../ledtask/) - [reading](https://github.com/wellenvogel/esp32-nmea2000/blob/9b955d135d74937a60f2926e8bfb9395585ff8cd/lib/ledtask/GwLedTask.cpp#L52).
Hints
-----
Just be careful not to interfere with C symbols from the core - so it is a good practice to prefix your files and class like in the example.
@ -47,7 +82,4 @@ Files
By following the hints in this doc the merge should always succeed without conflicts.
Future Plans
------------
If there will be a need we can extend this extension API by means of adding specific java script code and css for the UI.

View File

@ -20,5 +20,30 @@
"capabilities": {
"testboard":"true"
}
},
{
"name": "exTransducer",
"label": "voltage transducer name",
"type": "String",
"default": "",
"description": "set the name for the xdr transducer for the simulated voltage, leave empty to disable ",
"category": "example",
"capabilities": {
"testboard":"true"
}
},
{
"name": "exInstanceId",
"label": "voltage instance id",
"type": "number",
"default": 99,
"description": "the N2K instance id for the simulated voltage ",
"category": "example",
"min": 0,
"max": 255,
"check": "checkMinMax",
"capabilities": {
"testboard":"true"
}
}
]
]

View File

@ -7,7 +7,6 @@
board = m5stack-atom
lib_deps =
${env.lib_deps}
own_lib
build_flags=
-D BOARD_TEST
${env.build_flags}

View File

@ -64,7 +64,6 @@ void GwWebServer::begin(){
GwWebServer::~GwWebServer(){
server->end();
delete server;
vQueueDelete(queue);
}
void GwWebServer::handleAsyncWebRequest(AsyncWebServerRequest *request, GwRequestMessage *msg)
{

View File

@ -11,10 +11,30 @@ GwWifi::GwWifi(const GwConfigHandler *config,GwLog *log, bool fixedApPass){
}
void GwWifi::setup(){
LOG_DEBUG(GwLog::LOG,"Wifi setup");
IPAddress AP_local_ip(192, 168, 15, 1); // Static address for AP
IPAddress AP_gateway(192, 168, 15, 1);
IPAddress AP_subnet(255, 255, 255, 0);
IPAddress defaultAddr(192,168,15,1);
IPAddress AP_local_ip; // Static address for AP
const String apip=config->getString(config->apIp);
bool cfgIpOk=false;
if (!apip.isEmpty()){
cfgIpOk= AP_local_ip.fromString(apip);
}
if (! cfgIpOk){
AP_local_ip=IPAddress(192,168,15,1);
LOG_DEBUG(GwLog::ERROR,"unable to set access point IP %s, falling back to %s",
apip.c_str(),AP_local_ip.toString().c_str());
}
IPAddress AP_gateway(AP_local_ip);
bool maskOk=false;
IPAddress AP_subnet;
const String apMask=config->getString(config->apMask);
if (!apMask.isEmpty()){
maskOk=AP_subnet.fromString(apMask);
}
if (! maskOk){
AP_subnet=IPAddress(255, 255, 255, 0);
LOG_DEBUG(GwLog::ERROR,"unable to set access point mask %s, falling back to %s",
apMask.c_str(),AP_subnet.toString().c_str());
}
WiFi.mode(WIFI_MODE_APSTA); //enable both AP and client
const char *ssid=config->getConfigItem(config->systemName)->asCString();
if (fixedApPass){
@ -33,7 +53,7 @@ void GwWifi::setup(){
lastApAccess=millis();
apShutdownTime=config->getConfigItem(config->stopApTime)->asInt() * 60;
if (apShutdownTime < 120 && apShutdownTime != 0) apShutdownTime=120; //min 2 minutes
LOG_DEBUG(GwLog::LOG,"GWWIFI: AP auto shutdown %s (%ds)",apShutdownTime> 0?"enabled":"disabled",apShutdownTime);
LOG_DEBUG(GwLog::ERROR,"GWWIFI: AP auto shutdown %s (%ds)",apShutdownTime> 0?"enabled":"disabled",apShutdownTime);
apShutdownTime=apShutdownTime*1000; //ms
clientIsConnected=false;
connectInternal();
@ -65,7 +85,7 @@ void GwWifi::loop(){
}
else{
if (! clientIsConnected){
LOG_DEBUG(GwLog::LOG,"client %s now connected",wifiSSID->asCString());
LOG_DEBUG(GwLog::LOG,"wifiClient %s now connected to",wifiSSID->asCString());
clientIsConnected=true;
}
}
@ -75,7 +95,7 @@ void GwWifi::loop(){
lastApAccess=millis();
}
if ((lastApAccess + apShutdownTime) < millis()){
LOG_DEBUG(GwLog::LOG,"GWWIFI: shutdown AP");
LOG_DEBUG(GwLog::ERROR,"GWWIFI: shutdown AP");
WiFi.softAPdisconnect(true);
apActive=false;
}

View File

@ -11,108 +11,124 @@
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifdef _NOGWHARDWAREUT
#error "you are not allowed to include GwHardware.h in your user task header"
#endif
#ifndef _GWHARDWARE_H
#define _GWHARDWARE_H
#define GWSERIAL_TYPE_UNI 1
#define GWSERIAL_TYPE_BI 2
#define GWSERIAL_TYPE_RX 3
#define GWSERIAL_TYPE_TX 4
#include <GwConfigItem.h>
#include <HardwareSerial.h>
#include "GwAppInfo.h"
#include "GwUserTasks.h"
//SERIAL_MODE can be: UNI (RX or TX only), BI (both), RX, TX
//board specific pins
//general definitions for M5AtomLite
//hint for groove pins:
//according to some schematics the numbering is 1,2,3(VCC),4(GND)
#ifdef PLATFORM_BOARD_M5STACK_ATOM
#define GROOVE_PIN_2 GPIO_NUM_26
#define GROOVE_PIN_1 GPIO_NUM_32
#define GWBUTTON_PIN GPIO_NUM_39
#define GWLED_FASTLED
#define GWLED_TYPE SK6812
//color schema for fastled
#define GWLED_SCHEMA GRB
#define GWLED_PIN GPIO_NUM_27
#define GWBUTTON_ACTIVE LOW
//if GWBUTTON_PULLUPDOWN we enable a pulup/pulldown
#define GWBUTTON_PULLUPDOWN
#define BOARD_LEFT1 GPIO_NUM_22
#define BOARD_LEFT2 GPIO_NUM_19
#define USBSerial Serial
#endif
//general definitiones for M5AtomS3
#ifdef PLATFORM_BOARD_M5STACK_ATOMS3
#define GROOVE_PIN_2 GPIO_NUM_2
#define GROOVE_PIN_1 GPIO_NUM_1
#define GWBUTTON_PIN GPIO_NUM_41
#define GWLED_FASTLED
#define GWLED_TYPE WS2812
//color schema for fastled
#define GWLED_SCHEMA GRB
#define GWLED_PIN GPIO_NUM_35
#define GWBUTTON_ACTIVE LOW
//if GWBUTTON_PULLUPDOWN we enable a pulup/pulldown
#define GWBUTTON_PULLUPDOWN
#define BOARD_LEFT1 GPIO_NUM_5
#define BOARD_LEFT2 GPIO_NUM_6
#endif
//M5Stick C
#ifdef PLATFORM_BOARD_M5STICK_C
#define GROOVE_PIN_2 GPIO_NUM_32
#define GROOVE_PIN_1 GPIO_NUM_31
#define USBSerial Serial
#endif
//NodeMCU 32 S
#ifdef PLATFORM_BOARD_NODEMCU_32S
#define USBSerial Serial
#endif
#ifdef BOARD_M5ATOM
#define ESP32_CAN_TX_PIN GPIO_NUM_22
#define ESP32_CAN_RX_PIN GPIO_NUM_19
#define M5_CAN_KIT
//150mA if we power from the bus
#define N2K_LOAD_LEVEL 3
//if using tail485
#define GWSERIAL_TX 26
#define GWSERIAL_RX 32
#define GWSERIAL_MODE "UNI"
#define GWBUTTON_PIN GPIO_NUM_39
#define GWBUTTON_ACTIVE LOW
//if GWBUTTON_PULLUPDOWN we enable a pulup/pulldown
#define GWBUTTON_PULLUPDOWN
//led handling
//if we define GWLED_FASTNET the arduino fastnet lib is used
#define GWLED_FASTLED
#define GWLED_TYPE SK6812
//color schema for fastled
#define GWLED_SCHEMA GRB
#define GWLED_PIN GPIO_NUM_27
//brightness 0...255
#define GWLED_BRIGHTNESS 64
#endif
#ifdef BOARD_M5ATOM_CANUNIT
#define ESP32_CAN_TX_PIN GPIO_NUM_26
#define ESP32_CAN_RX_PIN GPIO_NUM_32
#define GWBUTTON_PIN GPIO_NUM_39
#define GWBUTTON_ACTIVE LOW
//if GWBUTTON_PULLUPDOWN we enable a pulup/pulldown
#define GWBUTTON_PULLUPDOWN
//led handling
//if we define GWLED_FASTNET the arduino fastnet lib is used
#define GWLED_FASTLED
#define GWLED_TYPE SK6812
//color schema for fastled
#define GWLED_SCHEMA GRB
#define GWLED_PIN GPIO_NUM_27
#define SERIAL_GROOVE_485
//brightness 0...255
#define GWLED_BRIGHTNESS 64
#endif
#ifdef BOARD_M5ATOM_RS232_CANUNIT
#define ESP32_CAN_TX_PIN GPIO_NUM_26
#define ESP32_CAN_RX_PIN GPIO_NUM_32
//if using rs232
#define GWSERIAL_TX 19
#define GWSERIAL_RX 22
#define GWSERIAL_MODE "BI"
#define GWBUTTON_PIN GPIO_NUM_39
#define GWBUTTON_ACTIVE LOW
//if GWBUTTON_PULLUPDOWN we enable a pulup/pulldown
#define GWBUTTON_PULLUPDOWN
//led handling
//if we define GWLED_FASTNET the arduino fastnet lib is used
#define GWLED_FASTLED
#define GWLED_TYPE SK6812
//color schema for fastled
#define GWLED_SCHEMA GRB
#define GWLED_PIN GPIO_NUM_27
#ifdef BOARD_M5ATOMS3
#define M5_CAN_KIT
//150mA if we power from the bus
#define N2K_LOAD_LEVEL 3
//if using tail485
#define SERIAL_GROOVE_485
//brightness 0...255
#define GWLED_BRIGHTNESS 64
#endif
#ifdef BOARD_M5ATOM_CANUNIT
#define M5_CANUNIT
#define GWLED_BRIGHTNESS 64
//150mA if we power from the bus
#define N2K_LOAD_LEVEL 3
#endif
#ifdef BOARD_M5ATOMS3_CANUNIT
#define M5_CANUNIT
#define GWLED_BRIGHTNESS 64
#endif
#ifdef BOARD_M5ATOM_RS232_CANUNIT
#define M5_CANUNIT
#define M5_SERIAL_KIT_232
#define GWLED_BRIGHTNESS 64
#endif
#ifdef BOARD_M5ATOM_RS485_CANUNIT
#define ESP32_CAN_TX_PIN GPIO_NUM_26
#define ESP32_CAN_RX_PIN GPIO_NUM_32
//if using rs232
#define GWSERIAL_TX 19
#define GWSERIAL_RX 22
#define GWSERIAL_MODE "UNI"
#define GWBUTTON_PIN GPIO_NUM_39
#define GWBUTTON_ACTIVE LOW
//if GWBUTTON_PULLUPDOWN we enable a pulup/pulldown
#define GWBUTTON_PULLUPDOWN
//led handling
//if we define GWLED_FASTNET the arduino fastnet lib is used
#define GWLED_FASTLED
#define GWLED_TYPE SK6812
//color schema for fastled
#define GWLED_SCHEMA GRB
#define GWLED_PIN GPIO_NUM_27
//brightness 0...255
#define M5_SERIAL_KIT_485
#define M5_CANUNIT
#define GWLED_BRIGHTNESS 64
#endif
#ifdef BOARD_M5STICK_CANUNIT
#define ESP32_CAN_TX_PIN GPIO_NUM_32
#define ESP32_CAN_RX_PIN GPIO_NUM_33
#define M5_CANUNIT
#endif
#ifdef BOARD_HOMBERGER
#define ESP32_CAN_TX_PIN GPIO_NUM_5
#define ESP32_CAN_RX_PIN GPIO_NUM_4
//serial input only
#define GWSERIAL_RX 16
#define GWSERIAL_MODE "RX"
#define GWSERIAL_RX GPIO_NUM_16
#define GWSERIAL_TYPE GWSERIAL_TYPE_RX
#define GWBUTTON_PIN GPIO_NUM_0
#define GWBUTTON_ACTIVE LOW
@ -120,4 +136,190 @@
#define GWBUTTON_PULLUPDOWN
#endif
//M5 Serial (Atomic RS232 Base)
#ifdef M5_SERIAL_KIT_232
#define _GWM5_BOARD
#define GWSERIAL_TX BOARD_LEFT2
#define GWSERIAL_RX BOARD_LEFT1
#define GWSERIAL_TYPE GWSERIAL_TYPE_BI
#endif
//M5 Serial (Atomic RS485 Base)
#ifdef M5_SERIAL_KIT_485
#ifdef _GWM5_BOARD
#error "can only define one M5 base"
#endif
#define _GWM5_BOARD
#define GWSERIAL_TX BOARD_LEFT2
#define GWSERIAL_RX BOARD_LEFT1
#define GWSERIAL_TYPE GWSERIAL_TYPE_UNI
#endif
//M5 GPS (Atomic GPS Base)
#ifdef M5_GPS_KIT
#ifdef _GWM5_BOARD
#error "can only define one M5 base"
#endif
#define _GWM5_BOARD
#define GWSERIAL_RX BOARD_LEFT1
#define GWSERIAL_TYPE GWSERIAL_TYPE_RX
#define CFGDEFAULT_serialBaud "9600"
#define CFGMODE_serialBaud GwConfigInterface::READONLY
#endif
//below we define the final device config based on the above
//boards and peripherals
//this allows us to easily also set them from outside
//serial adapter at the M5 groove pins
//we use serial2 for groove serial if serial1 is already defined
//before (e.g. by serial kit)
#ifdef SERIAL_GROOVE_485
#define _GWM5_GROOVE
#ifdef GWSERIAL_TYPE
#define GWSERIAL2_TX GROOVE_PIN_2
#define GWSERIAL2_RX GROOVE_PIN_1
#define GWSERIAL2_TYPE GWSERIAL_TYPE_UNI
#else
#define GWSERIAL_TX GROOVE_PIN_2
#define GWSERIAL_RX GROOVE_PIN_1
#define GWSERIAL_TYPE GWSERIAL_TYPE_UNI
#endif
#endif
#ifdef SERIAL_GROOVE_232
#ifdef _GWM5_GROOVE
#error "can only have one groove device"
#endif
#define _GWM5_GROOVE
#ifdef GWSERIAL_TYPE
#define GWSERIAL2_TX GROOVE_PIN_2
#define GWSERIAL2_RX GROOVE_PIN_1
#define GWSERIAL2_TYPE GWSERIAL_TYPE_BI
#else
#define GWSERIAL_TX GROOVE_PIN_2
#define GWSERIAL_RX GROOVE_PIN_1
#define GWSERIAL_TYPE GWSERIAL_TYPE_BI
#endif
#endif
//http://docs.m5stack.com/en/unit/gps
#ifdef M5_GPS_UNIT
#ifdef _GWM5_GROOVE
#error "can only have one M5 groove"
#endif
#define _GWM5_GROOVE
#ifdef GWSERIAL_TYPE
#define GWSERIAL2_RX GROOVE_PIN_1
#define GWSERIAL2_TYPE GWSERIAL_TYPE_RX
#define CFGDEFAULT_serialBaud "9600"
#define CFGMODE_serialBaud GwConfigInterface::READONLY
#else
#define GWSERIAL_RX GROOVE_PIN_1
#define GWSERIAL_TYPE GWSERIAL_TYPE_RX
#define CFGDEFAULT_serial2Baud "9600"
#define CFGMODE_serial2Baud GwConfigInterface::READONLY
#endif
#endif
//can kit for M5 Atom
#ifdef M5_CAN_KIT
#ifdef _GWM5_BOARD
#error "can only define one M5 base"
#endif
#define _GWM5_BOARD
#define ESP32_CAN_TX_PIN BOARD_LEFT1
#define ESP32_CAN_RX_PIN BOARD_LEFT2
#endif
//CAN via groove
#ifdef M5_CANUNIT
#ifdef _GWM5_GROOVE
#error "can only have one M5 groove"
#endif
#define _GWM5_GROOVE
#define ESP32_CAN_TX_PIN GROOVE_PIN_2
#define ESP32_CAN_RX_PIN GROOVE_PIN_1
#endif
#ifdef M5_ENV3
#ifndef M5_GROOVEIIC
#define M5_GROOVEIIC
#endif
#ifndef GWSHT3X
#define GWSHT3X -1
#endif
#ifndef GWQMP6988
#define GWQMP6988 -1
#endif
#endif
#ifdef M5_GROOVEIIC
#ifdef _GWM5_GROOVE
#error "can only have one M5 groove"
#endif
#define _GWM5_GROOVE
#ifdef GWIIC_SCL
#error "you cannot define both GWIIC_SCL and M5_GROOVEIIC"
#endif
#define GWIIC_SCL GROOVE_PIN_1
#ifdef GWIIC_SDA
#error "you cannot define both GWIIC_SDA and M5_GROOVEIIC"
#endif
#define GWIIC_SDA GROOVE_PIN_2
#endif
#ifdef GWIIC_SDA
#ifndef GWIIC_SCL
#error "you must both define GWIIC_SDA and GWIIC_SCL"
#endif
#endif
#ifdef GWIIC_SCL
#ifndef GWIIC_SDA
#error "you must both define GWIIC_SDA and GWIIC_SCL"
#endif
#define _GWIIC
#endif
#ifdef GWIIC_SDA2
#ifndef GWIIC_SCL2
#error "you must both define GWIIC_SDA2 and GWIIC_SCL2"
#endif
#endif
#ifdef GWIIC_SCL2
#ifndef GWIIC_SDA2
#error "you must both define GWIIC_SDA and GWIIC_SCL2"
#endif
#define _GWIIC
#endif
#ifndef GWLED_TYPE
#ifdef GWLED_CODE
#if GWLED_CODE == 0
#define GWLED_TYPE SK6812
#endif
#if GWLED_CODE == 1
#define GWLED_TYPE WS2812
#endif
#endif
#endif
#ifdef GWLED_TYPE
#define GWLED_FASTLED
#ifndef GWLED_BRIGHTNESS
#define GWLED_BRIGHTNESS 64
#endif
#endif
#ifdef ESP32_CAN_TX_PIN
#ifndef N2K_LOAD_LEVEL
#define N2K_LOAD_LEVEL 3
#endif
#endif
#ifdef GWLED_FASTLED
#define CFGMODE_ledBrightness GwConfigInterface::NORMAL
#ifdef GWLED_BRIGHTNESS
#define CFGDEFAULT_ledBrightness GWSTRINGIFY(GWLED_BRIGHTNESS)
#endif
#else
#define CFGMODE_ledBrightness GwConfigInterface::HIDDEN
#endif
#endif

180
lib/iictask/GwBME280.cpp Normal file
View File

@ -0,0 +1,180 @@
#include "GwBME280.h"
#ifdef _GWIIC
#if defined(GWBME280) || defined(GWBME28011) || defined(GWBME28012)|| defined(GWBME28021)|| defined(GWBME28022)
#define _GWBME280
#else
#undef _GWBME280
#endif
#else
#undef _GWBME280
#undef GWBME280
#undef GWBME28011
#undef GWBME28012
#undef GWBME28021
#undef GWBME28022
#endif
#ifdef _GWBME280
#include <Adafruit_BME280.h>
#endif
#ifdef _GWBME280
#define PRFX1 "BME28011"
#define PRFX2 "BME28012"
#define PRFX3 "BME28021"
#define PRFX4 "BME28022"
class BME280Config : public SensorBase{
public:
bool prAct=true;
bool tmAct=true;
bool huAct=true;
tN2kTempSource tmSrc=tN2kTempSource::N2kts_InsideTemperature;
tN2kHumiditySource huSrc=tN2kHumiditySource::N2khs_InsideHumidity;
tN2kPressureSource prSrc=tN2kPressureSource::N2kps_Atmospheric;
String tmNam="Temperature";
String huNam="Humidity";
String prNam="Pressure";
float tmOff=0;
float prOff=0;
Adafruit_BME280 *device=nullptr;
uint32_t sensorId=-1;
BME280Config(GwApi * api, const String &prfx):SensorBase(api,prfx){
}
virtual bool isActive(){return prAct||huAct||tmAct;}
virtual bool initDevice(GwApi *api,TwoWire *wire){
GwLog *logger=api->getLogger();
device= new Adafruit_BME280();
if (! device->begin(addr,wire)){
LOG_DEBUG(GwLog::ERROR,"unable to initialize %s at %d",prefix.c_str(),addr);
delete device;
device=nullptr;
return false;
}
if (tmOff != 0){
device->setTemperatureCompensation(tmOff);
}
sensorId=device->sensorID();
LOG_DEBUG(GwLog::LOG, "initialized %s at %d, sensorId 0x%x", prefix.c_str(), addr, sensorId);
return (huAct && sensorId == 0x60) || tmAct || prAct;
}
virtual bool preinit(GwApi * api){
GwLog *logger=api->getLogger();
LOG_DEBUG(GwLog::LOG,"%s configured",prefix.c_str());
api->addCapability(prefix,"true");
addPressureXdr(api,*this);
addTempXdr(api,*this);
addHumidXdr(api,*this);
return isActive();
}
virtual void measure(GwApi *api, TwoWire *wire, int counterId)
{
if (!device)
return;
GwLog *logger = api->getLogger();
if (prAct)
{
float pressure = device->readPressure();
float computed = pressure + prOff;
LOG_DEBUG(GwLog::DEBUG, "%s measure %2.0fPa, computed %2.0fPa", prefix.c_str(), pressure, computed);
sendN2kPressure(api, *this, computed, counterId);
}
if (tmAct)
{
float temperature = device->readTemperature(); // offset is handled internally
temperature = CToKelvin(temperature);
LOG_DEBUG(GwLog::DEBUG, "%s measure temp=%2.1f", prefix.c_str(), temperature);
sendN2kTemperature(api, *this, temperature, counterId);
}
if (huAct && sensorId == 0x60)
{
float humidity = device->readHumidity();
LOG_DEBUG(GwLog::DEBUG, "%s read humid=%02.0f", prefix.c_str(), humidity);
sendN2kHumidity(api, *this, humidity, counterId);
}
}
#define CFG280(prefix) \
CFG_GET(prAct,prefix); \
CFG_GET(tmAct,prefix);\
CFG_GET(huAct,prefix);\
CFG_GET(tmSrc,prefix);\
CFG_GET(huSrc,prefix);\
CFG_GET(iid,prefix);\
CFG_GET(intv,prefix);\
CFG_GET(tmNam,prefix);\
CFG_GET(huNam,prefix);\
CFG_GET(prNam,prefix);\
CFG_GET(tmOff,prefix);\
CFG_GET(prOff,prefix);
virtual void readConfig(GwConfigHandler *cfg) override
{
if (prefix == PRFX1)
{
busId = 1;
addr = 0x76;
CFG280(BME28011);
ok=true;
}
if (prefix == PRFX2)
{
busId = 1;
addr = 0x77;
CFG280(BME28012);
ok=true;
}
if (prefix == PRFX3)
{
busId = 2;
addr = 0x76;
CFG280(BME28021);
ok=true;
}
if (prefix == PRFX4)
{
busId = 2;
addr = 0x77;
CFG280(BME28022);
}
intv *= 1000;
}
};
void registerBME280(GwApi *api,SensorList &sensors){
#if defined(GWBME280) || defined(GWBME28011)
{
BME280Config *cfg=new BME280Config(api,PRFX1);
sensors.add(api,cfg);
CHECK_IIC1();
#pragma message "GWBME28011 defined"
}
#endif
#if defined(GWBME28012)
{
BME280Config *cfg=new BME280Config(api,PRFX2);
sensors.add(api,cfg);
CHECK_IIC1();
#pragma message "GWBME28012 defined"
}
#endif
#if defined(GWBME28021)
{
BME280Config *cfg=new BME280Config(api,PRFX3);
sensors.add(api,cfg);
CHECK_IIC2();
#pragma message "GWBME28021 defined"
}
#endif
#if defined(GWBME28022)
{
BME280Config *cfg=new BME280Config(api,PRFX4);
sensors.add(api,cfg);
CHECK_IIC1();
#pragma message "GWBME28022 defined"
}
#endif
}
#else
void registerBME280(GwApi *api,SensorList &sensors){
}
#endif

5
lib/iictask/GwBME280.h Normal file
View File

@ -0,0 +1,5 @@
#ifndef _GWBME280_H
#define _GWBME280_H
#include "GwIicSensors.h"
void registerBME280(GwApi *api,SensorList &sensors);
#endif

134
lib/iictask/GwIicSensors.h Normal file
View File

@ -0,0 +1,134 @@
#ifndef _GWIICSENSSORS_H
#define _GWIICSENSSORS_H
#include "GwApi.h"
#include "N2kMessages.h"
#include "GwXdrTypeMappings.h"
#include "GwHardware.h"
#ifdef _GWIIC
#include <Wire.h>
#else
class TwoWire;
#endif
#define CFG_GET(name,prefix) \
cfg->getValue(name, GwConfigDefinitions::prefix ## name)
template <class CFG>
bool addPressureXdr(GwApi *api, CFG &cfg)
{
if (! cfg.prAct) return false;
if (cfg.prNam.isEmpty()){
api->getLogger()->logDebug(GwLog::LOG, "pressure active for %s, no xdr mapping", cfg.prefix.c_str());
return true;
}
api->getLogger()->logDebug(GwLog::LOG, "adding pressure xdr mapping for %s", cfg.prefix.c_str());
GwXDRMappingDef xdr;
xdr.category = GwXDRCategory::XDRPRESSURE;
xdr.direction = GwXDRMappingDef::M_FROM2K;
xdr.selector = (int)cfg.prSrc;
xdr.instanceId = cfg.iid;
xdr.instanceMode = GwXDRMappingDef::IS_SINGLE;
xdr.xdrName = cfg.prNam;
api->addXdrMapping(xdr);
return true;
}
template <class CFG>
bool addTempXdr(GwApi *api, CFG &cfg)
{
if (!cfg.tmAct) return false;
if (cfg.tmNam.isEmpty()){
api->getLogger()->logDebug(GwLog::LOG, "temperature active for %s, no xdr mapping", cfg.prefix.c_str());
return true;
}
api->getLogger()->logDebug(GwLog::LOG, "adding temperature xdr mapping for %s", cfg.prefix.c_str());
GwXDRMappingDef xdr;
xdr.category = GwXDRCategory::XDRTEMP;
xdr.direction = GwXDRMappingDef::M_FROM2K;
xdr.field = GWXDRFIELD_TEMPERATURE_ACTUALTEMPERATURE;
xdr.selector = (int)cfg.tmSrc;
xdr.instanceMode = GwXDRMappingDef::IS_SINGLE;
xdr.instanceId = cfg.iid;
xdr.xdrName = cfg.tmNam;
api->addXdrMapping(xdr);
return true;
}
template <class CFG>
bool addHumidXdr(GwApi *api, CFG &cfg)
{
if (! cfg.huAct) return false;
if (cfg.huNam.isEmpty()){
api->getLogger()->logDebug(GwLog::LOG, "humidity active for %s, no xdr mapping", cfg.prefix.c_str());
return true;
}
api->getLogger()->logDebug(GwLog::LOG, "adding humidity xdr mapping for %s", cfg.prefix.c_str());
GwXDRMappingDef xdr;
xdr.category = GwXDRCategory::XDRHUMIDITY;
xdr.direction = GwXDRMappingDef::M_FROM2K;
xdr.field = GWXDRFIELD_HUMIDITY_ACTUALHUMIDITY;
xdr.selector = (int)cfg.huSrc;
xdr.instanceMode = GwXDRMappingDef::IS_SINGLE;
xdr.instanceId = cfg.iid;
xdr.xdrName = cfg.huNam;
api->addXdrMapping(xdr);
return true;
}
template <class CFG>
void sendN2kHumidity(GwApi *api,CFG &cfg,double value, int counterId){
tN2kMsg msg;
SetN2kHumidity(msg,1,cfg.iid,cfg.huSrc,value);
api->sendN2kMessage(msg);
api->increment(counterId,cfg.prefix+String("hum"));
}
template <class CFG>
void sendN2kPressure(GwApi *api,CFG &cfg,double value, int counterId){
tN2kMsg msg;
SetN2kPressure(msg,1,cfg.iid,cfg.prSrc,value);
api->sendN2kMessage(msg);
api->increment(counterId,cfg.prefix+String("press"));
}
template <class CFG>
void sendN2kTemperature(GwApi *api,CFG &cfg,double value, int counterId){
tN2kMsg msg;
SetN2kTemperature(msg,1,cfg.iid,cfg.tmSrc,value);
api->sendN2kMessage(msg);
api->increment(counterId,cfg.prefix+String("temp"));
}
class SensorBase{
public:
int busId=0;
int iid=99; //N2K instanceId
int addr=-1;
String prefix;
long intv=0;
bool ok=false;
virtual void readConfig(GwConfigHandler *cfg)=0;
SensorBase(GwApi *api,const String &prfx):prefix(prfx){
}
virtual bool isActive(){return false;};
virtual bool initDevice(GwApi *api,TwoWire *wire){return false;};
virtual bool preinit(GwApi * api){return false;}
virtual void measure(GwApi * api,TwoWire *wire, int counterId){};
virtual ~SensorBase(){}
};
class SensorList : public std::vector<SensorBase*>{
public:
void add(GwApi *api, SensorBase *sensor){
sensor->readConfig(api->getConfig());
api->getLogger()->logDebug(GwLog::LOG,"configured sensor %s, status %d",sensor->prefix.c_str(),(int)sensor->ok);
push_back(sensor);
}
using std::vector<SensorBase*>::vector;
};
#define CHECK_IIC1() checkDef(GWIIC_SCL,GWIIC_SDA)
#define CHECK_IIC2() checkDef(GWIIC_SCL2,GWIIC_SDA2)
#endif

155
lib/iictask/GwIicTask.cpp Normal file
View File

@ -0,0 +1,155 @@
#include "GwIicTask.h"
#include "GwIicSensors.h"
#include "GwHardware.h"
#include "GwBME280.h"
#include "GwQMP6988.h"
#include "GwSHT3X.h"
#include <map>
#ifndef GWIIC_SDA
#define GWIIC_SDA -1
#endif
#ifndef GWIIC_SCL
#define GWIIC_SCL -1
#endif
#ifndef GWIIC_SDA2
#define GWIIC_SDA2 -1
#endif
#ifndef GWIIC_SCL2
#define GWIIC_SCL2 -1
#endif
#include "GwTimer.h"
#include "GwHardware.h"
void runIicTask(GwApi *api);
static SensorList sensors;
void initIicTask(GwApi *api){
GwLog *logger=api->getLogger();
#ifndef _GWIIC
return;
#else
bool addTask=false;
GwConfigHandler *config=api->getConfig();
registerSHT3X(api,sensors);
registerQMP6988(api,sensors);
registerBME280(api,sensors);
for (auto it=sensors.begin();it != sensors.end();it++){
if ((*it)->preinit(api)) addTask=true;
}
if (addTask){
api->addUserTask(runIicTask,"iicTask",3000);
}
#endif
}
#ifndef _GWIIC
void runIicTask(GwApi *api){
GwLog *logger=api->getLogger();
LOG_DEBUG(GwLog::LOG,"no iic defined, iic task stopped");
vTaskDelete(NULL);
return;
}
#else
void runIicTask(GwApi *api){
GwLog *logger=api->getLogger();
std::map<int,TwoWire *> buses;
LOG_DEBUG(GwLog::LOG,"iic task started");
for (auto it=sensors.begin();it != sensors.end();it++){
int busId=(*it)->busId;
auto bus=buses.find(busId);
if (bus == buses.end()){
switch (busId)
{
case 1:
{
if (GWIIC_SDA < 0 || GWIIC_SCL < 0)
{
LOG_DEBUG(GwLog::ERROR, "IIC 1 invalid config sda=%d,scl=%d",
(int)GWIIC_SDA, (int)GWIIC_SCL);
}
else
{
bool rt = Wire.begin(GWIIC_SDA, GWIIC_SCL);
if (!rt)
{
LOG_DEBUG(GwLog::ERROR, "unable to initialize IIC 1 at sad=%d,scl=%d",
(int)GWIIC_SDA, (int)GWIIC_SCL);
}
else
{
buses[busId] = &Wire;
LOG_DEBUG(GwLog::ERROR, "initialized IIC 1 at sda=%d,scl=%d",
(int)GWIIC_SDA, (int)GWIIC_SCL);
}
}
}
break;
case 2:
{
if (GWIIC_SDA2 < 0 || GWIIC_SCL2 < 0)
{
LOG_DEBUG(GwLog::ERROR, "IIC 2 invalid config sda=%d,scl=%d",
(int)GWIIC_SDA2, (int)GWIIC_SCL2);
}
else
{
bool rt = Wire1.begin(GWIIC_SDA2, GWIIC_SCL2);
if (!rt)
{
LOG_DEBUG(GwLog::ERROR, "unable to initialize IIC 2 at sda=%d,scl=%d",
(int)GWIIC_SDA2, (int)GWIIC_SCL2);
}
else
{
buses[busId] = &Wire1;
LOG_DEBUG(GwLog::LOG, "initialized IIC 2 at sda=%d,scl=%d",
(int)GWIIC_SDA2, (int)GWIIC_SCL2);
}
}
}
break;
default:
LOG_DEBUG(GwLog::ERROR, "invalid bus id %d at config %s", busId, (*it)->prefix.c_str());
break;
}
}
}
GwConfigHandler *config=api->getConfig();
bool runLoop=false;
GwIntervalRunner timers;
int counterId=api->addCounter("iicsensors");
for (auto it=sensors.begin();it != sensors.end();it++){
SensorBase *cfg=*it;
auto bus=buses.find(cfg->busId);
if (! cfg->isActive()) continue;
if (bus == buses.end()){
LOG_DEBUG(GwLog::ERROR,"No bus initialized for %s",cfg->prefix.c_str());
continue;
}
TwoWire *wire=bus->second;
bool rt=cfg->initDevice(api,wire);
if (rt){
runLoop=true;
timers.addAction(cfg->intv,[wire,api,cfg,counterId](){
cfg->measure(api,wire,counterId);
});
}
}
if (! runLoop){
LOG_DEBUG(GwLog::LOG,"nothing to do for IIC task, finish");
vTaskDelete(NULL);
return;
}
while(true){
delay(100);
timers.loop();
}
vTaskDelete(NULL);
}
#endif

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

@ -0,0 +1,6 @@
#ifndef _GWIICTASK_H
#define _GWIICTASK_H
#include "GwApi.h"
void initIicTask(GwApi *api);
DECLARE_INITFUNCTION(initIicTask);
#endif

117
lib/iictask/GwQMP6988.cpp Normal file
View File

@ -0,0 +1,117 @@
#include "GwQMP6988.h"
#ifdef _GWQMP6988
#define PRFX1 "QMP698811"
#define PRFX2 "QMP698812"
#define PRFX3 "QMP698821"
#define PRFX4 "QMP698822"
class QMP6988Config : public SensorBase{
public:
String prNam="Pressure";
bool prAct=true;
tN2kPressureSource prSrc=tN2kPressureSource::N2kps_Atmospheric;
float prOff=0;
QMP6988 *device=nullptr;
QMP6988Config(GwApi* api,const String &prefix):SensorBase(api,prefix){}
virtual bool isActive(){return prAct;};
virtual bool initDevice(GwApi *api,TwoWire *wire){
if (!isActive()) return false;
GwLog *logger=api->getLogger();
device=new QMP6988();
if (!device->init(addr,wire)){
LOG_DEBUG(GwLog::ERROR,"unable to initialize %s at address %d, intv %ld",prefix.c_str(),addr,intv);
delete device;
device=nullptr;
return false;
}
LOG_DEBUG(GwLog::LOG,"initialized %s at address %d, intv %ld",prefix.c_str(),addr,intv);
return true;
};
virtual bool preinit(GwApi * api){
GwLog *logger=api->getLogger();
LOG_DEBUG(GwLog::LOG,"QMP6988 configured");
api->addCapability(prefix,"true");
addPressureXdr(api,*this);
return isActive();
}
virtual void measure(GwApi * api,TwoWire *wire, int counterId){
GwLog *logger=api->getLogger();
float pressure=device->calcPressure();
float computed=pressure+prOff;
LOG_DEBUG(GwLog::DEBUG,"%s measure %2.0fPa, computed %2.0fPa",prefix.c_str(), pressure,computed);
sendN2kPressure(api,*this,computed,counterId);
}
#define CFG6988(prefix)\
CFG_GET(prNam,prefix); \
CFG_GET(iid,prefix); \
CFG_GET(prAct,prefix); \
CFG_GET(intv,prefix); \
CFG_GET(prOff,prefix);
virtual void readConfig(GwConfigHandler *cfg){
if (prefix == PRFX1){
busId=1;
addr=86;
CFG6988(QMP698811);
ok=true;
}
if (prefix == PRFX2){
busId=1;
addr=112;
CFG6988(QMP698812);
ok=true;
}
if (prefix == PRFX3){
busId=2;
addr=86;
CFG6988(QMP698821);
ok=true;
}
if (prefix == PRFX4){
busId=2;
addr=112;
CFG6988(QMP698822);
ok=true;
}
intv*=1000;
}
};
void registerQMP6988(GwApi *api,SensorList &sensors){
GwLog *logger=api->getLogger();
#if defined(GWQMP6988) || defined(GWQMP698811)
{
QMP6988Config *scfg=new QMP6988Config(api,PRFX1);
sensors.add(api,scfg);
CHECK_IIC1();
#pragma message "GWQMP698811 defined"
}
#endif
#if defined(GWQMP698812)
{
QMP6988Config *scfg=new QMP6988Config(api,PRFX2);
sensors.add(api,scfg);
CHECK_IIC1();
#pragma message "GWQMP698812 defined"
}
#endif
#if defined(GWQMP698821)
{
QMP6988Config *scfg=new QMP6988Config(api,PRFX3);
sensors.add(api,scfg);
CHECK_IIC2();
#pragma message "GWQMP698821 defined"
}
#endif
#if defined(GWQMP698822)
{
QMP6988Config *scfg=new QMP6988Config(api,PRFX4);
sensors.add(api,scfg);
CHECK_IIC2();
#pragma message "GWQMP698822 defined"
}
#endif
}
#else
void registerQMP6988(GwApi *api,SensorList &sensors){}
#endif

22
lib/iictask/GwQMP6988.h Normal file
View File

@ -0,0 +1,22 @@
#ifndef _GQQMP6988_H
#define _GQQMP6988_H
#include "GwIicSensors.h"
#ifdef _GWIIC
#if defined(GWQMP6988) || defined(GWQMP698811) || defined(GWQMP698812) || defined(GWQMP698821) || defined(GWQMP698822)
#define _GWQMP6988
#else
#undef _GWQMP6988
#endif
#else
#undef _GWQMP6988
#undef GWQMP6988
#undef GWQMP698811
#undef GWQMP698812
#undef GWQMP698821
#undef GWQMP698822
#endif
#ifdef _GWQMP6988
#include "QMP6988.h"
#endif
void registerQMP6988(GwApi *api,SensorList &sensors);
#endif

150
lib/iictask/GwSHT3X.cpp Normal file
View File

@ -0,0 +1,150 @@
#include "GwSHT3X.h"
#ifdef _GWSHT3X
#define PRFX1 "SHT3X11"
#define PRFX2 "SHT3X12"
#define PRFX3 "SHT3X21"
#define PRFX4 "SHT3X22"
class SHT3XConfig : public SensorBase{
public:
String tmNam;
String huNam;
bool tmAct=false;
bool huAct=false;
tN2kHumiditySource huSrc;
tN2kTempSource tmSrc;
SHT3X *device=nullptr;
SHT3XConfig(GwApi *api,const String &prefix):
SensorBase(api,prefix){}
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());
api->addCapability(prefix,"true");
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);
}
}
/**
* we do not dynamically compute the config names
* just to get compile time errors if something does not fit
* correctly
*/
#define CFG3X(prefix) \
CFG_GET(tmNam,prefix); \
CFG_GET(huNam,prefix); \
CFG_GET(iid,prefix); \
CFG_GET(tmAct,prefix); \
CFG_GET(huAct,prefix); \
CFG_GET(intv,prefix); \
CFG_GET(huSrc,prefix); \
CFG_GET(tmSrc,prefix);
virtual void readConfig(GwConfigHandler *cfg){
if (prefix == PRFX1){
busId=1;
addr=0x44;
CFG3X(SHT3X11);
ok=true;
}
if (prefix == PRFX2){
busId=1;
addr=0x45;
CFG3X(SHT3X12);
ok=true;
}
if (prefix == PRFX3){
busId=2;
addr=0x44;
CFG3X(SHT3X21);
ok=true;
}
if (prefix == PRFX4){
busId=2;
addr=0x45;
CFG3X(SHT3X22);
ok=true;
}
intv*=1000;
}
};
void registerSHT3X(GwApi *api,SensorList &sensors){
GwLog *logger=api->getLogger();
#if defined(GWSHT3X) || defined (GWSHT3X11)
{
SHT3XConfig *scfg=new SHT3XConfig(api,PRFX1);
sensors.add(api,scfg);
CHECK_IIC1();
#pragma message "GWSHT3X11 defined"
}
#endif
#if defined(GWSHT3X12)
{
SHT3XConfig *scfg=new SHT3XConfig(api,PRFX2);
sensors.add(api,scfg);
CHECK_IIC1();
#pragma message "GWSHT3X12 defined"
}
#endif
#if defined(GWSHT3X21)
{
SHT3XConfig *scfg=new SHT3XConfig(api,PRFX3);
sensors.add(api,scfg);
CHECK_IIC2();
#pragma message "GWSHT3X21 defined"
}
#endif
#if defined(GWSHT3X22)
{
SHT3XConfig *scfg=new SHT3XConfig(api,PRFX4);
sensors.add(api,scfg);
CHECK_IIC2();
#pragma message "GWSHT3X22 defined"
}
#endif
}
#else
void registerSHT3X(GwApi *api,SensorList &sensors){
}
#endif

22
lib/iictask/GwSHT3X.h Normal file
View File

@ -0,0 +1,22 @@
#ifndef _GWSHT3X_H
#define _GWSHT3X_H
#include "GwIicSensors.h"
#ifdef _GWIIC
#if defined(GWSHT3X) || defined(GWSHT3X11) || defined(GWSHT3X12) || defined(GWSHT3X21) || defined(GWSHT3X22)
#define _GWSHT3X
#else
#undef _GWSHT3X
#endif
#else
#undef _GWSHT3X
#undef GWSHT3X
#undef GWSHT3X11
#undef GWSHT3X12
#undef GWSHT3X21
#undef GWSHT3X22
#endif
#ifdef _GWSHT3X
#include "SHT3X.h"
#endif
void registerSHT3X(GwApi *api,SensorList &sensors);
#endif

394
lib/iictask/QMP6988.cpp Normal file
View File

@ -0,0 +1,394 @@
#include "GwQMP6988.h"
#ifdef _GWQMP6988
#include <math.h>
#include "stdint.h"
#include "stdio.h"
// DISABLE LOG
#define QMP6988_LOG(format...)
#define QMP6988_ERR(format...)
// ENABLE LOG
// #define QMP6988_LOG Serial.printf
// #define QMP6988_ERR Serial.printf
void QMP6988::delayMS(unsigned int ms) {
delay(ms);
}
uint8_t QMP6988::writeReg(uint8_t slave, uint8_t reg_add, uint8_t reg_dat) {
device_wire->beginTransmission(slave);
device_wire->write(reg_add);
device_wire->write(reg_dat);
device_wire->endTransmission();
return 1;
}
uint8_t QMP6988::readData(uint16_t slave, uint8_t reg_add, unsigned char* Read,
uint8_t num) {
device_wire->beginTransmission(slave);
device_wire->write(reg_add);
device_wire->endTransmission();
device_wire->requestFrom(slave, num);
for (int i = 0; i < num; i++) {
*(Read + i) = device_wire->read();
}
return 1;
}
uint8_t QMP6988::deviceCheck() {
uint8_t slave_addr_list[2] = {QMP6988_SLAVE_ADDRESS_L,
QMP6988_SLAVE_ADDRESS_H};
uint8_t ret = 0;
uint8_t i;
for (i = 0; i < 2; i++) {
slave_addr = slave_addr_list[i];
ret = readData(slave_addr, QMP6988_CHIP_ID_REG, &(qmp6988.chip_id), 1);
if (ret == 0) {
QMP6988_LOG("%s: read 0xD1 failed\r\n", __func__);
continue;
}
QMP6988_LOG("qmp6988 read chip id = 0x%x\r\n", qmp6988.chip_id);
if (qmp6988.chip_id == QMP6988_CHIP_ID) {
return 1;
}
}
return 0;
}
int QMP6988::getCalibrationData() {
int status = 0;
// BITFIELDS temp_COE;
uint8_t a_data_uint8_tr[QMP6988_CALIBRATION_DATA_LENGTH] = {0};
int len;
for (len = 0; len < QMP6988_CALIBRATION_DATA_LENGTH; len += 1) {
status = readData(slave_addr, QMP6988_CALIBRATION_DATA_START + len,
&a_data_uint8_tr[len], 1);
if (status == 0) {
QMP6988_LOG("qmp6988 read 0xA0 error!");
return status;
}
}
qmp6988.qmp6988_cali.COE_a0 =
(QMP6988_S32_t)(((a_data_uint8_tr[18] << SHIFT_LEFT_12_POSITION) |
(a_data_uint8_tr[19] << SHIFT_LEFT_4_POSITION) |
(a_data_uint8_tr[24] & 0x0f))
<< 12);
qmp6988.qmp6988_cali.COE_a0 = qmp6988.qmp6988_cali.COE_a0 >> 12;
qmp6988.qmp6988_cali.COE_a1 =
(QMP6988_S16_t)(((a_data_uint8_tr[20]) << SHIFT_LEFT_8_POSITION) |
a_data_uint8_tr[21]);
qmp6988.qmp6988_cali.COE_a2 =
(QMP6988_S16_t)(((a_data_uint8_tr[22]) << SHIFT_LEFT_8_POSITION) |
a_data_uint8_tr[23]);
qmp6988.qmp6988_cali.COE_b00 =
(QMP6988_S32_t)(((a_data_uint8_tr[0] << SHIFT_LEFT_12_POSITION) |
(a_data_uint8_tr[1] << SHIFT_LEFT_4_POSITION) |
((a_data_uint8_tr[24] & 0xf0) >>
SHIFT_RIGHT_4_POSITION))
<< 12);
qmp6988.qmp6988_cali.COE_b00 = qmp6988.qmp6988_cali.COE_b00 >> 12;
qmp6988.qmp6988_cali.COE_bt1 =
(QMP6988_S16_t)(((a_data_uint8_tr[2]) << SHIFT_LEFT_8_POSITION) |
a_data_uint8_tr[3]);
qmp6988.qmp6988_cali.COE_bt2 =
(QMP6988_S16_t)(((a_data_uint8_tr[4]) << SHIFT_LEFT_8_POSITION) |
a_data_uint8_tr[5]);
qmp6988.qmp6988_cali.COE_bp1 =
(QMP6988_S16_t)(((a_data_uint8_tr[6]) << SHIFT_LEFT_8_POSITION) |
a_data_uint8_tr[7]);
qmp6988.qmp6988_cali.COE_b11 =
(QMP6988_S16_t)(((a_data_uint8_tr[8]) << SHIFT_LEFT_8_POSITION) |
a_data_uint8_tr[9]);
qmp6988.qmp6988_cali.COE_bp2 =
(QMP6988_S16_t)(((a_data_uint8_tr[10]) << SHIFT_LEFT_8_POSITION) |
a_data_uint8_tr[11]);
qmp6988.qmp6988_cali.COE_b12 =
(QMP6988_S16_t)(((a_data_uint8_tr[12]) << SHIFT_LEFT_8_POSITION) |
a_data_uint8_tr[13]);
qmp6988.qmp6988_cali.COE_b21 =
(QMP6988_S16_t)(((a_data_uint8_tr[14]) << SHIFT_LEFT_8_POSITION) |
a_data_uint8_tr[15]);
qmp6988.qmp6988_cali.COE_bp3 =
(QMP6988_S16_t)(((a_data_uint8_tr[16]) << SHIFT_LEFT_8_POSITION) |
a_data_uint8_tr[17]);
QMP6988_LOG("<-----------calibration data-------------->\r\n");
QMP6988_LOG("COE_a0[%d] COE_a1[%d] COE_a2[%d] COE_b00[%d]\r\n",
qmp6988.qmp6988_cali.COE_a0, qmp6988.qmp6988_cali.COE_a1,
qmp6988.qmp6988_cali.COE_a2, qmp6988.qmp6988_cali.COE_b00);
QMP6988_LOG("COE_bt1[%d] COE_bt2[%d] COE_bp1[%d] COE_b11[%d]\r\n",
qmp6988.qmp6988_cali.COE_bt1, qmp6988.qmp6988_cali.COE_bt2,
qmp6988.qmp6988_cali.COE_bp1, qmp6988.qmp6988_cali.COE_b11);
QMP6988_LOG("COE_bp2[%d] COE_b12[%d] COE_b21[%d] COE_bp3[%d]\r\n",
qmp6988.qmp6988_cali.COE_bp2, qmp6988.qmp6988_cali.COE_b12,
qmp6988.qmp6988_cali.COE_b21, qmp6988.qmp6988_cali.COE_bp3);
QMP6988_LOG("<-----------calibration data-------------->\r\n");
qmp6988.ik.a0 = qmp6988.qmp6988_cali.COE_a0; // 20Q4
qmp6988.ik.b00 = qmp6988.qmp6988_cali.COE_b00; // 20Q4
qmp6988.ik.a1 = 3608L * (QMP6988_S32_t)qmp6988.qmp6988_cali.COE_a1 -
1731677965L; // 31Q23
qmp6988.ik.a2 = 16889L * (QMP6988_S32_t)qmp6988.qmp6988_cali.COE_a2 -
87619360L; // 30Q47
qmp6988.ik.bt1 = 2982L * (QMP6988_S64_t)qmp6988.qmp6988_cali.COE_bt1 +
107370906L; // 28Q15
qmp6988.ik.bt2 = 329854L * (QMP6988_S64_t)qmp6988.qmp6988_cali.COE_bt2 +
108083093L; // 34Q38
qmp6988.ik.bp1 = 19923L * (QMP6988_S64_t)qmp6988.qmp6988_cali.COE_bp1 +
1133836764L; // 31Q20
qmp6988.ik.b11 = 2406L * (QMP6988_S64_t)qmp6988.qmp6988_cali.COE_b11 +
118215883L; // 28Q34
qmp6988.ik.bp2 = 3079L * (QMP6988_S64_t)qmp6988.qmp6988_cali.COE_bp2 -
181579595L; // 29Q43
qmp6988.ik.b12 = 6846L * (QMP6988_S64_t)qmp6988.qmp6988_cali.COE_b12 +
85590281L; // 29Q53
qmp6988.ik.b21 = 13836L * (QMP6988_S64_t)qmp6988.qmp6988_cali.COE_b21 +
79333336L; // 29Q60
qmp6988.ik.bp3 = 2915L * (QMP6988_S64_t)qmp6988.qmp6988_cali.COE_bp3 +
157155561L; // 28Q65
QMP6988_LOG("<----------- int calibration data -------------->\r\n");
QMP6988_LOG("a0[%d] a1[%d] a2[%d] b00[%d]\r\n", qmp6988.ik.a0,
qmp6988.ik.a1, qmp6988.ik.a2, qmp6988.ik.b00);
QMP6988_LOG("bt1[%lld] bt2[%lld] bp1[%lld] b11[%lld]\r\n",
qmp6988.ik.bt1, qmp6988.ik.bt2, qmp6988.ik.bp1, qmp6988.ik.b11);
QMP6988_LOG("bp2[%lld] b12[%lld] b21[%lld] bp3[%lld]\r\n",
qmp6988.ik.bp2, qmp6988.ik.b12, qmp6988.ik.b21, qmp6988.ik.bp3);
QMP6988_LOG("<----------- int calibration data -------------->\r\n");
return 1;
}
QMP6988_S16_t QMP6988::convTx02e(qmp6988_ik_data_t* ik, QMP6988_S32_t dt) {
QMP6988_S16_t ret;
QMP6988_S64_t wk1, wk2;
// wk1: 60Q4 // bit size
wk1 = ((QMP6988_S64_t)ik->a1 * (QMP6988_S64_t)dt); // 31Q23+24-1=54 (54Q23)
wk2 = ((QMP6988_S64_t)ik->a2 * (QMP6988_S64_t)dt) >>
14; // 30Q47+24-1=53 (39Q33)
wk2 = (wk2 * (QMP6988_S64_t)dt) >> 10; // 39Q33+24-1=62 (52Q23)
wk2 = ((wk1 + wk2) / 32767) >> 19; // 54,52->55Q23 (20Q04)
ret = (QMP6988_S16_t)((ik->a0 + wk2) >> 4); // 21Q4 -> 17Q0
return ret;
}
QMP6988_S32_t QMP6988::getPressure02e(qmp6988_ik_data_t* ik, QMP6988_S32_t dp,
QMP6988_S16_t tx) {
QMP6988_S32_t ret;
QMP6988_S64_t wk1, wk2, wk3;
// wk1 = 48Q16 // bit size
wk1 =
((QMP6988_S64_t)ik->bt1 * (QMP6988_S64_t)tx); // 28Q15+16-1=43 (43Q15)
wk2 = ((QMP6988_S64_t)ik->bp1 * (QMP6988_S64_t)dp) >>
5; // 31Q20+24-1=54 (49Q15)
wk1 += wk2; // 43,49->50Q15
wk2 = ((QMP6988_S64_t)ik->bt2 * (QMP6988_S64_t)tx) >>
1; // 34Q38+16-1=49 (48Q37)
wk2 = (wk2 * (QMP6988_S64_t)tx) >> 8; // 48Q37+16-1=63 (55Q29)
wk3 = wk2; // 55Q29
wk2 = ((QMP6988_S64_t)ik->b11 * (QMP6988_S64_t)tx) >>
4; // 28Q34+16-1=43 (39Q30)
wk2 = (wk2 * (QMP6988_S64_t)dp) >> 1; // 39Q30+24-1=62 (61Q29)
wk3 += wk2; // 55,61->62Q29
wk2 = ((QMP6988_S64_t)ik->bp2 * (QMP6988_S64_t)dp) >>
13; // 29Q43+24-1=52 (39Q30)
wk2 = (wk2 * (QMP6988_S64_t)dp) >> 1; // 39Q30+24-1=62 (61Q29)
wk3 += wk2; // 62,61->63Q29
wk1 += wk3 >> 14; // Q29 >> 14 -> Q15
wk2 =
((QMP6988_S64_t)ik->b12 * (QMP6988_S64_t)tx); // 29Q53+16-1=45 (45Q53)
wk2 = (wk2 * (QMP6988_S64_t)tx) >> 22; // 45Q53+16-1=61 (39Q31)
wk2 = (wk2 * (QMP6988_S64_t)dp) >> 1; // 39Q31+24-1=62 (61Q30)
wk3 = wk2; // 61Q30
wk2 = ((QMP6988_S64_t)ik->b21 * (QMP6988_S64_t)tx) >>
6; // 29Q60+16-1=45 (39Q54)
wk2 = (wk2 * (QMP6988_S64_t)dp) >> 23; // 39Q54+24-1=62 (39Q31)
wk2 = (wk2 * (QMP6988_S64_t)dp) >> 1; // 39Q31+24-1=62 (61Q20)
wk3 += wk2; // 61,61->62Q30
wk2 = ((QMP6988_S64_t)ik->bp3 * (QMP6988_S64_t)dp) >>
12; // 28Q65+24-1=51 (39Q53)
wk2 = (wk2 * (QMP6988_S64_t)dp) >> 23; // 39Q53+24-1=62 (39Q30)
wk2 = (wk2 * (QMP6988_S64_t)dp); // 39Q30+24-1=62 (62Q30)
wk3 += wk2; // 62,62->63Q30
wk1 += wk3 >> 15; // Q30 >> 15 = Q15
wk1 /= 32767L;
wk1 >>= 11; // Q15 >> 7 = Q4
wk1 += ik->b00; // Q4 + 20Q4
// wk1 >>= 4; // 28Q4 -> 24Q0
ret = (QMP6988_S32_t)wk1;
return ret;
}
void QMP6988::softwareReset() {
uint8_t ret = 0;
ret = writeReg(slave_addr, QMP6988_RESET_REG, 0xe6);
if (ret == 0) {
QMP6988_LOG("softwareReset fail!!! \r\n");
}
delayMS(20);
ret = writeReg(slave_addr, QMP6988_RESET_REG, 0x00);
}
void QMP6988::setpPowermode(int power_mode) {
uint8_t data;
QMP6988_LOG("qmp_set_powermode %d \r\n", power_mode);
qmp6988.power_mode = power_mode;
readData(slave_addr, QMP6988_CTRLMEAS_REG, &data, 1);
data = data & 0xfc;
if (power_mode == QMP6988_SLEEP_MODE) {
data |= 0x00;
} else if (power_mode == QMP6988_FORCED_MODE) {
data |= 0x01;
} else if (power_mode == QMP6988_NORMAL_MODE) {
data |= 0x03;
}
writeReg(slave_addr, QMP6988_CTRLMEAS_REG, data);
QMP6988_LOG("qmp_set_powermode 0xf4=0x%x \r\n", data);
delayMS(20);
}
void QMP6988::setFilter(unsigned char filter) {
uint8_t data;
data = (filter & 0x03);
writeReg(slave_addr, QMP6988_CONFIG_REG, data);
delayMS(20);
}
void QMP6988::setOversamplingP(unsigned char oversampling_p) {
uint8_t data;
readData(slave_addr, QMP6988_CTRLMEAS_REG, &data, 1);
data &= 0xe3;
data |= (oversampling_p << 2);
writeReg(slave_addr, QMP6988_CTRLMEAS_REG, data);
delayMS(20);
}
void QMP6988::setOversamplingT(unsigned char oversampling_t) {
uint8_t data;
readData(slave_addr, QMP6988_CTRLMEAS_REG, &data, 1);
data &= 0x1f;
data |= (oversampling_t << 5);
writeReg(slave_addr, QMP6988_CTRLMEAS_REG, data);
delayMS(20);
}
float QMP6988::calcAltitude(float pressure, float temp) {
float altitude;
altitude =
(pow((101325 / pressure), 1 / 5.257) - 1) * (temp + 273.15) / 0.0065;
QMP6988_LOG("altitude = %f\r\n", altitude);
return altitude;
}
float QMP6988::calcPressure() {
uint8_t err = 0;
QMP6988_U32_t P_read, T_read;
QMP6988_S32_t P_raw, T_raw;
uint8_t a_data_uint8_tr[6] = {0};
QMP6988_S32_t T_int, P_int;
// press
err = readData(slave_addr, QMP6988_PRESSURE_MSB_REG, a_data_uint8_tr, 6);
if (err == 0) {
QMP6988_LOG("qmp6988 read press raw error! \r\n");
return 0.0f;
}
P_read = (QMP6988_U32_t)((((QMP6988_U32_t)(a_data_uint8_tr[0]))
<< SHIFT_LEFT_16_POSITION) |
(((QMP6988_U16_t)(a_data_uint8_tr[1]))
<< SHIFT_LEFT_8_POSITION) |
(a_data_uint8_tr[2]));
P_raw = (QMP6988_S32_t)(P_read - SUBTRACTOR);
T_read = (QMP6988_U32_t)((((QMP6988_U32_t)(a_data_uint8_tr[3]))
<< SHIFT_LEFT_16_POSITION) |
(((QMP6988_U16_t)(a_data_uint8_tr[4]))
<< SHIFT_LEFT_8_POSITION) |
(a_data_uint8_tr[5]));
T_raw = (QMP6988_S32_t)(T_read - SUBTRACTOR);
T_int = convTx02e(&(qmp6988.ik), T_raw);
P_int = getPressure02e(&(qmp6988.ik), P_raw, T_int);
qmp6988.temperature = (float)T_int / 256.0f;
qmp6988.pressure = (float)P_int / 16.0f;
return qmp6988.pressure;
}
float QMP6988::calcTemperature() {
uint8_t err = 0;
QMP6988_U32_t P_read, T_read;
QMP6988_S32_t P_raw, T_raw;
uint8_t a_data_uint8_tr[6] = {0};
QMP6988_S32_t T_int, P_int;
// press
err = readData(slave_addr, QMP6988_PRESSURE_MSB_REG, a_data_uint8_tr, 6);
if (err == 0) {
QMP6988_LOG("qmp6988 read press raw error! \r\n");
return 0.0f;
}
P_read = (QMP6988_U32_t)((((QMP6988_U32_t)(a_data_uint8_tr[0]))
<< SHIFT_LEFT_16_POSITION) |
(((QMP6988_U16_t)(a_data_uint8_tr[1]))
<< SHIFT_LEFT_8_POSITION) |
(a_data_uint8_tr[2]));
P_raw = (QMP6988_S32_t)(P_read - SUBTRACTOR);
// temp
err = readData(slave_addr, QMP6988_TEMPERATURE_MSB_REG, a_data_uint8_tr, 3);
if (err == 0) {
QMP6988_LOG("qmp6988 read temp raw error! \n");
}
T_read = (QMP6988_U32_t)((((QMP6988_U32_t)(a_data_uint8_tr[3]))
<< SHIFT_LEFT_16_POSITION) |
(((QMP6988_U16_t)(a_data_uint8_tr[4]))
<< SHIFT_LEFT_8_POSITION) |
(a_data_uint8_tr[5]));
T_raw = (QMP6988_S32_t)(T_read - SUBTRACTOR);
T_int = convTx02e(&(qmp6988.ik), T_raw);
P_int = getPressure02e(&(qmp6988.ik), P_raw, T_int);
qmp6988.temperature = (float)T_int / 256.0f;
qmp6988.pressure = (float)P_int / 16.0f;
return qmp6988.temperature;
}
uint8_t QMP6988::init(uint8_t slave_addr_in, TwoWire* wire_in) {
device_wire = wire_in;
uint8_t ret;
slave_addr = slave_addr_in;
ret = deviceCheck();
if (ret == 0) {
return 0;
}
softwareReset();
getCalibrationData();
setpPowermode(QMP6988_NORMAL_MODE);
setFilter(QMP6988_FILTERCOEFF_4);
setOversamplingP(QMP6988_OVERSAMPLING_8X);
setOversamplingT(QMP6988_OVERSAMPLING_1X);
return 1;
}
#endif

152
lib/iictask/QMP6988.h Normal file
View File

@ -0,0 +1,152 @@
#ifndef __QMP6988_H
#define __QMP6988_H
#include "Arduino.h"
#include "Wire.h"
#define QMP6988_SLAVE_ADDRESS_L (0x70)
#define QMP6988_SLAVE_ADDRESS_H (0x56)
#define QMP6988_U16_t unsigned short
#define QMP6988_S16_t short
#define QMP6988_U32_t unsigned int
#define QMP6988_S32_t int
#define QMP6988_U64_t unsigned long long
#define QMP6988_S64_t long long
#define QMP6988_CHIP_ID 0x5C
#define QMP6988_CHIP_ID_REG 0xD1
#define QMP6988_RESET_REG 0xE0 /* Device reset register */
#define QMP6988_DEVICE_STAT_REG 0xF3 /* Device state register */
#define QMP6988_CTRLMEAS_REG 0xF4 /* Measurement Condition Control Register */
/* data */
#define QMP6988_PRESSURE_MSB_REG 0xF7 /* Pressure MSB Register */
#define QMP6988_TEMPERATURE_MSB_REG 0xFA /* Temperature MSB Reg */
/* compensation calculation */
#define QMP6988_CALIBRATION_DATA_START \
0xA0 /* QMP6988 compensation coefficients */
#define QMP6988_CALIBRATION_DATA_LENGTH 25
#define SHIFT_RIGHT_4_POSITION 4
#define SHIFT_LEFT_2_POSITION 2
#define SHIFT_LEFT_4_POSITION 4
#define SHIFT_LEFT_5_POSITION 5
#define SHIFT_LEFT_8_POSITION 8
#define SHIFT_LEFT_12_POSITION 12
#define SHIFT_LEFT_16_POSITION 16
/* power mode */
#define QMP6988_SLEEP_MODE 0x00
#define QMP6988_FORCED_MODE 0x01
#define QMP6988_NORMAL_MODE 0x03
#define QMP6988_CTRLMEAS_REG_MODE__POS 0
#define QMP6988_CTRLMEAS_REG_MODE__MSK 0x03
#define QMP6988_CTRLMEAS_REG_MODE__LEN 2
/* oversampling */
#define QMP6988_OVERSAMPLING_SKIPPED 0x00
#define QMP6988_OVERSAMPLING_1X 0x01
#define QMP6988_OVERSAMPLING_2X 0x02
#define QMP6988_OVERSAMPLING_4X 0x03
#define QMP6988_OVERSAMPLING_8X 0x04
#define QMP6988_OVERSAMPLING_16X 0x05
#define QMP6988_OVERSAMPLING_32X 0x06
#define QMP6988_OVERSAMPLING_64X 0x07
#define QMP6988_CTRLMEAS_REG_OSRST__POS 5
#define QMP6988_CTRLMEAS_REG_OSRST__MSK 0xE0
#define QMP6988_CTRLMEAS_REG_OSRST__LEN 3
#define QMP6988_CTRLMEAS_REG_OSRSP__POS 2
#define QMP6988_CTRLMEAS_REG_OSRSP__MSK 0x1C
#define QMP6988_CTRLMEAS_REG_OSRSP__LEN 3
/* filter */
#define QMP6988_FILTERCOEFF_OFF 0x00
#define QMP6988_FILTERCOEFF_2 0x01
#define QMP6988_FILTERCOEFF_4 0x02
#define QMP6988_FILTERCOEFF_8 0x03
#define QMP6988_FILTERCOEFF_16 0x04
#define QMP6988_FILTERCOEFF_32 0x05
#define QMP6988_CONFIG_REG 0xF1 /*IIR filter co-efficient setting Register*/
#define QMP6988_CONFIG_REG_FILTER__POS 0
#define QMP6988_CONFIG_REG_FILTER__MSK 0x07
#define QMP6988_CONFIG_REG_FILTER__LEN 3
#define SUBTRACTOR 8388608
typedef struct _qmp6988_cali_data {
QMP6988_S32_t COE_a0;
QMP6988_S16_t COE_a1;
QMP6988_S16_t COE_a2;
QMP6988_S32_t COE_b00;
QMP6988_S16_t COE_bt1;
QMP6988_S16_t COE_bt2;
QMP6988_S16_t COE_bp1;
QMP6988_S16_t COE_b11;
QMP6988_S16_t COE_bp2;
QMP6988_S16_t COE_b12;
QMP6988_S16_t COE_b21;
QMP6988_S16_t COE_bp3;
} qmp6988_cali_data_t;
typedef struct _qmp6988_fk_data {
float a0, b00;
float a1, a2, bt1, bt2, bp1, b11, bp2, b12, b21, bp3;
} qmp6988_fk_data_t;
typedef struct _qmp6988_ik_data {
QMP6988_S32_t a0, b00;
QMP6988_S32_t a1, a2;
QMP6988_S64_t bt1, bt2, bp1, b11, bp2, b12, b21, bp3;
} qmp6988_ik_data_t;
typedef struct _qmp6988_data {
uint8_t slave;
uint8_t chip_id;
uint8_t power_mode;
float temperature;
float pressure;
float altitude;
qmp6988_cali_data_t qmp6988_cali;
qmp6988_ik_data_t ik;
} qmp6988_data_t;
class QMP6988 {
private:
qmp6988_data_t qmp6988;
uint8_t slave_addr;
TwoWire* device_wire;
void delayMS(unsigned int ms);
// read calibration data from otp
int getCalibrationData();
QMP6988_S32_t getPressure02e(qmp6988_ik_data_t* ik, QMP6988_S32_t dp,
QMP6988_S16_t tx);
QMP6988_S16_t convTx02e(qmp6988_ik_data_t* ik, QMP6988_S32_t dt);
void softwareReset();
public:
uint8_t init(uint8_t slave_addr = 0x56, TwoWire* wire_in = &Wire);
uint8_t deviceCheck();
float calcAltitude(float pressure, float temp);
float calcPressure();
float calcTemperature();
void setpPowermode(int power_mode);
void setFilter(unsigned char filter);
void setOversamplingP(unsigned char oversampling_p);
void setOversamplingT(unsigned char oversampling_t);
uint8_t writeReg(uint8_t slave, uint8_t reg_add, uint8_t reg_dat);
uint8_t readData(uint16_t slave, uint8_t reg_add, unsigned char* Read,
uint8_t num);
};
#endif

47
lib/iictask/SHT3X.cpp Normal file
View File

@ -0,0 +1,47 @@
#include "GwSHT3X.h"
#ifdef _GWSHT3X
bool SHT3X::init(uint8_t slave_addr_in, TwoWire* wire_in)
{
_wire = wire_in;
_address=slave_addr_in;
return true;
}
byte SHT3X::get()
{
unsigned int data[6];
// Start I2C Transmission
_wire->beginTransmission(_address);
// Send measurement command
_wire->write(0x2C);
_wire->write(0x06);
// Stop I2C transmission
if (_wire->endTransmission()!=0)
return 1;
delay(200);
// Request 6 bytes of data
_wire->requestFrom(_address, (uint8_t) 6);
// Read 6 bytes of data
// cTemp msb, cTemp lsb, cTemp crc, humidity msb, humidity lsb, humidity crc
for (int i=0;i<6;i++) {
data[i]=_wire->read();
};
delay(50);
if (_wire->available()!=0)
return 2;
// Convert the data
cTemp = ((((data[0] * 256.0) + data[1]) * 175) / 65535.0) - 45;
fTemp = (cTemp * 1.8) + 32;
humidity = ((((data[3] * 256.0) + data[4]) * 100) / 65535.0);
return 0;
}
#endif

26
lib/iictask/SHT3X.h Normal file
View File

@ -0,0 +1,26 @@
#ifndef __SHT3X_H
#define __HT3X_H
//taken from https://github.com/m5stack/M5Unit-ENV/tree/0.0.8/src
#if ARDUINO >= 100
#include "Arduino.h"
#else
#include "WProgram.h"
#endif
#include "Wire.h"
class SHT3X{
public:
bool init(uint8_t slave_addr_in=0x44, TwoWire* wire_in = &Wire);
byte get(void);
float cTemp=0;
float fTemp=0;
float humidity=0;
private:
uint8_t _address;
TwoWire* _wire;
};
#endif

534
lib/iictask/config.json Normal file
View File

@ -0,0 +1,534 @@
[
{
"type": "array",
"name": "SHT3X",
"replace": [
{
"b": "1",
"i": "11",
"n": "99"
},
{
"b": "1",
"i": "12",
"n": "98"
},
{
"b": "2",
"i": "21",
"n": "109"
},
{
"b": "2",
"i": "22",
"n": "108"
}
],
"children": [
{
"name": "SHT3X$itmAct",
"label": "SHT3X$i Temp",
"type": "boolean",
"default": "true",
"description": "Enable the $i. I2C SHT3x temp sensor (bus $b)",
"category": "iicsensors$b",
"capabilities": {
"SHT3X$i": "true"
}
},
{
"name": "SHT3X$itmSrc",
"label": "SHT3X$i Temp Type",
"type": "list",
"default": "2",
"description": "the NMEA2000 source type for the temperature",
"list": [
{
"l": "SeaTemperature",
"v": "0"
},
{
"l": "OutsideTemperature",
"v": "1"
},
{
"l": "InsideTemperature",
"v": "2"
},
{
"l": "EngineRoomTemperature",
"v": "3"
},
{
"l": "MainCabinTemperature",
"v": "4"
},
{
"l": "LiveWellTemperature",
"v": "5"
},
{
"l": "BaitWellTemperature",
"v": "6"
},
{
"l": "RefridgerationTemperature",
"v": "7"
},
{
"l": "HeatingSystemTemperature",
"v": "8"
},
{
"l": "DewPointTemperature",
"v": "9"
},
{
"l": "ApparentWindChillTemperature",
"v": "10"
},
{
"l": "TheoreticalWindChillTemperature",
"v": "11"
},
{
"l": "HeatIndexTemperature",
"v": "12"
},
{
"l": "FreezerTemperature",
"v": "13"
},
{
"l": "ExhaustGasTemperature",
"v": "14"
},
{
"l": "ShaftSealTemperature",
"v": "15"
}
],
"category": "iicsensors$b",
"capabilities": {
"SHT3X$i": "true"
}
},
{
"name": "SHT3X$ihuAct",
"label": "SHT3X$i Humidity",
"type": "boolean",
"default": "true",
"description": "Enable the $i. I2C SHT3x humidity sensor (bus $b)",
"category": "iicsensors$b",
"capabilities": {
"SHT3X$i": "true"
}
},
{
"name": "SHT3X$ihuSrc",
"label": "SHT3X$i Humid Type",
"list": [
{
"l": "OutsideHumidity",
"v": "1"
},
{
"l": "Undef",
"v": "0xff"
}
],
"category": "iicsensors$b",
"capabilities": {
"SHT3X": "true"
}
},
{
"name": "SHT3X$iiid",
"label": "SHT3X$i N2K iid",
"type": "number",
"default": "$n",
"description": "the N2K instance id for the $i. SHT3X Temperature and Humidity ",
"category": "iicsensors$b",
"min": 0,
"max": 253,
"check": "checkMinMax",
"capabilities": {
"SHT3X$i": "true"
}
},
{
"name": "SHT3X$iintv",
"label": "SHT3X$i Interval",
"type": "number",
"default": 2,
"description": "Interval(s) to query SHT3X Temperature and Humidity (1...300)",
"category": "iicsensors$b",
"min": 1,
"max": 300,
"check": "checkMinMax",
"capabilities": {
"SHT3X$i": "true"
}
},
{
"name": "SHT3X$itmNam",
"label": "SHT3X$i Temp XDR",
"type": "String",
"default": "Temp$i",
"description": "set the XDR transducer name for the $i. SHT3X Temperature, leave empty to disable NMEA0183 XDR ",
"category": "iicsensors$b",
"capabilities": {
"SHT3X$i": "true"
}
},
{
"name": "SHT3X$ihuNam",
"label": "SHT3X$i Humid XDR",
"type": "String",
"default": "Humidity$i",
"description": "set the XDR transducer name for the $i. SHT3X Humidity, leave empty to disable NMEA0183 XDR",
"category": "iicsensors$b",
"capabilities": {
"SHT3X$i": "true"
}
}
]
},
{
"type": "array",
"name": "QMP6988",
"replace": [
{
"b": "1",
"i": "11",
"n": "97"
},
{
"b": "1",
"i": "12",
"n": "96"
},
{
"b": "2",
"i": "21",
"n": "107"
},
{
"b": "2",
"i": "22",
"n": "106"
}
],
"children": [
{
"name": "QMP6988$iprAct",
"label": "QMP6988-$i pressure",
"description": "activate the $i. QMP6988 pressure measurement (bus $b)",
"type": "boolean",
"default": "true",
"category": "iicsensors$b",
"capabilities": {
"QMP6988$i": "true"
}
},
{
"name": "QMP6988$iiid",
"label": "QMP6988-$i N2K iid",
"type": "number",
"default": "$n",
"description": "the N2K instance id for the $i. QMP6988 pressure",
"category": "iicsensors$b",
"min": 0,
"max": 253,
"check": "checkMinMax",
"capabilities": {
"QMP6988$i": "true"
}
},
{
"name": "QMP6988$iintv",
"label": "QMP6988-$i Interval",
"type": "number",
"default": 2,
"description": "Interval(s) to query the $i. QMP6988 Pressure (1...300)",
"category": "iicsensors$b",
"min": 1,
"max": 300,
"check": "checkMinMax",
"capabilities": {
"QMP6988$i": "true"
}
},
{
"name": "QMP6988$iprNam",
"label": "QMP6988-$i Pressure XDR",
"type": "String",
"default": "Pressure$i",
"description": "set the XDR transducer name for the $i. QMP6988 Pressure, leave empty to disable NMEA0183 XDR",
"category": "iicsensors$b",
"capabilities": {
"QMP6988$i": "true"
}
},
{
"name": "QMP6988$iprOff",
"label": "QMP6988-$i Pressure Offset",
"type": "number",
"description": "offset (in pa) to be added to the $i. QMP6988 pressure measurements",
"default": "0",
"category": "iicsensors$b",
"capabilities": {
"QMP6988$i": "true"
}
}
]
},
{
"type": "array",
"name": "BME280",
"replace": [
{
"b": "1",
"i": "11",
"n": "95"
},
{
"b": "1",
"i": "12",
"n": "94"
},
{
"b": "2",
"i": "21",
"n": "105"
},
{
"b": "2",
"i": "22",
"n": "104"
}
],
"children": [
{
"name": "BME280$itmAct",
"label": "BME280-$i Temp",
"type": "boolean",
"default": "true",
"description": "Enable the $i. I2C BME280 temp sensor (bus $b)",
"category": "iicsensors$b",
"capabilities": {
"BME280$i": "true"
}
},
{
"name": "BME280$itmSrc",
"label": "BME280-$i Temp Type",
"type": "list",
"default": "2",
"description": "the NMEA2000 source type for the temperature",
"list": [
{
"l": "SeaTemperature",
"v": "0"
},
{
"l": "OutsideTemperature",
"v": "1"
},
{
"l": "InsideTemperature",
"v": "2"
},
{
"l": "EngineRoomTemperature",
"v": "3"
},
{
"l": "MainCabinTemperature",
"v": "4"
},
{
"l": "LiveWellTemperature",
"v": "5"
},
{
"l": "BaitWellTemperature",
"v": "6"
},
{
"l": "RefridgerationTemperature",
"v": "7"
},
{
"l": "HeatingSystemTemperature",
"v": "8"
},
{
"l": "DewPointTemperature",
"v": "9"
},
{
"l": "ApparentWindChillTemperature",
"v": "10"
},
{
"l": "TheoreticalWindChillTemperature",
"v": "11"
},
{
"l": "HeatIndexTemperature",
"v": "12"
},
{
"l": "FreezerTemperature",
"v": "13"
},
{
"l": "ExhaustGasTemperature",
"v": "14"
},
{
"l": "ShaftSealTemperature",
"v": "15"
}
],
"category": "iicsensors$b",
"capabilities": {
"BME280$i": "true"
}
},
{
"name": "BME280$itmOff",
"label": "BME280-$i Temperature Offset",
"type": "number",
"description": "offset (in °) to be added to the BME280 temperature measurements",
"default": "0",
"category": "iicsensors$b",
"capabilities": {
"BME280$i": "true"
}
},
{
"name": "BME280$ihuAct",
"label": "BME280-$i Humidity",
"type": "boolean",
"default": "true",
"description": "Enable the $i. I2C BME280 humidity sensor",
"category": "iicsensors$b",
"capabilities": {
"BME280$i": "true"
}
},
{
"name": "BME280$ihuSrc",
"label": "BME280-$i Humid Type",
"type": "list",
"description": "the NMEA2000 source type for the humidity",
"default": "0",
"list": [
{
"l": "InsideHumidity",
"v": "0"
},
{
"l": "OutsideHumidity",
"v": "1"
},
{
"l": "Undef",
"v": "0xff"
}
],
"category": "iicsensors$b",
"capabilities": {
"BME280$i": "true"
}
},
{
"name": "BME280$iprAct",
"label": "BME280-$i Pressure",
"type": "boolean",
"default": "true",
"description": "Enable the $i. I2C BME280 pressure sensor (bus $b)",
"category": "iicsensors$b",
"capabilities": {
"BME280$i": "true"
}
},
{
"name": "BME280$iprOff",
"label": "BME280 Pressure Offset",
"type": "number",
"description": "offset (in pa) to be added to the BME280 pressure measurements",
"default": "0",
"category": "iicsensors$b",
"capabilities": {
"BME280$i": "true"
}
},
{
"name": "BME280$iiid",
"label": "BME280-$i N2K iid",
"type": "number",
"default": "$n",
"description": "the N2K instance id for the BME280 Temperature and Humidity ",
"category": "iicsensors$b",
"min": 0,
"max": 253,
"check": "checkMinMax",
"capabilities": {
"BME280$i": "true"
}
},
{
"name": "BME280$iintv",
"label": "BME280-$i Interval",
"type": "number",
"default": 2,
"description": "Interval(s) to query BME280 Temperature and Humidity (1...300)",
"category": "iicsensors$b",
"min": 1,
"max": 300,
"check": "checkMinMax",
"capabilities": {
"BME280$i": "true"
}
},
{
"name": "BME280$itmNam",
"label": "BME280-$i Temp XDR",
"type": "String",
"default": "BTemp$i",
"description": "set the XDR transducer name for the BME280 Temperature, leave empty to disable NMEA0183 XDR ",
"category": "iicsensors$b",
"capabilities": {
"BME280$i": "true"
}
},
{
"name": "BME280$ihuNam",
"label": "BME280-$i Humid XDR",
"type": "String",
"default": "BHumidity$i",
"description": "set the XDR transducer name for the BME280 Humidity, leave empty to disable NMEA0183 XDR",
"category": "iicsensors$b",
"capabilities": {
"BME280$i": "true"
}
},
{
"name": "BME280$iprNam",
"label": "BME280-$i Pressure XDR",
"type": "String",
"default": "BPressure$i",
"description": "set the XDR transducer name for the BME280 Pressure, leave empty to disable NMEA0183 XDR",
"category": "iicsensors$b",
"capabilities": {
"BME280$i": "true"
}
}
]
}
]

View File

@ -0,0 +1,37 @@
[platformio]
#basically for testing purposes
[env:m5stack-atom-env3]
extends = sensors
board = m5stack-atom
lib_deps =
${env.lib_deps}
${sensors.lib_deps}
build_flags=
-D M5_ENV3
-D M5_CAN_KIT
${env.build_flags}
[env:m5stack-atom-bme280]
extends = sensors
board = m5stack-atom
lib_deps =
${env.lib_deps}
${sensors.lib_deps}
build_flags=
-D GWBME280=-1
-D M5_GROOVEIIC
-D M5_CAN_KIT
${env.build_flags}
[env:m5stack-atom-bme2802]
extends = sensors
board = m5stack-atom
lib_deps =
${env.lib_deps}
${sensors.lib_deps}
build_flags=
-D GWBME280
-D GWBME2802
-D M5_GROOVEIIC
-D M5_CAN_KIT
${env.build_flags}

View File

@ -1,61 +0,0 @@
#include "GwLeds.h"
#include "GwHardware.h"
#include "GwApi.h"
#include "FastLED.h"
static GwLedMode mode=LED_OFF;
void setLedMode(GwLedMode newMode){
//we consider the mode to an atomic item...
mode=newMode;
}
static CRGB::HTMLColorCode colorFromMode(GwLedMode cmode){
switch(cmode){
case LED_BLUE:
return CRGB::Blue;
case LED_GREEN:
return CRGB::Green;
case LED_RED:
return CRGB::Red;
case LED_WHITE:
return CRGB::White;
default:
return CRGB::Black;
}
}
void handleLeds(void *param){
GwApi *api=(GwApi*)param;
GwLog *logger=api->getLogger();
#ifndef GWLED_FASTLED
LOG_DEBUG(GwLog::LOG,"currently only fastled handling");
delay(50);
vTaskDelete(NULL);
return;
#else
CRGB leds[1];
#ifdef GWLED_SCHEMA
FastLED.addLeds<GWLED_TYPE,GWLED_PIN,GWLED_SCHEMA>(leds,1);
#else
FastLED.addLeds<GWLED_TYPE,GWLED_PIN>(leds,1);
#endif
#ifdef GWLED_BRIGHTNESS
uint8_t brightness=GWLED_BRIGHTNESS;
#else
uint8_t brightness=128; //50%
#endif
GwLedMode currentMode=mode;
leds[0]=colorFromMode(currentMode);
FastLED.setBrightness(brightness);
FastLED.show();
while(true){
delay(50);
GwLedMode newMode=mode;
if (newMode != currentMode){
leds[0]=colorFromMode(newMode);
FastLED.show();
currentMode=newMode;
}
}
vTaskDelete(NULL);
#endif
}

View File

@ -1,13 +0,0 @@
#ifndef _GWLEDS_H
#define _GWLEDS_H
//task function
void handleLeds(void *param);
typedef enum {
LED_OFF,
LED_GREEN,
LED_BLUE,
LED_RED,
LED_WHITE
} GwLedMode;
void setLedMode(GwLedMode mode);
#endif

81
lib/ledtask/GwLedTask.cpp Normal file
View File

@ -0,0 +1,81 @@
#include "GwLedTask.h"
#include "GwHardware.h"
#include "GwApi.h"
#include "FastLED.h"
typedef enum {
LED_OFF,
LED_GREEN,
LED_BLUE,
LED_RED,
LED_WHITE
} GwLedMode;
static CRGB::HTMLColorCode colorFromMode(GwLedMode cmode){
switch(cmode){
case LED_BLUE:
return CRGB::Blue;
case LED_GREEN:
return CRGB::Green;
case LED_RED:
return CRGB::Red;
case LED_WHITE:
return CRGB::White;
default:
return CRGB::Black;
}
}
void handleLeds(GwApi *api){
GwLog *logger=api->getLogger();
#ifndef GWLED_FASTLED
LOG_DEBUG(GwLog::LOG,"currently only fastled handling");
delay(50);
vTaskDelete(NULL);
return;
#else
CRGB leds[1];
#ifdef GWLED_SCHEMA
FastLED.addLeds<GWLED_TYPE,GWLED_PIN,(EOrder)GWLED_SCHEMA>(leds,1);
#else
FastLED.addLeds<GWLED_TYPE,GWLED_PIN>(leds,1);
#endif
uint8_t brightness=api->getConfig()->getInt(GwConfigDefinitions::ledBrightness,128);
GwLedMode currentMode=LED_GREEN;
leds[0]=colorFromMode(currentMode);
FastLED.setBrightness(brightness);
FastLED.show();
LOG_DEBUG(GwLog::LOG,"led task started with mode %d",(int)currentMode);
int apiResult=0;
while (true)
{
delay(50);
GwLedMode newMode = currentMode;
IButtonTask buttonState = api->taskInterfaces()->get<IButtonTask>(apiResult);
if (apiResult >= 0)
{
switch (buttonState.state)
{
case IButtonTask::PRESSED_5:
newMode = LED_BLUE;
break;
case IButtonTask::PRESSED_10:
newMode = LED_RED;
break;
default:
newMode = LED_GREEN;
break;
}
}
else
{
newMode = LED_WHITE;
}
if (newMode != currentMode)
{
leds[0] = colorFromMode(newMode);
FastLED.show();
currentMode = newMode;
}
}
vTaskDelete(NULL);
#endif
}

8
lib/ledtask/GwLedTask.h Normal file
View File

@ -0,0 +1,8 @@
#ifndef _GWLEDS_H
#define _GWLEDS_H
#include "GwApi.h"
//task function
void handleLeds(GwApi *param);
DECLARE_USERTASK(handleLeds);
#endif

View File

@ -1,22 +1,41 @@
#include "GwLog.h"
#include "GwHardware.h"
class DefaultLogWriter: public GwLogWriter{
public:
virtual ~DefaultLogWriter(){};
virtual void write(const char *data){
Serial.print(data);
}
};
GwLog::GwLog(int level, GwLogWriter *writer){
logLevel=level;
if (writer == NULL) writer=new DefaultLogWriter();
this->writer=writer;
if (!writer){
iniBuffer=new char[INIBUFFERSIZE];
iniBuffer[0]=0;
}
locker = xSemaphoreCreateMutex();
}
GwLog::~GwLog(){
vSemaphoreDelete(locker);
}
void GwLog::writeOut(const char *data)
{
if (!writer)
{
if (iniBuffer && iniBufferFill < (INIBUFFERSIZE - 1))
{
size_t remain = INIBUFFERSIZE - iniBufferFill-1;
size_t len = strlen(data);
if (len < remain)
remain = len;
if (remain){
memcpy(iniBuffer + iniBufferFill, data, remain);
iniBufferFill += remain;
iniBuffer[iniBufferFill] = 0;
}
}
}
else
{
writer->write(data);
}
}
void GwLog::logString(const char *fmt,...){
va_list args;
va_start(args,fmt);
@ -24,42 +43,43 @@ void GwLog::logString(const char *fmt,...){
recordCounter++;
vsnprintf(buffer,bufferSize-1,fmt,args);
buffer[bufferSize-1]=0;
if (! writer) {
xSemaphoreGive(locker);
return;
}
writer->write(prefix.c_str());
writeOut(prefix.c_str());
char buf[20];
snprintf(buf,20,"%lu:",millis());
writer->write(buf);
writer->write(buffer);
writer->write("\n");
writeOut(buf);
writeOut(buffer);
writeOut("\n");
xSemaphoreGive(locker);
}
void GwLog::logDebug(int level,const char *fmt,...){
if (level > logLevel) return;
va_list args;
va_start(args,fmt);
logDebug(level,fmt,args);
}
void GwLog::logDebug(int level,const char *fmt,va_list args){
if (level > logLevel) return;
xSemaphoreTake(locker, portMAX_DELAY);
recordCounter++;
vsnprintf(buffer,bufferSize-1,fmt,args);
buffer[bufferSize-1]=0;
if (! writer) {
xSemaphoreGive(locker);
return;
}
writer->write(prefix.c_str());
writeOut(prefix.c_str());
char buf[20];
snprintf(buf,20,"%lu:",millis());
writer->write(buf);
writer->write(buffer);
writer->write("\n");
writeOut(buf);
writeOut(buffer);
writeOut("\n");
xSemaphoreGive(locker);
}
void GwLog::setWriter(GwLogWriter *writer){
xSemaphoreTake(locker, portMAX_DELAY);
if (this->writer) delete this->writer;
this->writer=writer;
if (iniBuffer && iniBufferFill){
writer->write(iniBuffer);
iniBufferFill=0;
delete[] iniBuffer;
iniBuffer=nullptr;
}
xSemaphoreGive(locker);
}

View File

@ -16,6 +16,10 @@ class GwLog{
GwLogWriter *writer;
SemaphoreHandle_t locker;
long long recordCounter=0;
const size_t INIBUFFERSIZE=1024;
char *iniBuffer=nullptr;
size_t iniBufferFill=0;
void writeOut(const char *data);
public:
static const int LOG=1;
static const int ERROR=0;
@ -27,6 +31,7 @@ class GwLog{
void setWriter(GwLogWriter *writer);
void logString(const char *fmt,...);
void logDebug(int level, const char *fmt,...);
void logDebug(int level, const char *fmt,va_list ap);
int isActive(int level){return level <= logLevel;};
void flush();
void setLevel(int level){this->logLevel=level;}

View File

@ -160,7 +160,7 @@ class MyAisDecoder : public AIS::AisDecoder
_uToPort + _uToStarboard, _uToStarboard, _uToBow, eta_days,
(_uEtaHour * 3600) + (_uEtaMinute * 60), _uDraught / 10.0, Dest,
(tN2kAISVersion) _ais_version, (tN2kGNSStype) _uFixType,
(tN2kAISDTE) _dte, (tN2kAISTranceiverInfo) _ais_version);
(tN2kAISDTE) _dte, (tN2kAISTransceiverInformation) _ais_version);
send(N2kMsg);
}

View File

@ -1,291 +0,0 @@
/**
* @section License
*
* The MIT License (MIT)
*
* Copyright (c) 2017, Thomas Barth, barth-dev.de
*
* 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:
*
* 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 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.
*/
#ifndef __DRIVERS_CAN_REGDEF_H_
#define __DRIVERS_CAN_REGDEF_H_
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
CAN_SPEED_100KBPS=100, /**< \brief CAN Node runs at 100kBit/s. */
CAN_SPEED_125KBPS=125, /**< \brief CAN Node runs at 125kBit/s. */
CAN_SPEED_250KBPS=250, /**< \brief CAN Node runs at 250kBit/s. */
CAN_SPEED_500KBPS=500, /**< \brief CAN Node runs at 500kBit/s. */
CAN_SPEED_800KBPS=800, /**< \brief CAN Node runs at 800kBit/s. */
CAN_SPEED_1000KBPS=1000 /**< \brief CAN Node runs at 1000kBit/s. */
}CAN_speed_t;
/**
* \brief CAN frame type (standard/extended)
*/
typedef enum {
CAN_frame_std=0, /**< Standard frame, using 11 bit identifer. */
CAN_frame_ext=1 /**< Extended frame, using 29 bit identifer. */
}CAN_frame_format_t;
/**
* \brief CAN RTR
*/
typedef enum {
CAN_no_RTR=0, /**< No RTR frame. */
CAN_RTR=1 /**< RTR frame. */
}CAN_RTR_t;
/** \brief Frame information record type */
typedef union{uint32_t U; /**< \brief Unsigned access */
struct {
uint8_t DLC:4; /**< \brief [3:0] DLC, Data length container */
unsigned int unknown_2:2; /**< \brief \internal unknown */
CAN_RTR_t RTR:1; /**< \brief [6:6] RTR, Remote Transmission Request */
CAN_frame_format_t FF:1; /**< \brief [7:7] Frame Format, see# CAN_frame_format_t*/
unsigned int reserved_24:24; /**< \brief \internal Reserved */
} B;
} CAN_FIR_t;
/** \brief Start address of CAN registers */
#define MODULE_CAN ((volatile CAN_Module_t *)0x3ff6b000)
/** \brief Get standard message ID */
#define _CAN_GET_STD_ID (((uint32_t)MODULE_CAN->MBX_CTRL.FCTRL.TX_RX.STD.ID[0] << 3) | \
(MODULE_CAN->MBX_CTRL.FCTRL.TX_RX.STD.ID[1] >> 5))
/** \brief Get extended message ID */
#define _CAN_GET_EXT_ID (((uint32_t)MODULE_CAN->MBX_CTRL.FCTRL.TX_RX.EXT.ID[0] << 21) | \
(MODULE_CAN->MBX_CTRL.FCTRL.TX_RX.EXT.ID[1] << 13) | \
(MODULE_CAN->MBX_CTRL.FCTRL.TX_RX.EXT.ID[2] << 5) | \
(MODULE_CAN->MBX_CTRL.FCTRL.TX_RX.EXT.ID[3] >> 3 ))
/** \brief Set standard message ID */
#define _CAN_SET_STD_ID(x) MODULE_CAN->MBX_CTRL.FCTRL.TX_RX.STD.ID[0] = ((x) >> 3); \
MODULE_CAN->MBX_CTRL.FCTRL.TX_RX.STD.ID[1] = ((x) << 5);
/** \brief Set extended message ID */
#define _CAN_SET_EXT_ID(x) MODULE_CAN->MBX_CTRL.FCTRL.TX_RX.EXT.ID[0] = ((x) >> 21); \
MODULE_CAN->MBX_CTRL.FCTRL.TX_RX.EXT.ID[1] = ((x) >> 13); \
MODULE_CAN->MBX_CTRL.FCTRL.TX_RX.EXT.ID[2] = ((x) >> 5); \
MODULE_CAN->MBX_CTRL.FCTRL.TX_RX.EXT.ID[3] = ((x) << 3); \
/** \brief Interrupt status register */
typedef enum {
__CAN_IRQ_RX= BIT(0), /**< \brief RX Interrupt */
__CAN_IRQ_TX= BIT(1), /**< \brief TX Interrupt */
__CAN_IRQ_ERR= BIT(2), /**< \brief Error Interrupt */
__CAN_IRQ_DATA_OVERRUN= BIT(3), /**< \brief Date Overrun Interrupt */
__CAN_IRQ_WAKEUP= BIT(4), /**< \brief Wakeup Interrupt */
__CAN_IRQ_ERR_PASSIVE= BIT(5), /**< \brief Passive Error Interrupt */
__CAN_IRQ_ARB_LOST= BIT(6), /**< \brief Arbitration lost interrupt */
__CAN_IRQ_BUS_ERR= BIT(7), /**< \brief Bus error Interrupt */
}__CAN_IRQ_t;
/** \brief OCMODE options. */
typedef enum {
__CAN_OC_BOM= 0b00, /**< \brief bi-phase output mode */
__CAN_OC_TOM= 0b01, /**< \brief test output mode */
__CAN_OC_NOM= 0b10, /**< \brief normal output mode */
__CAN_OC_COM= 0b11, /**< \brief clock output mode */
}__CAN_OCMODE_t;
/**
* CAN controller (SJA1000).
*/
typedef struct {
union{uint32_t U; /**< \brief Unsigned access */
struct {
unsigned int RM:1; /**< \brief MOD.0 Reset Mode */
unsigned int LOM:1; /**< \brief MOD.1 Listen Only Mode */
unsigned int STM:1; /**< \brief MOD.2 Self Test Mode */
unsigned int AFM:1; /**< \brief MOD.3 Acceptance Filter Mode */
unsigned int SM:1; /**< \brief MOD.4 Sleep Mode */
unsigned int reserved_27:27; /**< \brief \internal Reserved */
} B;
} MOD;
union{uint32_t U; /**< \brief Unsigned access */
struct {
unsigned int TR:1; /**< \brief CMR.0 Transmission Request */
unsigned int AT:1; /**< \brief CMR.1 Abort Transmission */
unsigned int RRB:1; /**< \brief CMR.2 Release Receive Buffer */
unsigned int CDO:1; /**< \brief CMR.3 Clear Data Overrun */
unsigned int GTS:1; /**< \brief CMR.4 Go To Sleep */
unsigned int reserved_27:27; /**< \brief \internal Reserved */
} B;
} CMR;
union{uint32_t U; /**< \brief Unsigned access */
struct {
unsigned int RBS:1; /**< \brief SR.0 Receive Buffer Status */
unsigned int DOS:1; /**< \brief SR.1 Data Overrun Status */
unsigned int TBS:1; /**< \brief SR.2 Transmit Buffer Status */
unsigned int TCS:1; /**< \brief SR.3 Transmission Complete Status */
unsigned int RS:1; /**< \brief SR.4 Receive Status */
unsigned int TS:1; /**< \brief SR.5 Transmit Status */
unsigned int ES:1; /**< \brief SR.6 Error Status */
unsigned int BS:1; /**< \brief SR.7 Bus Status */
unsigned int reserved_24:24; /**< \brief \internal Reserved */
} B;
} SR;
union{uint32_t U; /**< \brief Unsigned access */
struct {
unsigned int RI:1; /**< \brief IR.0 Receive Interrupt */
unsigned int TI:1; /**< \brief IR.1 Transmit Interrupt */
unsigned int EI:1; /**< \brief IR.2 Error Interrupt */
unsigned int DOI:1; /**< \brief IR.3 Data Overrun Interrupt */
unsigned int WUI:1; /**< \brief IR.4 Wake-Up Interrupt */
unsigned int EPI:1; /**< \brief IR.5 Error Passive Interrupt */
unsigned int ALI:1; /**< \brief IR.6 Arbitration Lost Interrupt */
unsigned int BEI:1; /**< \brief IR.7 Bus Error Interrupt */
unsigned int reserved_24:24; /**< \brief \internal Reserved */
} B;
} IR;
union{uint32_t U; /**< \brief Unsigned access */
struct {
unsigned int RIE:1; /**< \brief IER.0 Receive Interrupt Enable */
unsigned int TIE:1; /**< \brief IER.1 Transmit Interrupt Enable */
unsigned int EIE:1; /**< \brief IER.2 Error Interrupt Enable */
unsigned int DOIE:1; /**< \brief IER.3 Data Overrun Interrupt Enable */
unsigned int WUIE:1; /**< \brief IER.4 Wake-Up Interrupt Enable */
unsigned int EPIE:1; /**< \brief IER.5 Error Passive Interrupt Enable */
unsigned int ALIE:1; /**< \brief IER.6 Arbitration Lost Interrupt Enable */
unsigned int BEIE:1; /**< \brief IER.7 Bus Error Interrupt Enable */
unsigned int reserved_24:24; /**< \brief \internal Reserved */
} B;
} IER;
uint32_t RESERVED0;
union{uint32_t U; /**< \brief Unsigned access */
struct {
unsigned int BRP:6; /**< \brief BTR0[5:0] Baud Rate Prescaler */
unsigned int SJW:2; /**< \brief BTR0[7:6] Synchronization Jump Width*/
unsigned int reserved_24:24; /**< \brief \internal Reserved */
} B;
} BTR0;
union{uint32_t U; /**< \brief Unsigned access */
struct {
unsigned int TSEG1:4; /**< \brief BTR1[3:0] Timing Segment 1 */
unsigned int TSEG2:3; /**< \brief BTR1[6:4] Timing Segment 2*/
unsigned int SAM:1; /**< \brief BTR1.7 Sampling*/
unsigned int reserved_24:24; /**< \brief \internal Reserved */
} B;
} BTR1;
union{uint32_t U; /**< \brief Unsigned access */
struct {
unsigned int OCMODE:2; /**< \brief OCR[1:0] Output Control Mode, see # */
unsigned int OCPOL0:1; /**< \brief OCR.2 Output Control Polarity 0 */
unsigned int OCTN0:1; /**< \brief OCR.3 Output Control Transistor N0 */
unsigned int OCTP0:1; /**< \brief OCR.4 Output Control Transistor P0 */
unsigned int OCPOL1:1; /**< \brief OCR.5 Output Control Polarity 1 */
unsigned int OCTN1:1; /**< \brief OCR.6 Output Control Transistor N1 */
unsigned int OCTP1:1; /**< \brief OCR.7 Output Control Transistor P1 */
unsigned int reserved_24:24; /**< \brief \internal Reserved */
} B;
} OCR;
uint32_t RESERVED1[2];
union{uint32_t U; /**< \brief Unsigned access */
struct {
unsigned int ALC:8; /**< \brief ALC[7:0] Arbitration Lost Capture */
unsigned int reserved_24:24; /**< \brief \internal Reserved */
} B;
} ALC;
union{uint32_t U; /**< \brief Unsigned access */
struct {
unsigned int ECC:8; /**< \brief ECC[7:0] Error Code Capture */
unsigned int reserved_24:24; /**< \brief \internal Reserved */
} B;
} ECC;
union{uint32_t U; /**< \brief Unsigned access */
struct {
unsigned int EWLR:8; /**< \brief EWLR[7:0] Error Warning Limit */
unsigned int reserved_24:24; /**< \brief \internal Reserved */
} B;
} EWLR;
union{uint32_t U; /**< \brief Unsigned access */
struct {
unsigned int RXERR:8; /**< \brief RXERR[7:0] Receive Error Counter */
unsigned int reserved_24:24; /**< \brief \internal Reserved */
} B;
} RXERR;
union{uint32_t U; /**< \brief Unsigned access */
struct {
unsigned int TXERR:8; /**< \brief TXERR[7:0] Transmit Error Counter */
unsigned int reserved_24:24; /**< \brief \internal Reserved */
} B;
} TXERR;
union {
struct {
uint32_t CODE[4]; /**< \brief Acceptance Message ID */
uint32_t MASK[4]; /**< \brief Acceptance Mask */
uint32_t RESERVED2[5];
} ACC; /**< \brief Acceptance filtering */
struct {
CAN_FIR_t FIR; /**< \brief Frame information record */
union{
struct {
uint32_t ID[2]; /**< \brief Standard frame message-ID*/
uint32_t data[8]; /**< \brief Standard frame payload */
uint32_t reserved[2];
} STD; /**< \brief Standard frame format */
struct {
uint32_t ID[4]; /**< \brief Extended frame message-ID*/
uint32_t data[8]; /**< \brief Extended frame payload */
} EXT; /**< \brief Extended frame format */
}TX_RX; /**< \brief RX/TX interface */
}FCTRL; /**< \brief Function control regs */
} MBX_CTRL; /**< \brief Mailbox control */
union{uint32_t U; /**< \brief Unsigned access */
struct {
unsigned int RMC:8; /**< \brief RMC[7:0] RX Message Counter */
unsigned int reserved_24:24; /**< \brief \internal Reserved Enable */
} B;
} RMC;
union{uint32_t U; /**< \brief Unsigned access */
struct {
unsigned int RBSA:8; /**< \brief RBSA[7:0] RX Buffer Start Address */
unsigned int reserved_24:24; /**< \brief \internal Reserved Enable */
} B;
} RBSA;
union{uint32_t U; /**< \brief Unsigned access */
struct {
unsigned int COD:3; /**< \brief CDR[2:0] CLKOUT frequency selector based of fOSC*/
unsigned int COFF:1; /**< \brief CDR.3 CLKOUT off*/
unsigned int reserved_1:1; /**< \brief \internal Reserved */
unsigned int RXINTEN:1; /**< \brief CDR.5 This bit allows the TX1 output to be used as a dedicated receive interrupt output*/
unsigned int CBP:1; /**< \brief CDR.6 allows to bypass the CAN input comparator and is only possible in reset mode.*/
unsigned int CAN_M:1; /**< \brief CDR.7 If CDR.7 is at logic 0 the CAN controller operates in BasicCAN mode. If set to logic 1 the CAN controller operates in PeliCAN mode. Write access is only possible in reset mode*/
unsigned int reserved_24:24; /**< \brief \internal Reserved */
} B;
} CDR;
uint32_t IRAM[2];
}CAN_Module_t;
#ifdef __cplusplus
}
#endif
#endif /* __DRIVERS_CAN_REGDEF_H_ */

View File

@ -1,385 +0,0 @@
/*
NMEA2000_esp32.cpp
Copyright (c) 2015-2020 Timo Lappalainen, Kave Oy, www.kave.fi
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:
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
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.
Inherited NMEA2000 object for ESP32 modules. See also NMEA2000 library.
Thanks to Thomas Barth, barth-dev.de, who has written ESP32 CAN code. To avoid extra
libraries, I implemented his code directly to the NMEA2000_esp32 to avoid extra
can.h library, which may cause even naming problem.
*/
#include "driver/periph_ctrl.h"
#include "soc/dport_reg.h"
#include "NMEA2000_esp32.h"
//time to reinit CAN bus if the queue is full for that time
#define SEND_CANCEL_TIME 2000
//reinit CAN bis if nothing send/received within this time
#define RECEIVE_REINIT_TIME 60000
bool tNMEA2000_esp32::CanInUse=false;
tNMEA2000_esp32 *pNMEA2000_esp32=0;
void ESP32Can1Interrupt(void *);
#define ECDEBUG(fmt,args...) if(debugStream){debugStream->printf(fmt, ## args);}
//*****************************************************************************
tNMEA2000_esp32::tNMEA2000_esp32(gpio_num_t _TxPin,
gpio_num_t _RxPin,
Print *dbg) :
tNMEA2000(), IsOpen(false),
speed(CAN_SPEED_250KBPS), TxPin(_TxPin), RxPin(_RxPin),
RxQueue(NULL), TxQueue(NULL) {
debugStream=dbg;
}
//*****************************************************************************
bool tNMEA2000_esp32::CANSendFrame(unsigned long id, unsigned char len, const unsigned char *buf, bool /*wait_sent*/) {
tCANFrame frame;
unsigned long now=millis();
if ( uxQueueSpacesAvailable(TxQueue)==0 ) {
if (lastSend && (lastSend + SEND_CANCEL_TIME) < now){
ECDEBUG("CanSendFrame Aborting and emptying queue\n");
while (xQueueReceive(TxQueue,&frame,0)){}
errReinit++;
CAN_init(false);
if ( uxQueueSpacesAvailable(TxQueue)==0 ) return false;
}
else{
ECDEBUG("CanSendFrame queue full\n");
return false; // can not send to queue
}
}
lastSend=now;
frame.id=id;
frame.len=len>8?8:len;
memcpy(frame.buf,buf,len);
CheckBusOff();
ECDEBUG("CanSendFrame IntrCnt %d\n",cntIntr);
ECDEBUG("CanSendFrame Error TX/RX %d/%d\n",MODULE_CAN->TXERR.U,MODULE_CAN->RXERR.U);
ECDEBUG("CanSendFrame Error Overrun %d\n",errOverrun);
ECDEBUG("CanSendFrame Error Arbitration %d\n",errArb);
ECDEBUG("CanSendFrame Error Bus %d\n",errBus);
ECDEBUG("CanSendFrame Error Recovery %d\n",errRecovery);
ECDEBUG("CanSendFrame ErrorCount %d\n",errCountTxInternal);
ECDEBUG("CanSendFrame ErrorCancel %d\n",errCancelTransmit);
ECDEBUG("CanSendFrame ErrorReinit %d\n",errReinit);
ECDEBUG("CanSendFrame busOff=%d, errPassive=%d\n",MODULE_CAN->SR.B.BS,MODULE_CAN->SR.B.ES)
xQueueSendToBack(TxQueue,&frame,0); // Add frame to queue
if ( MODULE_CAN->SR.B.TBS==0 ) {
ECDEBUG("CanSendFrame: wait for ISR to send %d\n",frame.id);
return true; // Currently sending, ISR takes care of sending
}
if ( MODULE_CAN->SR.B.TBS==1 ) { // Check again and restart send, if is not going on
xQueueReceive(TxQueue,&frame,0);
ECDEBUG("CanSendFrame: send direct %d\n",frame.id);
CAN_send_frame(frame);
//return MODULE_CAN->TXERR.U < 127;
}
return true;
}
//*****************************************************************************
void tNMEA2000_esp32::InitCANFrameBuffers() {
if (MaxCANReceiveFrames<10 ) MaxCANReceiveFrames=50; // ESP32 has plenty of RAM
if (MaxCANSendFrames<10 ) MaxCANSendFrames=40;
uint16_t CANGlobalBufSize=MaxCANSendFrames-4;
MaxCANSendFrames=4; // we do not need much libary internal buffer since driver has them.
RxQueue=xQueueCreate(MaxCANReceiveFrames,sizeof(tCANFrame));
TxQueue=xQueueCreate(CANGlobalBufSize,sizeof(tCANFrame));
tNMEA2000::InitCANFrameBuffers(); // call main initialization
}
//*****************************************************************************
bool tNMEA2000_esp32::CANOpen() {
if (IsOpen) return true;
if (CanInUse) return false; // currently prevent accidental second instance. Maybe possible in future.
pNMEA2000_esp32=this;
IsOpen=true;
CAN_init();
CanInUse=IsOpen;
return IsOpen;
}
//*****************************************************************************
bool tNMEA2000_esp32::CANGetFrame(unsigned long &id, unsigned char &len, unsigned char *buf) {
bool HasFrame=false;
tCANFrame frame;
CheckBusOff();
unsigned long now=millis();
//receive next CAN frame from queue
if ( xQueueReceive(RxQueue,&frame, 0)==pdTRUE ) {
HasFrame=true;
id=frame.id;
len=frame.len;
memcpy(buf,frame.buf,frame.len);
lastReceive=now;
}
else{
if (lastReceive != 0 && (lastReceive + RECEIVE_REINIT_TIME) < now && (lastSend + RECEIVE_REINIT_TIME) < now){
ECDEBUG("Noting received within %d ms, reinit",RECEIVE_REINIT_TIME);
CAN_init(false);
}
}
return HasFrame;
}
//*****************************************************************************
void tNMEA2000_esp32::CAN_init(bool installIsr) {
//Time quantum
double __tq;
// A soft reset of the ESP32 leaves it's CAN controller in an undefined state so a reset is needed.
// Reset CAN controller to same state as it would be in after a power down reset.
periph_module_reset(PERIPH_CAN_MODULE);
//enable module
DPORT_SET_PERI_REG_MASK(DPORT_PERIP_CLK_EN_REG, DPORT_CAN_CLK_EN);
DPORT_CLEAR_PERI_REG_MASK(DPORT_PERIP_RST_EN_REG, DPORT_CAN_RST);
//configure RX pin
gpio_set_direction(RxPin,GPIO_MODE_INPUT);
gpio_matrix_in(RxPin,CAN_RX_IDX,0);
gpio_pad_select_gpio(RxPin);
//set to PELICAN mode
MODULE_CAN->CDR.B.CAN_M=0x1;
//synchronization jump width is the same for all baud rates
MODULE_CAN->BTR0.B.SJW =0x1;
//TSEG2 is the same for all baud rates
MODULE_CAN->BTR1.B.TSEG2 =0x1;
//select time quantum and set TSEG1
switch (speed) {
case CAN_SPEED_1000KBPS:
MODULE_CAN->BTR1.B.TSEG1 =0x4;
__tq = 0.125;
break;
case CAN_SPEED_800KBPS:
MODULE_CAN->BTR1.B.TSEG1 =0x6;
__tq = 0.125;
break;
default:
MODULE_CAN->BTR1.B.TSEG1 =0xc;
__tq = ((float)1000/speed) / 16;
}
//set baud rate prescaler
MODULE_CAN->BTR0.B.BRP=(uint8_t)round((((APB_CLK_FREQ * __tq) / 2) - 1)/1000000)-1;
/* Set sampling
* 1 -> triple; the bus is sampled three times; recommended for low/medium speed buses (class A and B) where filtering spikes on the bus line is beneficial
* 0 -> single; the bus is sampled once; recommended for high speed buses (SAE class C)*/
MODULE_CAN->BTR1.B.SAM =0x1;
//enable all interrupts
MODULE_CAN->IER.U = 0xef; // bit 0x10 contains Baud Rate Prescaler Divider (BRP_DIV) bit
//no acceptance filtering, as we want to fetch all messages
MODULE_CAN->MBX_CTRL.ACC.CODE[0] = 0;
MODULE_CAN->MBX_CTRL.ACC.CODE[1] = 0;
MODULE_CAN->MBX_CTRL.ACC.CODE[2] = 0;
MODULE_CAN->MBX_CTRL.ACC.CODE[3] = 0;
MODULE_CAN->MBX_CTRL.ACC.MASK[0] = 0xff;
MODULE_CAN->MBX_CTRL.ACC.MASK[1] = 0xff;
MODULE_CAN->MBX_CTRL.ACC.MASK[2] = 0xff;
MODULE_CAN->MBX_CTRL.ACC.MASK[3] = 0xff;
//set to normal mode
MODULE_CAN->OCR.B.OCMODE=__CAN_OC_NOM;
//clear error counters
MODULE_CAN->TXERR.U = 0;
MODULE_CAN->RXERR.U = 0;
(void)MODULE_CAN->ECC;
//clear interrupt flags
(void)MODULE_CAN->IR.U;
//install CAN ISR
if (installIsr) esp_intr_alloc(ETS_CAN_INTR_SOURCE,0,ESP32Can1Interrupt,NULL,NULL);
//configure TX pin
// We do late configure, since some initialization above caused CAN Tx flash
// shortly causing one error frame on startup. By setting CAN pin here
// it works right.
gpio_set_direction(TxPin,GPIO_MODE_OUTPUT);
gpio_matrix_out(TxPin,CAN_TX_IDX,0,0);
gpio_pad_select_gpio(TxPin);
//Showtime. Release Reset Mode.
MODULE_CAN->MOD.B.RM = 0;
}
//*****************************************************************************
void tNMEA2000_esp32::CAN_read_frame() {
tCANFrame frame;
CAN_FIR_t FIR;
//get FIR
FIR.U=MODULE_CAN->MBX_CTRL.FCTRL.FIR.U;
frame.len=FIR.B.DLC>8?8:FIR.B.DLC;
// Handle only extended frames
if (FIR.B.FF==CAN_frame_ext) { //extended frame
//Get Message ID
frame.id = _CAN_GET_EXT_ID;
//deep copy data bytes
for( size_t i=0; i<frame.len; i++ ) {
frame.buf[i]=MODULE_CAN->MBX_CTRL.FCTRL.TX_RX.EXT.data[i];
}
//send frame to input queue
xQueueSendToBackFromISR(RxQueue,&frame,0);
}
//Let the hardware know the frame has been read.
MODULE_CAN->CMR.B.RRB=1;
}
//*****************************************************************************
void tNMEA2000_esp32::CAN_send_frame(tCANFrame &frame) {
CAN_FIR_t FIR;
FIR.U=0;
FIR.B.DLC=frame.len>8?8:frame.len;
FIR.B.FF=CAN_frame_ext;
//copy frame information record
MODULE_CAN->MBX_CTRL.FCTRL.FIR.U=FIR.U;
//Write message ID
_CAN_SET_EXT_ID(frame.id);
// Copy the frame data to the hardware
for ( size_t i=0; i<frame.len; i++) {
MODULE_CAN->MBX_CTRL.FCTRL.TX_RX.EXT.data[i]=frame.buf[i];
}
// Transmit frame
MODULE_CAN->CMR.B.TR=1;
}
#define CAN_MAX_TX_RETRY 12
#define RECOVERY_RETRY_MS 1000
void tNMEA2000_esp32::CAN_bus_off_recovery(){
unsigned long now=millis();
if (recoveryStarted && (recoveryStarted + RECOVERY_RETRY_MS) > now ) return;
ECDEBUG("CAN_bus_off_recovery started\n");
recoveryStarted=now;
errRecovery++;
MODULE_CAN->CMR.B.AT=1; // abort transmission
(void)MODULE_CAN->SR.U;
MODULE_CAN->TXERR.U = 127;
MODULE_CAN->RXERR.U = 0;
MODULE_CAN->MOD.B.RM = 0;
}
void tNMEA2000_esp32::CheckBusOff(){
//should we really recover here?
if (MODULE_CAN->SR.B.BS){
ECDEBUG("Bus off detected, trying recovery\n");
CAN_bus_off_recovery();
}
}
//*****************************************************************************
void tNMEA2000_esp32::InterruptHandler() {
//Interrupt flag buffer
cntIntr++;
uint32_t interrupt;
// Read interrupt status and clear flags
interrupt = (MODULE_CAN->IR.U & 0xff);
// Handle TX complete interrupt
//see http://uglyduck.vajn.icu/PDF/wireless/Espressif/ESP32/Eco_and_Workarounds_for_Bugs_in_ESP32.pdf, 3.13.4
if ((interrupt & __CAN_IRQ_TX) != 0 || MODULE_CAN->SR.B.TBS) {
tCANFrame frame;
if ( (xQueueReceiveFromISR(TxQueue,&frame,NULL)==pdTRUE) ) {
CAN_send_frame(frame);
}
}
// Handle RX frame available interrupt
if ((interrupt & __CAN_IRQ_RX) != 0) {
CAN_read_frame();
}
// Handle error interrupts.
if ((interrupt & (__CAN_IRQ_ERR //0x4
| __CAN_IRQ_WAKEUP //0x10
| __CAN_IRQ_ERR_PASSIVE //0x20
)) != 0) {
/*handler*/
}
//https://www.esp32.com/viewtopic.php?t=5010
// Handle error interrupts.
if (interrupt & __CAN_IRQ_DATA_OVERRUN ) { //0x08
errOverrun++;
MODULE_CAN->CMR.B.CDO=1;
(void)MODULE_CAN->SR.U; // read SR after write to CMR to settle register changes
}
if (interrupt & __CAN_IRQ_ARB_LOST ) { //0x40
errArb++;
(void)MODULE_CAN->ALC.U; // must be read to re-enable interrupt
errCountTxInternal++;
}
if (interrupt & __CAN_IRQ_BUS_ERR ) { //0x80
errBus++;
(void)MODULE_CAN->ECC.U; // must be read to re-enable interrupt
errCountTxInternal+=2;
}
if (MODULE_CAN->TXERR.U == 0){
recoveryStarted=0;
}
if (errCountTxInternal >= 2 *CAN_MAX_TX_RETRY){
MODULE_CAN->CMR.B.AT=1; // abort transmission
(void)MODULE_CAN->SR.U;
errCountTxInternal=0;
}
}
//*****************************************************************************
void ESP32Can1Interrupt(void *) {
pNMEA2000_esp32->InterruptHandler();
}

View File

@ -1,106 +0,0 @@
/*
NMEA2000_esp32.h
Copyright (c) 2015-2020 Timo Lappalainen, Kave Oy, www.kave.fi
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:
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
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.
Inherited NMEA2000 object for ESP32 modules. See also NMEA2000 library.
Thanks to Thomas Barth, barth-dev.de, who has written ESP32 CAN code. To avoid extra
libraries, I implemented his code directly to the NMEA2000_esp32 to avoid extra
can.h library, which may cause even naming problem.
The library sets as default CAN Tx pin to GPIO 16 and CAN Rx pint to GPIO 4. If you
want to use other pins (I have not tested can any pins be used), add defines e.g.
#define ESP32_CAN_TX_PIN GPIO_NUM_34
#define ESP32_CAN_RX_PIN GPIO_NUM_35
before including NMEA2000_esp32.h or NMEA2000_CAN.h
*/
#ifndef _NMEA2000_ESP32_H_
#define _NMEA2000_ESP32_H_
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "driver/gpio.h"
#include "NMEA2000.h"
#include "N2kMsg.h"
#include "ESP32_CAN_def.h"
#ifndef ESP32_CAN_TX_PIN
#define ESP32_CAN_TX_PIN GPIO_NUM_16
#endif
#ifndef ESP32_CAN_RX_PIN
#define ESP32_CAN_RX_PIN GPIO_NUM_4
#endif
class tNMEA2000_esp32 : public tNMEA2000
{
private:
bool IsOpen;
static bool CanInUse;
protected:
struct tCANFrame {
uint32_t id; // can identifier
uint8_t len; // length of data
uint8_t buf[8];
};
protected:
CAN_speed_t speed;
gpio_num_t TxPin;
gpio_num_t RxPin;
QueueHandle_t RxQueue;
QueueHandle_t TxQueue;
Print *debugStream;
int errOverrun=0;
int errArb=0;
int errBus=0;
int errRecovery=0;
int errCountTxInternal=0;
int errCancelTransmit=0;
int errReinit=0;
unsigned long recoveryStarted=0;
unsigned long lastSend=0;
unsigned long lastReceive=0;
int cntIntr=0;
protected:
void CAN_read_frame(); // Read frame to queue within interrupt
void CAN_send_frame(tCANFrame &frame); // Send frame
void CAN_init(bool installIsr=true);
void CAN_bus_off_recovery(); //recover from bus off
void CheckBusOff();
protected:
bool CANSendFrame(unsigned long id, unsigned char len, const unsigned char *buf, bool wait_sent=true);
bool CANOpen();
bool CANGetFrame(unsigned long &id, unsigned char &len, unsigned char *buf);
virtual void InitCANFrameBuffers();
public:
tNMEA2000_esp32(gpio_num_t _TxPin=ESP32_CAN_TX_PIN,
gpio_num_t _RxPin=ESP32_CAN_RX_PIN,
Print *debugStream=NULL);
void InterruptHandler();
};
#endif

View File

@ -1,3 +0,0 @@
forked from https://github.com/ttlappalainen/NMEA2000_esp32
with some error handling additions
based on https://www.esp32.com/viewtopic.php?t=5010

View File

@ -738,7 +738,7 @@ private:
char _Destination[21];
tN2kAISVersion _AISversion;
tN2kGNSStype _GNSStype;
tN2kAISTranceiverInfo _AISinfo;
tN2kAISTransceiverInformation _AISinfo;
tN2kAISDTE _DTE;
tNMEA0183AISMsg NMEA0183AISMsg;
@ -848,15 +848,16 @@ private:
tN2kAISUnit _Unit;
bool _Display, _DSC, _Band, _Msg22, _State;
tN2kAISMode _Mode;
tN2kAISTransceiverInformation _AISTranceiverInformation;
if (ParseN2kPGN129039(N2kMsg, _MessageID, _Repeat, _UserID, _Latitude, _Longitude, _Accuracy, _RAIM,
_Seconds, _COG, _SOG, _Heading, _Unit, _Display, _DSC, _Band, _Msg22, _Mode, _State))
_Seconds, _COG, _SOG, _AISTranceiverInformation, _Heading, _Unit, _Display, _DSC, _Band, _Msg22, _Mode, _State))
{
tNMEA0183AISMsg NMEA0183AISMsg;
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);

View File

@ -347,26 +347,29 @@ bool AddDimensions(tNMEA0183AISMsg &NMEA0183AISMsg, double Length, double Beam,
uint16_t _PosRefStbd = 0;
uint16_t _PosRefPort = 0;
if ( PosRefBow >= 0.0 && PosRefBow <= 511.0 ) {
_PosRefBow = ceil(PosRefBow);
if (PosRefBow < 0) PosRefBow=0; //could be N2kIsNA
if ( PosRefBow <= 511.0 ) {
_PosRefBow = round(PosRefBow);
} else {
_PosRefBow = 511;
}
if ( PosRefStbd >= 0.0 && PosRefStbd <= 63.0 ) {
_PosRefStbd = ceil(PosRefStbd);
if (PosRefStbd < 0 ) PosRefStbd=0; //could be N2kIsNA
if (PosRefStbd <= 63.0 ) {
_PosRefStbd = round(PosRefStbd);
} else {
_PosRefStbd = 63;
}
if ( !N2kIsNA(Length) ) {
_PosRefStern = ceil( Length ) - _PosRefBow;
if ( _PosRefStern < 0 ) _PosRefStern = 0;
if (Length >= PosRefBow){
_PosRefStern=round(Length - PosRefBow);
}
if ( _PosRefStern > 511 ) _PosRefStern = 511;
}
if ( !N2kIsNA(Beam) ) {
_PosRefPort = ceil( Beam ) - _PosRefStbd;
if ( _PosRefPort < 0 ) _PosRefPort = 0;
if (Beam >= PosRefStbd){
_PosRefPort = round( Beam - PosRefStbd);
}
if ( _PosRefPort > 63 ) _PosRefPort = 63;
}

View File

@ -0,0 +1,204 @@
#include "Nmea2kTwai.h"
#include "driver/gpio.h"
#include "driver/twai.h"
#define LOGID(id) ((id >> 8) & 0x1ffff)
static const int TIMEOUT_OFFLINE=256; //# of timeouts to consider offline
Nmea2kTwai::Nmea2kTwai(gpio_num_t _TxPin, gpio_num_t _RxPin, unsigned long recP, unsigned long logP):
tNMEA2000(),RxPin(_RxPin),TxPin(_TxPin)
{
if (RxPin < 0 || TxPin < 0){
disabled=true;
}
else{
timers.addAction(logP,[this](){logStatus();});
timers.addAction(recP,[this](){checkRecovery();});
}
}
bool Nmea2kTwai::CANSendFrame(unsigned long id, unsigned char len, const unsigned char *buf, bool wait_sent)
{
if (disabled) return true;
twai_message_t message;
memset(&message,0,sizeof(message));
message.identifier = id;
message.extd = 1;
message.data_length_code = len;
memcpy(message.data,buf,len);
esp_err_t rt=twai_transmit(&message,0);
if (rt != ESP_OK){
if (rt == ESP_ERR_TIMEOUT){
if (txTimeouts < TIMEOUT_OFFLINE) txTimeouts++;
}
logDebug(LOG_MSG,"twai transmit for %ld failed: %x",LOGID(id),(int)rt);
return false;
}
txTimeouts=0;
logDebug(LOG_MSG,"twai transmit id %ld, len %d",LOGID(id),(int)len);
return true;
}
bool Nmea2kTwai::CANOpen()
{
if (disabled){
logDebug(LOG_INFO,"CAN disabled");
return true;
}
esp_err_t rt=twai_start();
if (rt != ESP_OK){
logDebug(LOG_ERR,"CANOpen failed: %x",(int)rt);
return false;
}
else{
logDebug(LOG_INFO,"CANOpen ok");
}
return true;
}
bool Nmea2kTwai::CANGetFrame(unsigned long &id, unsigned char &len, unsigned char *buf)
{
if (disabled) return false;
twai_message_t message;
esp_err_t rt=twai_receive(&message,0);
if (rt != ESP_OK){
return false;
}
if (! message.extd){
return false;
}
id=message.identifier;
len=message.data_length_code;
if (len > 8){
logDebug(LOG_DEBUG,"twai: received invalid message %lld, len %d",LOGID(id),len);
len=8;
}
logDebug(LOG_MSG,"twai rcv id=%ld,len=%d, ext=%d",LOGID(message.identifier),message.data_length_code,message.extd);
if (! message.rtr){
memcpy(buf,message.data,message.data_length_code);
}
return true;
}
void Nmea2kTwai::initDriver(){
if (disabled) return;
twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(TxPin,RxPin, TWAI_MODE_NORMAL);
g_config.tx_queue_len=20;
twai_timing_config_t t_config = TWAI_TIMING_CONFIG_250KBITS();
twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();
esp_err_t rt=twai_driver_install(&g_config, &t_config, &f_config);
if (rt == ESP_OK) {
logDebug(LOG_INFO,"twai driver initialzed, rx=%d,tx=%d",(int)RxPin,(int)TxPin);
}
else{
logDebug(LOG_ERR,"twai driver init failed: %x",(int)rt);
}
}
// This will be called on Open() before any other initialization. Inherit this, if buffers can be set for the driver
// and you want to change size of library send frame buffer size. See e.g. NMEA2000_teensy.cpp.
void Nmea2kTwai::InitCANFrameBuffers()
{
if (disabled){
logDebug(LOG_INFO,"twai init - disabled");
}
else{
initDriver();
}
tNMEA2000::InitCANFrameBuffers();
}
Nmea2kTwai::Status Nmea2kTwai::getStatus(){
twai_status_info_t state;
Status rt;
if (disabled){
rt.state=ST_DISABLED;
return rt;
}
if (twai_get_status_info(&state) != ESP_OK){
return rt;
}
switch(state.state){
case TWAI_STATE_STOPPED:
rt.state=ST_STOPPED;
break;
case TWAI_STATE_RUNNING:
rt.state=ST_RUNNING;
break;
case TWAI_STATE_BUS_OFF:
rt.state=ST_BUS_OFF;
break;
case TWAI_STATE_RECOVERING:
rt.state=ST_RECOVERING;
break;
}
rt.rx_errors=state.rx_error_counter;
rt.tx_errors=state.tx_error_counter;
rt.tx_failed=state.tx_failed_count;
rt.rx_missed=state.rx_missed_count;
rt.rx_overrun=state.rx_overrun_count;
rt.tx_timeouts=txTimeouts;
if (rt.tx_timeouts >= TIMEOUT_OFFLINE && rt.state == ST_RUNNING){
rt.state=ST_OFFLINE;
}
return rt;
}
bool Nmea2kTwai::checkRecovery(){
if (disabled) return false;
Status canState=getStatus();
bool strt=false;
if (canState.state != Nmea2kTwai::ST_RUNNING)
{
if (canState.state == Nmea2kTwai::ST_BUS_OFF)
{
strt = true;
bool rt = startRecovery();
logDebug(LOG_INFO, "twai BUS_OFF: start can recovery - result %d", (int)rt);
}
if (canState.state == Nmea2kTwai::ST_STOPPED)
{
bool rt = CANOpen();
logDebug(LOG_INFO, "twai STOPPED: restart can driver - result %d", (int)rt);
}
}
return strt;
}
void Nmea2kTwai::loop(){
if (disabled) return;
timers.loop();
}
Nmea2kTwai::Status Nmea2kTwai::logStatus(){
Status canState=getStatus();
logDebug(LOG_INFO, "twai state %s, rxerr %d, txerr %d, txfail %d, txtimeout %d, rxmiss %d, rxoverrun %d",
stateStr(canState.state),
canState.rx_errors,
canState.tx_errors,
canState.tx_failed,
canState.tx_timeouts,
canState.rx_missed,
canState.rx_overrun);
return canState;
}
bool Nmea2kTwai::startRecovery(){
if (disabled) return false;
lastRecoveryStart=millis();
esp_err_t rt=twai_driver_uninstall();
if (rt != ESP_OK){
logDebug(LOG_ERR,"twai: deinit for recovery failed with %x",(int)rt);
}
initDriver();
bool frt=CANOpen();
return frt;
}
const char * Nmea2kTwai::stateStr(const Nmea2kTwai::STATE &st){
switch (st)
{
case ST_BUS_OFF: return "BUS_OFF";
case ST_RECOVERING: return "RECOVERING";
case ST_RUNNING: return "RUNNING";
case ST_STOPPED: return "STOPPED";
case ST_OFFLINE: return "OFFLINE";
case ST_DISABLED: return "DISABLED";
}
return "ERROR";
}

View File

@ -0,0 +1,63 @@
#ifndef _NMEA2KTWAI_H
#define _NMEA2KTWAI_H
#include "NMEA2000.h"
#include "GwTimer.h"
class Nmea2kTwai : public tNMEA2000{
public:
Nmea2kTwai(gpio_num_t _TxPin, gpio_num_t _RxPin, unsigned long recP=0, unsigned long logPeriod=0);
typedef enum{
ST_STOPPED,
ST_RUNNING,
ST_BUS_OFF,
ST_RECOVERING,
ST_OFFLINE,
ST_DISABLED,
ST_ERROR
} STATE;
typedef struct{
//see https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/twai.html#_CPPv418twai_status_info_t
uint32_t rx_errors=0;
uint32_t tx_errors=0;
uint32_t tx_failed=0;
uint32_t rx_missed=0;
uint32_t rx_overrun=0;
uint32_t tx_timeouts=0;
STATE state=ST_ERROR;
} Status;
Status getStatus();
unsigned long getLastRecoveryStart(){return lastRecoveryStart;}
void loop();
static const char * stateStr(const STATE &st);
virtual bool CANOpen();
virtual ~Nmea2kTwai(){};
static const int LOG_ERR=0;
static const int LOG_INFO=1;
static const int LOG_DEBUG=2;
static const int LOG_MSG=3;
protected:
// Virtual functions for different interfaces. Currently there are own classes
// for Arduino due internal CAN (NMEA2000_due), external MCP2515 SPI CAN bus controller (NMEA2000_mcp),
// Teensy FlexCAN (NMEA2000_Teensy), NMEA2000_avr for AVR, NMEA2000_mbed for MBED and NMEA2000_socketCAN for e.g. RPi.
virtual bool CANSendFrame(unsigned long id, unsigned char len, const unsigned char *buf, bool wait_sent=true);
virtual bool CANGetFrame(unsigned long &id, unsigned char &len, unsigned char *buf);
// This will be called on Open() before any other initialization. Inherit this, if buffers can be set for the driver
// and you want to change size of library send frame buffer size. See e.g. NMEA2000_teensy.cpp.
virtual void InitCANFrameBuffers();
virtual void logDebug(int level,const char *fmt,...){}
private:
void initDriver();
bool startRecovery();
bool checkRecovery();
Status logStatus();
gpio_num_t TxPin;
gpio_num_t RxPin;
uint32_t txTimeouts=0;
GwIntervalRunner timers;
bool disabled=false;
unsigned long lastRecoveryStart=0;
};
#endif

View File

@ -40,12 +40,11 @@ class GwSerialStream: public Stream{
GwSerial::GwSerial(GwLog *logger, int num, int id,bool allowRead)
GwSerial::GwSerial(GwLog *logger, Stream *s, int id,bool allowRead):serial(s)
{
LOG_DEBUG(GwLog::DEBUG,"creating GwSerial %p port %d for %d",this,(int)num,id);
LOG_DEBUG(GwLog::DEBUG,"creating GwSerial %p id %d",this,id);
this->id=id;
this->logger = logger;
this->num = num;
String bufName="Ser(";
bufName+=String(id);
bufName+=")";
@ -54,21 +53,15 @@ GwSerial::GwSerial(GwLog *logger, int num, int id,bool allowRead)
if (allowRead){
this->readBuffer=new GwBuffer(logger, GwBuffer::RX_BUFFER_SIZE,bufName+"rd");
}
this->serial=new HardwareSerial(num);
buffer->reset("init");
initialized=true;
}
GwSerial::~GwSerial()
{
delete buffer;
if (readBuffer) delete readBuffer;
delete serial;
}
int GwSerial::setup(int baud, int rxpin, int txpin)
{
serial->begin(baud,SERIAL_8N1,rxpin,txpin);
buffer->reset(F("init"));
initialized = true;
return 0;
}
bool GwSerial::isInitialized() { return initialized; }
size_t GwSerial::enqueue(const uint8_t *data, size_t len, bool partial)
{

View File

@ -10,19 +10,17 @@ class GwSerial : public GwChannelInterface{
GwBuffer *buffer;
GwBuffer *readBuffer=NULL;
GwLog *logger;
int num;
bool initialized=false;
bool allowRead=true;
GwBuffer::WriteStatus write();
int id=-1;
int overflows=0;
size_t enqueue(const uint8_t *data, size_t len,bool partial=false);
HardwareSerial *serial;
Stream *serial;
public:
static const int bufferSize=200;
GwSerial(GwLog *logger,int num,int id,bool allowRead=true);
GwSerial(GwLog *logger,Stream *stream,int id,bool allowRead=true);
~GwSerial();
int setup(int baud,int rxpin,int txpin);
bool isInitialized();
virtual size_t sendToClients(const char *buf,int sourceId,bool partial=false);
virtual void loop(bool handleRead=true,bool handleWrite=true);

View File

@ -59,7 +59,7 @@ int GwSocketServer::available()
int client_sock;
struct sockaddr_in _client;
int cs = sizeof(struct sockaddr_in);
client_sock = lwip_accept_r(listener, (struct sockaddr *)&_client, (socklen_t *)&cs);
client_sock = accept(listener, (struct sockaddr *)&_client, (socklen_t *)&cs);
if (client_sock >= 0)
{
int val = 1;

View File

@ -80,7 +80,7 @@ void GwTcpClient::startConnection()
return;
}
fcntl( sockfd, F_SETFL, fcntl( sockfd, F_GETFL, 0 ) | O_NONBLOCK );
int res = lwip_connect_r(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
int res = connect(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
if (res < 0 ) {
if (errno != EINPROGRESS){
error=String("connect error ")+String(strerror(errno));
@ -258,7 +258,7 @@ void GwTcpClient::resolveHost(String host)
if (xTaskCreate([](void *p)
{
ResolveArgs *args = (ResolveArgs *)p;
struct ip4_addr addr;
esp_ip4_addr_t addr;
addr.addr = 0;
esp_err_t err = mdns_query_a(args->host.c_str(), args->timeout, &addr);
if (err)

48
lib/timer/GwTimer.h Normal file
View File

@ -0,0 +1,48 @@
#ifndef _GWTIMER_H
#define _GWTIMER_H
#include <vector>
#include <functional>
#include <Arduino.h>
class GwIntervalRunner{
public:
using RunFunction=std::function<void(void)>;
private:
class Run{
public:
unsigned long interval=0;
RunFunction runner;
unsigned long last=0;
Run(RunFunction r,unsigned long iv,unsigned long l=0):
runner(r),interval(iv),last(l){}
bool shouldRun(unsigned long now){
if ((last+interval) > now) return false;
last=now;
return true;
}
bool runIf(unsigned long now){
if (shouldRun(now)) {
runner();
return true;
}
return false;
}
};
std::vector<Run> runners;
unsigned long startTime=0;
public:
GwIntervalRunner(unsigned long now=millis()){
startTime=now;
}
void addAction(unsigned long interval,RunFunction run,unsigned long start=0){
if (start=0) start=startTime;
runners.push_back(Run(run,interval,start));
}
bool loop(unsigned long now=millis()){
bool rt=false;
for (auto it=runners.begin();it!=runners.end();it++){
if (it->runIf(now)) rt=true;
}
return rt;
}
};
#endif

View File

@ -1,8 +1,18 @@
#define DECLARE_USERTASK(task) GwUserTaskDef __##task##__(task,#task);
#define DECLARE_USERTASK_PARAM(task,...) GwUserTaskDef __##task##__(task,#task,__VA_ARGS__);
#define DECLARE_INITFUNCTION(task) GwInitTask __Init##task##__(task,#task);
#define DECLARE_CAPABILITY(name,value) GwUserCapability __CAP##name##__(#name,#value);
#define DECLARE_STRING_CAPABILITY(name,value) GwUserCapability __CAP##name##__(#name,value);
#define DECLARE_TASKIF(type) \
DECLARE_TASKIF_IMPL(type) \
GwIreg __register##type(__FILE__,#type)
#include "GwUserCode.h"
#include "GwSynchronized.h"
#include <Arduino.h>
#include <vector>
#include <map>
#include "GwCounter.h"
//user task handling
@ -11,6 +21,50 @@ std::vector<GwUserTask> userTasks;
std::vector<GwUserTask> initTasks;
GwUserCode::Capabilities userCapabilities;
template <typename V>
bool taskExists(V &list, const String &name){
for (auto it=list.begin();it!=list.end();it++){
if (it->name == name) return true;
}
return false;
}
class RegEntry{
public:
String file;
String task;
RegEntry(const String &t, const String &f):file(f),task(t){}
RegEntry(){}
};
using RegMap=std::map<String,RegEntry>;
static RegMap &registrations(){
static RegMap *regMap=new RegMap();
return *regMap;
}
static void registerInterface(const String &task,const String &file, const String &name){
auto it=registrations().find(name);
if (it != registrations().end()){
if (it->second.file != file){
ESP_LOGE("Assert","type %s redefined in %s original in %s",name,file,it->second.file);
std::abort();
};
if (it->second.task != task){
ESP_LOGE("Assert","type %s registered for multiple tasks %s and %s",name,task,it->second.task);
std::abort();
};
}
else{
registrations()[name]=RegEntry(task,file);
}
}
class GwIreg{
public:
GwIreg(const String &file, const String &name){
registerInterface("",file,name);
}
};
class GwUserTaskDef{
@ -38,25 +92,125 @@ class GwUserCapability{
userCapabilities[name]=value;
}
};
#define DECLARE_USERTASK(task) GwUserTaskDef __##task##__(task,#task);
#define DECLARE_USERTASK_PARAM(task,...) GwUserTaskDef __##task##__(task,#task,__VA_ARGS__);
#define DECLARE_INITFUNCTION(task) GwInitTask __Init##task##__(task,#task);
#define DECLARE_CAPABILITY(name,value) GwUserCapability __CAP##name##__(#name,#value);
#define DECLARE_STRING_CAPABILITY(name,value) GwUserCapability __CAP##name##__(#name,value);
#include "GwApi.h"
#define _NOGWHARDWAREUT
#include "GwUserTasks.h"
class TaskApi : public GwApi
#undef _NOGWHARDWAREUT
class TaskDataEntry{
public:
GwApi::TaskInterfaces::Ptr ptr;
int updates=0;
TaskDataEntry(GwApi::TaskInterfaces::Ptr p):ptr(p){}
TaskDataEntry(){}
};
class TaskInterfacesStorage{
GwLog *logger;
SemaphoreHandle_t lock;
std::map<String,TaskDataEntry> values;
public:
TaskInterfacesStorage(GwLog* l):
logger(l){
lock=xSemaphoreCreateMutex();
}
bool set(const String &file, const String &name, const String &task,GwApi::TaskInterfaces::Ptr v){
GWSYNCHRONIZED(&lock);
auto it=registrations().find(name);
if (it == registrations().end()){
LOG_DEBUG(GwLog::ERROR,"TaskInterfaces: invalid set %s not known",name.c_str());
return false;
}
if (it->second.file != file){
LOG_DEBUG(GwLog::ERROR,"TaskInterfaces: invalid set %s wrong file, expected %s , got %s",name.c_str(),it->second.file.c_str(),file.c_str());
return false;
}
if (it->second.task != task){
LOG_DEBUG(GwLog::ERROR,"TaskInterfaces: invalid set %s wrong task, expected %s , got %s",name.c_str(),it->second.task.c_str(),task.c_str());
return false;
}
auto vit=values.find(name);
if (vit != values.end()){
vit->second.updates++;
if (vit->second.updates < 0){
vit->second.updates=0;
}
vit->second.ptr=v;
}
else{
values[name]=TaskDataEntry(v);
}
return true;
}
GwApi::TaskInterfaces::Ptr get(const String &name, int &result){
GWSYNCHRONIZED(&lock);
auto it = values.find(name);
if (it == values.end())
{
result = -1;
return GwApi::TaskInterfaces::Ptr();
}
result = it->second.updates;
return it->second.ptr;
}
};
class TaskInterfacesImpl : public GwApi::TaskInterfaces{
String task;
TaskInterfacesStorage *storage;
GwLog *logger;
bool isInit=false;
public:
TaskInterfacesImpl(const String &n,TaskInterfacesStorage *s, GwLog *l,bool i):
task(n),storage(s),isInit(i),logger(l){}
virtual bool iset(const String &file, const String &name, Ptr v){
return storage->set(file,name,task,v);
}
virtual Ptr iget(const String &name, int &result){
return storage->get(name,result);
}
virtual bool iclaim(const String &name, const String &task){
if (! isInit) return false;
auto it=registrations().find(name);
if (it == registrations().end()){
LOG_DEBUG(GwLog::ERROR,"unable to claim interface %s for task %s, not registered",name.c_str(),task.c_str());
return false;
}
if (!it->second.task.isEmpty()){
LOG_DEBUG(GwLog::ERROR,"unable to claim interface %s for task %s, already claimed by %s",name.c_str(),task.c_str(),it->second.task.c_str());
return false;
}
it->second.task=task;
LOG_DEBUG(GwLog::LOG,"claimed interface %s for task %s",name.c_str(),task.c_str());
return true;
}
};
class TaskApi : public GwApiInternal
{
GwApi *api;
GwApiInternal *api=nullptr;
int sourceId;
SemaphoreHandle_t *mainLock;
SemaphoreHandle_t localLock;
std::map<int,GwCounter<String>> counter;
String name;
bool counterUsed=false;
int counterIdx=0;
TaskInterfacesImpl *interfaces;
bool isInit=false;
public:
TaskApi(GwApi *api, int sourceId, SemaphoreHandle_t *mainLock)
TaskApi(GwApiInternal *api,
int sourceId,
SemaphoreHandle_t *mainLock,
const String &name,
TaskInterfacesStorage *s,
bool init=false)
{
this->sourceId = sourceId;
this->api = api;
this->mainLock=mainLock;
this->name=name;
localLock=xSemaphoreCreateMutex();
interfaces=new TaskInterfacesImpl(name,s,api->getLogger(),init);
isInit=init;
}
virtual GwRequestQueue *getQueue()
{
@ -104,13 +258,91 @@ public:
GWSYNCHRONIZED(mainLock);
api->getStatus(status);
}
virtual ~TaskApi(){};
virtual ~TaskApi(){
delete interfaces;
vSemaphoreDelete(localLock);
};
virtual void fillStatus(GwJsonDocument &status){
GWSYNCHRONIZED(&localLock);
if (! counterUsed) return;
for (auto it=counter.begin();it != counter.end();it++){
it->second.toJson(status);
}
};
virtual int getJsonSize(){
GWSYNCHRONIZED(&localLock);
if (! counterUsed) return 0;
int rt=0;
for (auto it=counter.begin();it != counter.end();it++){
rt+=it->second.getJsonSize();
}
return rt;
};
virtual void increment(int idx,const String &name,bool failed=false){
GWSYNCHRONIZED(&localLock);
counterUsed=true;
auto it=counter.find(idx);
if (it == counter.end()) return;
if (failed) it->second.addFail(name);
else (it->second.add(name));
};
virtual void reset(int idx){
GWSYNCHRONIZED(&localLock);
counterUsed=true;
auto it=counter.find(idx);
if (it == counter.end()) return;
it->second.reset();
};
virtual void remove(int idx){
GWSYNCHRONIZED(&localLock);
counter.erase(idx);
}
virtual int addCounter(const String &name){
GWSYNCHRONIZED(&localLock);
counterUsed=true;
counterIdx++;
//avoid the need for an empty counter constructor
auto it=counter.find(counterIdx);
if (it == counter.end()){
counter.insert(std::make_pair(counterIdx,GwCounter<String>("count"+name)));
}
else it->second=GwCounter<String>("count"+name);
return counterIdx;
}
virtual TaskInterfaces * taskInterfaces(){
return interfaces;
}
virtual bool addXdrMapping(const GwXDRMappingDef &def){
return api->addXdrMapping(def);
}
virtual void addCapability(const String &name, const String &value){
if (! isInit) return;
userCapabilities[name]=value;
}
virtual bool addUserTask(GwUserTaskFunction task,const String tname, int stackSize=2000){
if (! isInit){
api->getLogger()->logDebug(GwLog::ERROR,"trying to add a user task %s outside init",tname.c_str());
return false;
}
if (taskExists(userTasks,name)){
api->getLogger()->logDebug(GwLog::ERROR,"trying to add a user task %s that already exists",tname.c_str());
return false;
}
userTasks.push_back(GwUserTask(tname,task,stackSize));
api->getLogger()->logDebug(GwLog::LOG,"adding user task %s",tname.c_str());
return true;
}
};
GwUserCode::GwUserCode(GwApi *api,SemaphoreHandle_t *mainLock){
GwUserCode::GwUserCode(GwApiInternal *api,SemaphoreHandle_t *mainLock){
this->logger=api->getLogger();
this->api=api;
this->mainLock=mainLock;
this->taskData=new TaskInterfacesStorage(this->logger);
}
GwUserCode::~GwUserCode(){
delete taskData;
}
void userTaskStart(void *p){
GwUserTask *task=(GwUserTask*)p;
@ -123,8 +355,8 @@ void userTaskStart(void *p){
delete task->api;
task->api=NULL;
}
void GwUserCode::startAddOnTask(GwApi *api,GwUserTask *task,int sourceId,String name){
task->api=new TaskApi(api,sourceId,mainLock);
void GwUserCode::startAddOnTask(GwApiInternal *api,GwUserTask *task,int sourceId,String name){
task->api=new TaskApi(api,sourceId,mainLock,name,taskData);
xTaskCreate(userTaskStart,name.c_str(),task->stackSize,task,3,NULL);
}
void GwUserCode::startUserTasks(int baseId){
@ -139,7 +371,7 @@ void GwUserCode::startInitTasks(int baseId){
LOG_DEBUG(GwLog::DEBUG,"starting %d user init tasks",initTasks.size());
for (auto it=initTasks.begin();it != initTasks.end();it++){
LOG_DEBUG(GwLog::LOG,"starting user init task %s with id %d",it->name.c_str(),baseId);
it->api=new TaskApi(api,baseId,mainLock);
it->api=new TaskApi(api,baseId,mainLock,it->name,taskData,true);
userTaskStart(&(*it));
baseId++;
}
@ -152,4 +384,21 @@ void GwUserCode::startAddonTask(String name, TaskFunction_t task, int id){
GwUserCode::Capabilities * GwUserCode::getCapabilities(){
return &userCapabilities;
}
void GwUserCode::fillStatus(GwJsonDocument &status){
for (auto it=userTasks.begin();it != userTasks.end();it++){
if (it->api){
it->api->fillStatus(status);
}
}
}
int GwUserCode::getJsonSize(){
int rt=0;
for (auto it=userTasks.begin();it != userTasks.end();it++){
if (it->api){
rt+=it->api->getJsonSize();
}
}
return rt;
}

View File

@ -2,16 +2,23 @@
#define _GWUSERCODE_H
#include <Arduino.h>
#include <map>
#include "GwApi.h"
#include "GwJsonDocument.h"
class GwLog;
class GwApi;
typedef void (*GwUserTaskFunction)(GwApi *);
class GwApiInternal : public GwApi{
public:
~GwApiInternal(){}
virtual void fillStatus(GwJsonDocument &status){};
virtual int getJsonSize(){return 0;};
};
class GwUserTask{
public:
String name;
TaskFunction_t task=NULL;
GwUserTaskFunction usertask=NULL;
bool isUserTask=false;
GwApi *api=NULL;
GwApiInternal *api=NULL;
int stackSize=2000;
GwUserTask(String name,TaskFunction_t task,int stackSize=2000){
this->name=name;
@ -25,17 +32,23 @@ class GwUserTask{
this->stackSize=stackSize;
}
};
class TaskInterfacesStorage;
class GwUserCode{
GwLog *logger;
GwApi *api;
GwApiInternal *api;
SemaphoreHandle_t *mainLock;
void startAddOnTask(GwApi *api,GwUserTask *task,int sourceId,String name);
TaskInterfacesStorage *taskData;
void startAddOnTask(GwApiInternal *api,GwUserTask *task,int sourceId,String name);
public:
~GwUserCode();
typedef std::map<String,String> Capabilities;
GwUserCode(GwApi *api, SemaphoreHandle_t *mainLock);
GwUserCode(GwApiInternal *api, SemaphoreHandle_t *mainLock);
void startUserTasks(int baseId);
void startInitTasks(int baseId);
void startAddonTask(String name,TaskFunction_t task, int id);
Capabilities *getCapabilities();
void fillStatus(GwJsonDocument &status);
int getJsonSize();
};
#endif

View File

@ -1,4 +1,5 @@
#include "GwXDRMappings.h"
#include "GWConfig.h"
#include "N2kMessages.h"
double PtoBar(double v)
@ -55,20 +56,19 @@ GwXDRType *types[] = {
new GwXDRType(GwXDRType::GENERIC, "G", ""),
new GwXDRType(GwXDRType::DISPLACEMENT, "A", "P"),
new GwXDRType(GwXDRType::DISPLACEMENTD, "A", "D",DegToRad,RadToDeg,"rd"),
new GwXDRType(GwXDRType::RPM,"T","R"),
//important to have 2x NULL!
NULL,
NULL};
new GwXDRType(GwXDRType::RPM,"T","R")
};
template<typename T, int size>
int GetArrLength(T(&)[size]){return size;}
static GwXDRType *findType(GwXDRType::TypeCode type, int *start = NULL)
{
int len=GetArrLength(types);
int from = 0;
if (start != NULL)
from = *start;
if (types[from] == NULL)
return NULL;
if (from < 0 || from >= len) return NULL;
int i = from;
for (; types[i] != NULL; i++)
for (; i< len; i++)
{
if (types[i]->code == type)
{
@ -97,7 +97,7 @@ static GwXDRType::TypeCode findTypeMapping(GwXDRCategory category, int field)
return GwXDRType::UNKNOWN;
}
//category,direction,selector,field,instanceMode,instance,name
String GwXDRMappingDef::toString()
String GwXDRMappingDef::toString() const
{
String rt = "";
rt += String((int)category);
@ -234,6 +234,82 @@ GwXDRMappings::GwXDRMappings(GwLog *logger, GwConfigHandler *config)
this->logger = logger;
this->config = config;
}
bool GwXDRMappings::addFixedMapping(const GwXDRMappingDef &mapping){
GwXDRMappingDef *nm=new GwXDRMappingDef(mapping);
bool res=addMapping(nm);
if (! res){
LOG_DEBUG(GwLog::ERROR,"unable to add fixed mapping %s",mapping.toString().c_str());
return false;
}
return true;
}
bool GwXDRMappings::addMapping(GwXDRMappingDef *def)
{
if (def)
{
int typeIndex = 0;
LOG_DEBUG(GwLog::LOG, "add xdr mapping %s",
def->toString().c_str());
// n2k: find first matching type mapping
GwXDRType::TypeCode code = findTypeMapping(def->category, def->field);
if (code == GwXDRType::UNKNOWN)
{
LOG_DEBUG(GwLog::ERROR, "no type mapping for %s", def->toString().c_str());
return false;
}
GwXDRType *type = findType(code, &typeIndex);
if (!type)
{
LOG_DEBUG(GwLog::ERROR, "no type definition for %s", def->toString().c_str());
return false;
}
long n2kkey = def->n2kKey();
auto it = n2kMap.find(n2kkey);
GwXDRMapping *mapping = new GwXDRMapping(def, type);
if (it == n2kMap.end())
{
LOG_DEBUG(GwLog::LOG, "insert mapping with key %ld", n2kkey);
GwXDRMapping::MappingList mappings;
mappings.push_back(mapping);
n2kMap[n2kkey] = mappings;
}
else
{
LOG_DEBUG(GwLog::LOG, "append mapping with key %ld", n2kkey);
it->second.push_back(mapping);
}
// for nmea0183 there could be multiple entries
// as potentially there are different units that we can handle
// so after we inserted the definition we do additional type lookups
while (type != NULL)
{
String n183key = GwXDRMappingDef::n183key(def->xdrName,
type->xdrtype, type->xdrunit);
auto it = n183Map.find(n183key);
if (it == n183Map.end())
{
LOG_DEBUG(GwLog::LOG, "insert mapping with n183key %s", n183key.c_str());
GwXDRMapping::MappingList mappings;
mappings.push_back(mapping);
n183Map[n183key] = mappings;
}
else
{
LOG_DEBUG(GwLog::LOG, "append mapping with n183key %s", n183key.c_str());
it->second.push_back(mapping);
}
type = findType(code, &typeIndex);
if (!type)
break;
mapping = new GwXDRMapping(def, type);
}
return true;
}
else
{
return false;
}
}
#define MAX_MAPPINGS 100
void GwXDRMappings::begin()
@ -259,61 +335,10 @@ void GwXDRMappings::begin()
GwXDRMappingDef *def = GwXDRMappingDef::fromString(cfg->asCString());
if (def)
{
int typeIndex = 0;
LOG_DEBUG(GwLog::DEBUG, "read xdr mapping %s from %s",
def->toString().c_str(),namebuf);
//n2k: find first matching type mapping
GwXDRType::TypeCode code = findTypeMapping(def->category, def->field);
if (code == GwXDRType::UNKNOWN)
{
LOG_DEBUG(GwLog::DEBUG, "no type mapping for %s", def->toString().c_str());
continue;
}
GwXDRType *type = findType(code, &typeIndex);
if (!type)
{
LOG_DEBUG(GwLog::DEBUG, "no type definition for %s", def->toString().c_str());
continue;
}
long n2kkey = def->n2kKey();
auto it = n2kMap.find(n2kkey);
GwXDRMapping *mapping = new GwXDRMapping(def, type);
if (it == n2kMap.end())
{
LOG_DEBUG(GwLog::DEBUG, "insert mapping with key %ld", n2kkey);
GwXDRMapping::MappingList mappings;
mappings.push_back(mapping);
n2kMap[n2kkey] = mappings;
}
else
{
LOG_DEBUG(GwLog::DEBUG, "append mapping with key %ld", n2kkey);
it->second.push_back(mapping);
}
//for nmea0183 there could be multiple entries
//as potentially there are different units that we can handle
//so after we inserted the definition we do additional type lookups
while (type != NULL)
{
String n183key = GwXDRMappingDef::n183key(def->xdrName,
type->xdrtype, type->xdrunit);
auto it = n183Map.find(n183key);
if (it == n183Map.end())
{
LOG_DEBUG(GwLog::DEBUG, "insert mapping with n183key %s", n183key.c_str());
GwXDRMapping::MappingList mappings;
mappings.push_back(mapping);
n183Map[n183key] = mappings;
}
else
{
LOG_DEBUG(GwLog::DEBUG, "append mapping with n183key %s", n183key.c_str());
it->second.push_back(mapping);
}
type = findType(code, &typeIndex);
if (!type)
break;
mapping=new GwXDRMapping(def,type);
bool res=addMapping(def);
if (! res){
LOG_DEBUG(GwLog::ERROR,"unable to add mapping from %s",cfg);
delete cfg;
}
}
else{

View File

@ -1,7 +1,6 @@
#ifndef _GWXDRMAPPINGS_H
#define _GWXDRMAPPINGS_H
#include "GwLog.h"
#include "GWConfig.h"
#include "GwBoatData.h"
#include <WString.h>
#include <vector>
@ -115,7 +114,7 @@ class GwXDRMappingDef{
category=XDRTEMP;
}
//category,direction,selector,field,instanceMode,instance,name
String toString();
String toString() const;
static GwXDRMappingDef *fromString(String s);
//we allow 100 entities of code,selector and field nid
static unsigned long n2kKey(GwXDRCategory category, int selector, int field)
@ -200,6 +199,7 @@ class GwXDRFoundMapping : public GwBoatItemNameProvider{
//the class GwXDRMappings is not intended to be deleted
//the deletion will leave memory leaks!
class GwConfigHandler;
class GwXDRMappings{
static const int MAX_UNKNOWN=200;
static const int ESIZE=13;
@ -212,8 +212,10 @@ class GwXDRMappings{
char *unknowAsString=NULL;
GwXDRFoundMapping selectMapping(GwXDRMapping::MappingList *list,int instance,const char * key);
bool addUnknown(GwXDRCategory category,int selector,int field=0,int instance=-1);
bool addMapping(GwXDRMappingDef *mapping);
public:
GwXDRMappings(GwLog *logger,GwConfigHandler *config);
bool addFixedMapping(const GwXDRMappingDef &mapping);
void begin();
//get the mappings
//the returned mapping will exactly contain one mapping def

View File

@ -17,14 +17,25 @@ extra_configs=
lib/*task*/platformio.ini
[env]
platform = espressif32 @ 3.4.0
platform = espressif32 @ 6.3.2
framework = arduino
;platform_packages=
; framework-arduinoespressif32 @ 3.20011.230801
; framework-espidf @ 3.50101.0
lib_deps =
ttlappalainen/NMEA2000-library @ 4.17.2
ttlappalainen/NMEA0183 @ 1.7.1
ttlappalainen/NMEA2000-library @ 4.18.9
ttlappalainen/NMEA0183 @ 1.9.1
ArduinoJson @ 6.18.5
ottowinter/ESPAsyncWebServer-esphome@2.0.1
fastled/FastLED @ 3.4.0
fastled/FastLED @ 3.6.0
FS
Preferences
ESPmDNS
WiFi
Update
board_build.embed_files =
lib/generated/index.html.gz
lib/generated/index.js.gz
@ -36,11 +47,21 @@ board_build.partitions = partitions_custom.csv
extra_scripts =
pre:extra_script.py
post:post.py
lib_ldf_mode = chain+
lib_ldf_mode = off
#lib_ldf_mode = chain+
monitor_speed = 115200
build_flags =
-D PIO_ENV_BUILD=$PIOENV
[sensors]
; collect the libraries for sensors here
lib_deps =
Wire
SPI
adafruit/Adafruit BME280 Library @ 2.2.2
adafruit/Adafruit BusIO @ 1.14.5
adafruit/Adafruit Unified Sensor @ 1.1.13
[env:m5stack-atom]
board = m5stack-atom
lib_deps = ${env.lib_deps}
@ -50,6 +71,37 @@ build_flags =
upload_port = /dev/esp32
upload_protocol = esptool
[env:m5stack-atom-generic]
extends = sensors
board = m5stack-atom
lib_deps =
${env.lib_deps}
${sensors.lib_deps}
build_flags =
${env.build_flags}
upload_port = /dev/esp32
upload_protocol = esptool
[env:m5stack-atoms3]
board = m5stack-atoms3
lib_deps = ${env.lib_deps}
build_flags =
-D BOARD_M5ATOMS3
${env.build_flags}
upload_port = /dev/esp32s3
upload_protocol = esptool
[env:m5stack-atoms3-generic]
extends = sensors
board = m5stack-atoms3
lib_deps =
${env.lib_deps}
${sensors.lib_deps}
build_flags =
${env.build_flags}
upload_port = /dev/esp32s3
upload_protocol = esptool
[env:m5stack-atom-canunit]
board = m5stack-atom
lib_deps = ${env.lib_deps}
@ -59,6 +111,16 @@ build_flags =
upload_port = /dev/esp32
upload_protocol = esptool
[env:m5stack-atoms3-canunit]
board = m5stack-atoms3
lib_deps = ${env.lib_deps}
build_flags =
-D BOARD_M5ATOMS3_CANUNIT
${env.build_flags}
upload_port = /dev/esp32s3
upload_protocol = esptool
[env:m5stack-atom-rs232-canunit]
board = m5stack-atom
lib_deps = ${env.lib_deps}
@ -87,6 +149,18 @@ build_flags =
upload_port = /dev/esp32
upload_protocol = esptool
[env:m5stickc-atom-generic]
extends = sensors
board = m5stick-c
lib_deps =
${env.lib_deps}
${sensors.lib_deps}
build_flags =
-D BOARD_M5STICK -D HAS_RTC -D HAS_M5LCD
${env.build_flags}
upload_port = /dev/esp32
upload_protocol = esptool
[env:nodemcu-homberger]
board = nodemcu-32s
lib_deps = ${env.lib_deps}
@ -94,4 +168,15 @@ build_flags =
-D BOARD_HOMBERGER
${env.build_flags}
upload_port = /dev/esp32
upload_protocol = esptool
upload_protocol = esptool
[env:nodemcu-generic]
extends = sensors
board = nodemcu-32s
lib_deps =
${env.lib_deps}
${sensors.lib_deps}
build_flags =
${env.build_flags}
upload_port = /dev/esp32
upload_protocol = esptool

19
post.py
View File

@ -42,11 +42,16 @@ def post(source,target,env):
print("found fwname=%s, fwversion=%s"%(fwname,version))
python=env.subst("$PYTHONEXE")
print("base=%s,esptool=%s,appoffset=%s,uploaderflags=%s"%(base,esptool,appoffset,uploaderflags))
chip="esp32"
uploadparts=uploaderflags.split(" ")
#currently hardcoded last 8 parameters...
if len(uploadparts) < 6:
print("uploaderflags does not have enough parameter")
return
for i in range(0,len(uploadparts)):
if uploadparts[i]=="--chip":
if i < (len(uploadparts) -1):
chip=uploadparts[i+1]
uploadfiles=uploadparts[-6:]
for i in range(1,len(uploadfiles),2):
if not os.path.isfile(uploadfiles[i]):
@ -58,17 +63,17 @@ def post(source,target,env):
for f in glob.glob(os.path.join(outdir,base+"*.bin")):
print("removing old file %s"%f)
os.unlink(f)
ofversion=''
if not version.startswith('dev'):
ofversion="-"+version
versionedFile=os.path.join(outdir,"%s%s-update.bin"%(base,ofversion))
shutil.copyfile(firmware,versionedFile)
outfile=os.path.join(outdir,"%s%s-all.bin"%(base,ofversion))
cmd=[python,esptool,"--chip","esp32","merge_bin","--target-offset",offset,"-o",outfile]
outfile=os.path.join(outdir,"%s-all.bin"%(base))
cmd=[python,esptool,"--chip",chip,"merge_bin","--target-offset",offset,"-o",outfile]
cmd+=uploadfiles
cmd+=[appoffset,firmware]
print("running %s"%" ".join(cmd))
env.Execute(" ".join(cmd),"#testpost")
ofversion="-"+version
versionedFile=os.path.join(outdir,"%s%s-update.bin"%(base,ofversion))
shutil.copyfile(firmware,versionedFile)
versioneOutFile=os.path.join(outdir,"%s%s-all.bin"%(base,ofversion))
shutil.copyfile(outfile,versioneOutFile)
env.AddPostAction(
"$BUILD_DIR/${PROGNAME}.bin",
post

View File

@ -14,9 +14,10 @@
#include "GwAppInfo.h"
// #define GW_MESSAGE_DEBUG_ENABLED
//#define FALLBACK_SERIAL
//#define CAN_ESP_DEBUG
#define OWN_LOOP
const unsigned long HEAP_REPORT_TIME=2000; //set to 0 to disable heap reporting
#include <Arduino.h>
#include "Preferences.h"
#include "GwApi.h"
#include "GwHardware.h"
@ -54,8 +55,6 @@ const unsigned long HEAP_REPORT_TIME=2000; //set to 0 to disable heap reporting
#include "GwSerial.h"
#include "GwWebServer.h"
#include "NMEA0183DataToN2K.h"
#include "GwButtons.h"
#include "GwLeds.h"
#include "GwCounter.h"
#include "GwXDRMappings.h"
#include "GwSynchronized.h"
@ -65,18 +64,7 @@ const unsigned long HEAP_REPORT_TIME=2000; //set to 0 to disable heap reporting
#include "GwTcpClient.h"
#include "GwChannel.h"
#include "GwChannelList.h"
#include <NMEA2000_esp32.h> // forked from https://github.com/ttlappalainen/NMEA2000_esp32
#ifdef FALLBACK_SERIAL
#ifdef CAN_ESP_DEBUG
#define CDBS &Serial
#else
#define CDBS NULL
#endif
tNMEA2000 &NMEA2000=*(new tNMEA2000_esp32(ESP32_CAN_TX_PIN,ESP32_CAN_RX_PIN,CDBS));
#else
tNMEA2000 &NMEA2000=*(new tNMEA2000_esp32());
#endif
#include "GwTimer.h"
#define MAX_NMEA2000_MESSAGE_SEASMART_SIZE 500
@ -88,6 +76,7 @@ const unsigned long HEAP_REPORT_TIME=2000; //set to 0 to disable heap reporting
//assert length of firmware name and version
CASSERT(strlen(FIRMWARE_TYPE) <= 32, "environment name (FIRMWARE_TYPE) must not exceed 32 chars");
CASSERT(strlen(VERSION) <= 32, "VERSION must not exceed 32 chars");
CASSERT(strlen(IDF_VERSION) <= 32,"IDF_VERSION must not exceed 32 chars");
//https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/app_image_format.html
//and removed the bugs in the doc...
__attribute__((section(".rodata_custom_desc"))) esp_app_desc_t custom_app_desc = {
@ -98,7 +87,7 @@ __attribute__((section(".rodata_custom_desc"))) esp_app_desc_t custom_app_desc =
FIRMWARE_TYPE,
"00:00:00",
"2021/12/13",
"0000",
IDF_VERSION,
{},
{}
};
@ -111,6 +100,35 @@ typedef std::map<String,String> StringMap;
GwLog logger(LOGLEVEL,NULL);
GwConfigHandler config(&logger);
#include "Nmea2kTwai.h"
static const unsigned long CAN_RECOVERY_PERIOD=3000; //ms
static const unsigned long NMEA2000_HEARTBEAT_INTERVAL=5000;
class Nmea2kTwaiLog : public Nmea2kTwai{
private:
GwLog* logger;
public:
Nmea2kTwaiLog(gpio_num_t _TxPin, gpio_num_t _RxPin, unsigned long recoveryPeriod,GwLog *l):
Nmea2kTwai(_TxPin,_RxPin,recoveryPeriod,recoveryPeriod),logger(l){}
virtual void logDebug(int level, const char *fmt,...){
va_list args;
va_start(args,fmt);
if (level > 2) level++; //error+info+debug are similar, map msg to 4
logger->logDebug(level,fmt,args);
}
};
#ifndef ESP32_CAN_TX_PIN
#pragma message "WARNING: ESP32_CAN_TX_PIN not defined"
#define ESP32_CAN_TX_PIN GPIO_NUM_NC
#endif
#ifndef ESP32_CAN_RX_PIN
#pragma message "WARNING: ESP32_CAN_RX_PIN not defined"
#define ESP32_CAN_RX_PIN GPIO_NUM_NC
#endif
Nmea2kTwai &NMEA2000=*(new Nmea2kTwaiLog((gpio_num_t)ESP32_CAN_TX_PIN,(gpio_num_t)ESP32_CAN_RX_PIN,CAN_RECOVERY_PERIOD,&logger));
#ifdef GWBUTTON_PIN
bool fixedApPass=false;
#else
@ -138,9 +156,9 @@ SemaphoreHandle_t mainLock;
GwRequestQueue mainQueue(&logger,20);
GwWebServer webserver(&logger,&mainQueue,80);
GwCounter<unsigned long> countNMEA2KIn("count2Kin");
GwCounter<unsigned long> countNMEA2KOut("count2Kout");
GwCounter<unsigned long> countNMEA2KIn("countNMEA2000in");
GwCounter<unsigned long> countNMEA2KOut("countNMEA2000out");
GwIntervalRunner timers;
bool checkPass(String hash){
return config.checkPass(hash);
@ -187,7 +205,6 @@ void handleN2kMessage(const tN2kMsg &n2kMsg,int sourceId, bool isConverted=false
nmea0183Converter->HandleMsg(n2kMsg,sourceId);
}
if (sourceId != N2K_CHANNEL_ID && sendOutN2k){
countNMEA2KOut.add(n2kMsg.PGN);
if (NMEA2000.SendMsg(n2kMsg)){
countNMEA2KOut.add(n2kMsg.PGN);
}
@ -217,7 +234,7 @@ void SendNMEA0183Message(const tNMEA0183Msg &NMEA0183Msg, int sourceId,bool conv
});
}
class ApiImpl : public GwApi
class ApiImpl : public GwApiInternal
{
private:
int sourceId = -1;
@ -299,9 +316,21 @@ public:
return &boatData;
}
virtual const char* getTalkerId(){
return config.getString(config.talkerId,String("GP")).c_str();
return config.getCString(config.talkerId,"GP");
}
virtual ~ApiImpl(){}
virtual TaskInterfaces *taskInterfaces(){ return nullptr;}
virtual bool addXdrMapping(const GwXDRMappingDef &mapping){
if (! config.userChangesAllowed()){
logger.logDebug(GwLog::ERROR,"trying to add an XDR mapping %s after the init phase",mapping.toString().c_str());
return false;
}
return xdrMappings.addFixedMapping(mapping);
}
virtual void addCapability(const String &name, const String &value){}
virtual bool addUserTask(GwUserTaskFunction task,const String Name, int stackSize=2000){
return false;
}
};
bool delayedRestart(){
@ -353,10 +382,11 @@ public:
protected:
virtual void processRequest()
{
GwJsonDocument status(256 +
GwJsonDocument status(300 +
countNMEA2KIn.getJsonSize()+
countNMEA2KOut.getJsonSize() +
channels.getJsonSize()
channels.getJsonSize()+
userCodeHandler.getJsonSize()
);
status["version"] = VERSION;
status["wifiConnected"] = gwWifi.clientConnected();
@ -368,11 +398,27 @@ protected:
GwConfigHandler::toHex(base,buffer,bsize);
status["salt"] = buffer;
status["fwtype"]= firmwareType;
status["chipid"]=CONFIG_IDF_FIRMWARE_CHIP_ID;
status["heap"]=(long)xPortGetFreeHeapSize();
Nmea2kTwai::Status n2kState=NMEA2000.getStatus();
Nmea2kTwai::STATE driverState=n2kState.state;
if (driverState == Nmea2kTwai::ST_RUNNING){
unsigned long lastRec=NMEA2000.getLastRecoveryStart();
if (lastRec > 0 && (lastRec+NMEA2000_HEARTBEAT_INTERVAL*2) > millis()){
//we still report bus off at least for 2 heartbeat intervals
//this avoids always reporting BUS_OFF-RUNNING-BUS_OFF if the bus off condition
//remains
driverState=Nmea2kTwai::ST_BUS_OFF;
}
}
status["n2kstate"]=NMEA2000.stateStr(driverState);
status["n2knode"]=NodeAddress;
status["minUser"]=MIN_USER_TASK;
//nmea0183Converter->toJson(status);
countNMEA2KIn.toJson(status);
countNMEA2KOut.toJson(status);
channels.toJson(status);
userCodeHandler.fillStatus(status);
serializeJson(status, result);
}
};
@ -397,17 +443,23 @@ class CapabilitiesRequest : public GwRequestMessage{
protected:
virtual void processRequest(){
int numCapabilities=userCodeHandler.getCapabilities()->size();
GwJsonDocument json(JSON_OBJECT_SIZE(numCapabilities*3+6));
int numSpecial=config.numSpecial();
logger.logDebug(GwLog::LOG,"capabilities user=%d, config=%d",numCapabilities,numSpecial);
GwJsonDocument json(JSON_OBJECT_SIZE(numCapabilities*3+numSpecial*2+8));
for (auto it=userCodeHandler.getCapabilities()->begin();
it != userCodeHandler.getCapabilities()->end();it++){
json[it->first]=it->second;
}
#ifdef GWSERIAL_MODE
String serial(F(GWSERIAL_MODE));
#else
String serial(F("NONE"));
#endif
json["serialmode"]=serial;
std::vector<String> specialCfg=config.getSpecial();
for (auto it=specialCfg.begin();it != specialCfg.end();it++){
GwConfigInterface *cfg=config.getConfigItem(*it);
if (cfg){
logger.logDebug(GwLog::LOG,"config mode %s=%d",it->c_str(),(int)(cfg->getType()));
json["CFGMODE"+*it]=(int)cfg->getType();
}
}
json["serialmode"]=channels.getMode(SERIAL1_CHANNEL_ID);
json["serial2mode"]=channels.getMode(SERIAL2_CHANNEL_ID);
#ifdef GWBUTTON_PIN
json["hardwareReset"]="true";
#endif
@ -656,7 +708,37 @@ void handleConfigRequestData(AsyncWebServerRequest *request, uint8_t *data, size
}
}
TimeMonitor monitor(20,0.2);
class DefaultLogWriter: public GwLogWriter{
public:
virtual ~DefaultLogWriter(){};
virtual void write(const char *data){
USBSerial.print(data);
}
};
void loopRun();
void loop(){
#ifdef OWN_LOOP
vTaskDelete(NULL);
return;
#else
loopRun();
#endif
}
void loopFunction(void *){
while (true){
loopRun();
//we don not call the serialEvent stuff as in the original
//main loop as this could cause some sort of a deadlock
//if serial writing or reading is done in a different thread
//and it remains inside some read/write routine with the uart being
//locked
//if(Serial1.available()) {}
//if(Serial.available()) {}
//if(Serial2.available()) {}
//delay(1);
}
}
void setup() {
mainLock=xSemaphoreCreateMutex();
uint8_t chipid[6];
@ -668,9 +750,10 @@ void setup() {
#ifdef FALLBACK_SERIAL
fallbackSerial=true;
//falling back to old style serial for logging
Serial.begin(115200);
Serial.printf("fallback serial enabled\n");
USBSerial.begin(115200);
USBSerial.printf("fallback serial enabled\n");
logger.prefix="FALLBACK:";
logger.setWriter(new DefaultLogWriter());
#endif
userCodeHandler.startInitTasks(MIN_USER_TASK);
config.stopChanges();
@ -780,6 +863,7 @@ void setup() {
logger.flush();
NMEA2000.SetMode(tNMEA2000::N2km_ListenAndNode, NodeAddress);
NMEA2000.SetForwardOwnMessages(false);
NMEA2000.SetHeartbeatInterval(NMEA2000_HEARTBEAT_INTERVAL);
if (sendOutN2k){
// Set the information for other bus devices, which messages we support
unsigned long *pgns=toN2KConverter->handledPgns();
@ -800,16 +884,26 @@ void setup() {
NMEA2000.Open();
logger.logDebug(GwLog::LOG,"starting addon tasks");
logger.flush();
userCodeHandler.startAddonTask(F("handleButtons"),handleButtons,100);
setLedMode(LED_GREEN);
userCodeHandler.startAddonTask(F("handleLeds"),handleLeds,101);
{
GWSYNCHRONIZED(&mainLock);
userCodeHandler.startUserTasks(MIN_USER_TASK);
}
timers.addAction(HEAP_REPORT_TIME,[](){
if (logger.isActive(GwLog::DEBUG)){
logger.logDebug(GwLog::DEBUG,"Heap free=%ld, minFree=%ld",
(long)xPortGetFreeHeapSize(),
(long)xPortGetMinimumEverFreeHeapSize()
);
logger.logDebug(GwLog::DEBUG,"Main loop %s",monitor.getLog().c_str());
}
});
logger.logString("wifi AP pass: %s",fixedApPass? gwWifi.AP_password:config.getString(config.apPassword).c_str());
logger.logString("admin pass: %s",config.getString(config.adminPassword).c_str());
logger.logDebug(GwLog::LOG,"setup done");
#ifdef OWN_LOOP
logger.logDebug(GwLog::LOG,"starting own main loop");
xTaskCreateUniversal(loopFunction,"loop",8192,NULL,1,NULL,ARDUINO_RUNNING_CORE);
#endif
}
//*****************************************************************************
void handleSendAndRead(bool handleRead){
@ -818,9 +912,8 @@ void handleSendAndRead(bool handleRead){
});
}
TimeMonitor monitor(20,0.2);
unsigned long lastHeapReport=0;
void loop() {
void loopRun() {
//logger.logDebug(GwLog::DEBUG,"main loop start");
monitor.reset();
GWSYNCHRONIZED(&mainLock);
logger.flush();
@ -828,29 +921,22 @@ void loop() {
gwWifi.loop();
unsigned long now=millis();
monitor.setTime(2);
if (HEAP_REPORT_TIME > 0 && now > (lastHeapReport+HEAP_REPORT_TIME)){
lastHeapReport=now;
if (logger.isActive(GwLog::DEBUG)){
logger.logDebug(GwLog::DEBUG,"Heap free=%ld, minFree=%ld",
(long)xPortGetFreeHeapSize(),
(long)xPortGetMinimumEverFreeHeapSize()
);
logger.logDebug(GwLog::DEBUG,"Main loop %s",monitor.getLog().c_str());
}
}
timers.loop();
monitor.setTime(3);
NMEA2000.loop();
monitor.setTime(4);
channels.allChannels([](GwChannel *c){
c->loop(true,false);
});
//reads
monitor.setTime(4);
monitor.setTime(5);
channels.allChannels([](GwChannel *c){
c->loop(false,true);
});
//writes
monitor.setTime(5);
monitor.setTime(6);
NMEA2000.ParseMessages();
monitor.setTime(6);
monitor.setTime(7);
int SourceAddress = NMEA2000.GetN2kSource();
if (SourceAddress != NodeAddress) { // Save potentially changed Source Address to NVS memory
@ -861,7 +947,7 @@ void loop() {
logger.logDebug(GwLog::LOG,"Address Change: New Address=%d\n", SourceAddress);
}
nmea0183Converter->loop();
monitor.setTime(7);
monitor.setTime(8);
//read channels
channels.allChannels([](GwChannel *c){
@ -888,13 +974,13 @@ void loop() {
}
});
});
monitor.setTime(8);
monitor.setTime(9);
channels.allChannels([](GwChannel *c){
c->parseActisense([](const tN2kMsg &msg,int source){
handleN2kMessage(msg,source);
});
});
monitor.setTime(9);
monitor.setTime(10);
//handle message requests
GwMessage *msg=mainQueue.fetchMessage(0);
@ -902,5 +988,7 @@ void loop() {
msg->process();
msg->unref();
}
monitor.setTime(10);
monitor.setTime(11);
//logger.logDebug(GwLog::DEBUG,"main loop end");
}

View File

@ -1 +1,3 @@
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", SYMLINK+="esp32"
SUBSYSTEM=="tty", ATTRS{idVendor}=="303a", ATTRS{idProduct}=="1001", SYMLINK+="esp32s3"

Binary file not shown.

129
tools/flashtool/flasher.py Normal file
View File

@ -0,0 +1,129 @@
try:
import esptool
except:
import flashtool.esptool as esptool
import os
VERSION="2.1"
class Flasher():
def getVersion(self):
return ("Version %s, esptool %s"%(VERSION,str(esptool.__version__)))
UPDATE_ADDR = 0x10000
HDROFFSET = 288
VERSIONOFFSET = 16
NAMEOFFSET = 48
IDOFFSET=12 #2 byte chipid
MINSIZE = HDROFFSET + NAMEOFFSET + 32
CHECKBYTES = {
288: 0x32, # app header magic
289: 0x54,
290: 0xcd,
291: 0xab
}
#flash addresses for full images based on chip id
FLASH_ADDR={
0: 0x1000,
9: 0
}
def getString(self,buffer, offset, len):
return buffer[offset:offset + len].rstrip(b'\0').decode('utf-8')
def getFirmwareInfo(self,filename,isFull):
with open(filename,"rb") as ih:
buffer = ih.read(self.MINSIZE)
if len(buffer) != self.MINSIZE:
return self.setErr("invalid image file %s, to short"%filename)
if buffer[0] != 0xe9:
return self.setErr("invalid magic in file, expected 0xe9 got 0x%02x"%buffer[0])
chipid= buffer[self.IDOFFSET]+256*buffer[self.IDOFFSET+1]
flashoffset=self.FLASH_ADDR.get(chipid)
if flashoffset is None:
return self.setErr("unknown chip id in image %d",chipid);
if isFull:
offset=self.UPDATE_ADDR-flashoffset;
offset-=self.MINSIZE
ih.seek(offset,os.SEEK_CUR)
buffer=ih.read(self.MINSIZE)
if len(buffer) != self.MINSIZE:
return self.setErr("invalid image file %s, to short"%filename)
if buffer[0] != 0xe9:
return self.setErr("invalid magic in file, expected 0xe9 got 0x%02x"%buffer[0])
for k, v in self.CHECKBYTES.items():
if buffer[k] != v:
return self.setErr("invalid magic at %d, expected %d got %d"
% (k+offset, v, buffer[k]))
name = self.getString(buffer, self.HDROFFSET + self.NAMEOFFSET, 32)
version = self.getString(buffer, self.HDROFFSET + self.VERSIONOFFSET, 32)
chipid= buffer[self.IDOFFSET]+256*buffer[self.IDOFFSET+1]
flashoffset=flashoffset if isFull else self.UPDATE_ADDR
return {
'error':False,
'info':"%s:%s"%(name,version),
'chipid':chipid,
'flashbase':flashoffset
}
def setErr(self,err):
return {'error':True,'info':err}
def checkImageFile(self,filename,isFull):
if not os.path.exists(filename):
return self.setErr("file %s not found"%filename)
return self.getFirmwareInfo(filename,isFull)
def checkSettings(self,port,fileName,isFull):
if port is None:
print("ERROR: no com port selected")
return
if fileName is None or fileName == '':
print("ERROR: no filename selected")
return
info = self.checkImageFile(fileName, isFull)
if info['error']:
print("ERROR: %s" % info['info'])
return
return {'fileName': fileName,'port':port,'isFull':isFull,'info':info}
def runEspTool(self,command):
print("run esptool: %s" % " ".join(command))
try:
esptool.main(command)
print("esptool done")
return True
except Exception as e:
print("Exception in esptool %s" % e)
def verifyChip(self,param):
if not param:
print("check failed")
return
imageChipId=param['info']['chipid']
try:
chip=esptool.ESPLoader.detect_chip(param['port'],trace_enabled=True)
print("Detected chip %s, id=%d"%(chip.CHIP_NAME,chip.IMAGE_CHIP_ID))
if (chip.IMAGE_CHIP_ID != imageChipId):
print("##Error: chip id in image %d does not match detected chip"%imageChipId)
return
print("Checks OK")
param['chipname']=chip.CHIP_NAME
except Exception as e:
print("ERROR: ",str(e))
return param
def runCheck(self,port,fileName,isFull):
param = self.checkSettings(port,fileName,isFull)
if not param:
return
print("Settings OK")
param=self.verifyChip(param)
if not param:
print("Check Failed")
return
print("flashbase=0x%x"%param['info']['flashbase'])
return param
def runFlash(self,param):
if not param:
return
if param['isFull']:
command=['--chip',param['chipname'],'--port',param['port'],'write_flash',str(param['info']['flashbase']),param['fileName']]
self.runEspTool(command)
else:
command=['--chip',param['chipname'],'--port',param['port'],'erase_region','0xe000','0x2000']
self.runEspTool(command)
command = ['--chip', param['chipname'], '--port', param['port'], 'write_flash', str(param['info']['flashbase']), param['fileName']]
self.runEspTool(command)

View File

@ -1,6 +1,39 @@
#! /usr/bin/env python3
import builtins
import subprocess
import sys
import os
import importlib.abc
import importlib.util
import types
'''
Inject a base package for our current directory
'''
class MyLoader(importlib.abc.InspectLoader):
def is_package(self, fullname: str) -> bool:
return True
def get_source(self, fullname: str):
return None
def get_code(self, fullname: str):
return ""
class MyFinder(importlib.abc.MetaPathFinder):
def __init__(self,baspkg,basedir=os.path.dirname(__file__),debug=False):
self.pkg=baspkg
self.dir=basedir
self.debug=debug
def find_spec(self,fullname, path, target=None):
if self.debug:
print("F:fullname=%s"%fullname)
if fullname == self.pkg:
if self.debug:
print("F:matching %s(%s)"%(fullname,self.dir))
spec=importlib.util.spec_from_file_location(fullname, self.dir,loader=MyLoader(), submodule_search_locations=[self.dir])
if self.debug:
print("F:injecting:",spec)
return spec
sys.meta_path.insert(0,MyFinder('flashtool'))
try:
import serial
@ -16,11 +49,13 @@ import tkinter.font as tkFont
import os
import serial.tools.list_ports
from tkinter import filedialog as FileDialog
try:
from flasher import Flasher
except:
from flashtool.flasher import Flasher
import builtins
def main():
VERSION="Version 1.1, esptool 3.2"
oldprint=builtins.print
def print(*args, **kwargs):
@ -32,6 +67,7 @@ def main():
class App:
def __init__(self, root):
self.flasher=Flasher()
root.title("ESP32 NMEA2000 Flash Tool")
root.geometry("800x600")
root.resizable(width=True, height=True)
@ -47,7 +83,7 @@ def main():
frame.columnconfigure(1, weight=3)
tk.Label(frame,text="ESP32 NMEA2000 Flash Tool").grid(row=row,column=0,columnspan=2,sticky='ew')
row+=1
tk.Label(frame, text=VERSION).grid(row=row,column=0,columnspan=2,sticky="ew",pady=10)
tk.Label(frame, text=self.flasher.getVersion()).grid(row=row,column=0,columnspan=2,sticky="ew",pady=10)
row+=1
self.mode=tk.IntVar()
self.mode.set(1)
@ -72,7 +108,7 @@ def main():
tk.Label(frame,textvariable=self.fileInfo).grid(row=row,column=0,columnspan=2,sticky="ew")
row+=1
self.flashInfo=tk.StringVar()
self.flashInfo.set("Address 0x1000")
self.flashInfo.set("Full Flash")
tk.Label(frame,textvariable=self.flashInfo).grid(row=row,column=0,columnspan=2,sticky='ew',pady=10)
row+=1
btFrame=tk.Frame(frame)
@ -96,7 +132,7 @@ def main():
def updateFlashInfo(self):
if self.mode.get() == 1:
#full
self.flashInfo.set("Address 0x1000")
self.flashInfo.set("Full Flash")
else:
self.flashInfo.set("Erase(otadata): 0xe000...0xffff, Address 0x10000")
def changeMode(self):
@ -108,7 +144,7 @@ def main():
fn=FileDialog.askopenfilename()
if fn:
self.filename.set(fn)
info=self.checkImageFile(fn,self.mode.get() == 1)
info=self.flasher.checkImageFile(fn,self.mode.get() == 1)
if info['error']:
self.fileInfo.set("***ERROR: %s"%info['info'])
else:
@ -141,51 +177,6 @@ def main():
self.interrupt=False
raise Exception("User cancel")
FULLOFFSET=61440
HDROFFSET = 288
VERSIONOFFSET = 16
NAMEOFFSET = 48
MINSIZE = HDROFFSET + NAMEOFFSET + 32
CHECKBYTES = {
0: 0xe9, # image magic
288: 0x32, # app header magic
289: 0x54,
290: 0xcd,
291: 0xab
}
def getString(self,buffer, offset, len):
return buffer[offset:offset + len].rstrip(b'\0').decode('utf-8')
def getFirmwareInfo(self,ih,imageFile,offset):
buffer = ih.read(self.MINSIZE)
if len(buffer) != self.MINSIZE:
return self.setErr("invalid image file %s, to short"%imageFile)
for k, v in self.CHECKBYTES.items():
if buffer[k] != v:
return self.setErr("invalid magic at %d, expected %d got %d"
% (k+offset, v, buffer[k]))
name = self.getString(buffer, self.HDROFFSET + self.NAMEOFFSET, 32)
version = self.getString(buffer, self.HDROFFSET + self.VERSIONOFFSET, 32)
return {'error':False,'info':"%s:%s"%(name,version)}
def setErr(self,err):
return {'error':True,'info':err}
def checkImageFile(self,filename,isFull):
if not os.path.exists(filename):
return self.setErr("file %s not found"%filename)
with open(filename,"rb") as fh:
offset=0
if isFull:
b=fh.read(1)
if len(b) != 1:
return self.setErr("unable to read header")
if b[0] != 0xe9:
return self.setErr("invalid magic in file, expected 0xe9 got 0x%02x"%b[0])
st=fh.seek(self.FULLOFFSET)
offset=self.FULLOFFSET
return self.getFirmwareInfo(fh,filename,offset)
def runCheck(self):
self.text_widget.delete("1.0", "end")
idx = self.port.current()
@ -195,52 +186,31 @@ def main():
return
port = self.serialDevices[idx]
fn = self.filename.get()
if fn is None or fn == '':
self.addText("ERROR: no filename selected")
return
info = self.checkImageFile(fn, isFull)
if info['error']:
print("ERROR: %s" % info['info'])
return
return {'port':port,'isFull':isFull}
param = self.flasher.runCheck(port,fn,isFull)
return param
def runEspTool(self,command):
def runFlash(self,param):
for b in self.actionButtons:
b.configure(state=tk.DISABLED)
self.cancelButton.configure(state=tk.NORMAL)
print("run esptool: %s" % " ".join(command))
root.update()
root.update_idletasks()
try:
esptool.main(command)
print("esptool done")
except Exception as e:
print("Exception in esptool %s" % e)
self.flasher.runFlash(param)
for b in self.actionButtons:
b.configure(state=tk.NORMAL)
self.cancelButton.configure(state=tk.DISABLED)
def buttonCheck(self):
param = self.runCheck()
if not param:
return
print("Settings OK")
command = ['--chip', 'ESP32', '--port', param['port'], 'chip_id']
self.runEspTool(command)
def buttonFlash(self):
param=self.runCheck()
if not param:
return
if param['isFull']:
command=['--chip','ESP32','--port',param['port'],'write_flash','0x1000',self.filename.get()]
self.runEspTool(command)
else:
command=['--chip','ESP32','--port',param['port'],'erase_region','0xe000','0x2000']
self.runEspTool(command)
command = ['--chip', 'ESP32', '--port', param['port'], 'write_flash', '0x10000', self.filename.get()]
self.runEspTool(command)
self.runFlash(param)
def buttonCancel(self):
self.interrupt=True

View File

@ -119,6 +119,22 @@
"category": "system",
"capabilities":{"apPwChange":["true"]}
},
{
"name": "apIp",
"type": "string",
"default":"192.168.15.1",
"check": "checkApIp",
"description": "The IP address for the access point. Clients will get addresses within the same subnet.",
"category":"system"
},
{
"name": "apMask",
"type": "string",
"default":"255.255.255.0",
"check": "checkNetMask",
"description": "The net mask for the access point",
"category":"system"
},
{
"name": "useAdminPass",
"type": "boolean",
@ -156,6 +172,16 @@
"description": "log level at the USB port",
"category":"system"
},
{
"name":"ledBrightness",
"label":"led brightness",
"type":"number",
"default":64,
"min":0,
"max":255,
"description":"the brightness of the led (0..255)",
"category":"system"
},
{
"name": "minXdrInterval",
"label":"min XDR interval",
@ -392,6 +418,128 @@
]
},
"category": "serial port"
}
,
{
"name": "serial2Dir",
"label": "serial2 direction",
"type": "list",
"default": "receive",
"list": [
"send",
"receive",
"off"
],
"description": "use the serial2 port to send or receive data",
"capabilities": {
"serial2mode": [
"UNI"
]
},
"category": "serial2 port"
},
{
"name": "serial2Baud",
"label": "serial2 baud rate",
"type": "list",
"default": "115200",
"description": "baud rate for the serial port 2",
"list": [
1200,
2400,
4800,
9600,
14400,
19200,
28800,
38400,
57600,
115200,
230400,
460800
],
"capabilities": {
"serial2mode": [
"RX",
"TX",
"UNI",
"BI"
]
},
"category": "serial2 port"
},
{
"name": "sendSerial2",
"label": "NMEA to Serial2",
"type": "boolean",
"default": "true",
"description": "send out NMEA data on the serial port 2",
"capabilities": {
"serial2mode": [
"TX",
"BI"
]
},
"category": "serial2 port"
},
{
"name": "receiveSerial2",
"label": "NMEA from Serial2",
"type": "boolean",
"default": "true",
"description": "receive NMEA data on the serial port 2",
"capabilities": {
"serial2mode": [
"RX",
"BI"
]
},
"category": "serial2 port"
},
{
"name": "serial2ToN2k",
"label": "serial2 to NMEA2000",
"type": "boolean",
"default": "true",
"description": "convert NMEA0183 from the serial port 2 to NMEA2000",
"capabilities": {
"serial2mode": [
"RX",
"BI",
"UNI"
]
},
"category": "serial2 port"
},
{
"name": "serial2ReadF",
"label": "serial2 read Filter",
"type": "filter",
"default": "",
"description": "filter for NMEA0183 data when reading from serial2\nselect aison|aisoff, set a whitelist or a blacklist with NMEA sentences like RMC,RMB",
"capabilities": {
"serial2mode": [
"RX",
"BI",
"UNI"
]
},
"category": "serial2 port"
},
{
"name": "serial2WriteF",
"label": "serial2 write Filter",
"type": "filter",
"default": "",
"description": "filter for NMEA0183 data when writing to serial2\nselect aison|aisoff, set a whitelist or a blacklist with NMEA sentences like RMC,RMB",
"capabilities": {
"serial2mode": [
"TX",
"BI",
"UNI"
]
},
"category": "serial2 port"
},
{
"name": "serverPort",

View File

@ -47,7 +47,7 @@
<span class="label"># clients</span>
<span class="value" id="numClients">---</span>
</div>
<div class="row">
<div class="row even">
<span class="label">TCP client connected</span>
<span class="value" id="clientCon">---</span>
</div>
@ -55,10 +55,15 @@
<span class="label">TCP client error</span>
<span class="value" id="clientErr">---</span>
</div>
<div class="row">
<div class="row even">
<span class="label">Free heap</span>
<span class="value" id="heap">---</span>
</div>
</div>
<div class="row">
<span class="label">NMEA2000 State</span>
[<span class="value" id="n2knode">---</span>]&nbsp;
<span class="value" id="n2kstate">UNKNOWN</span>
</div>
</div>
<button id="reset">Reset</button>
</div>
@ -95,6 +100,10 @@
<span class="label">firmware type</span>
<span class="value status-fwtype">---</span>
</div>
<div class="row">
<span class="label">chip type</span>
<span class="value status-chiptype">---</span>
</div>
<div class="row">
<span class="label">currentVersion</span>
<span class="value status-version">---</span>

View File

@ -3,6 +3,8 @@ let lastUpdate = (new Date()).getTime();
let reloadConfig = false;
let needAdminPass=true;
let lastSalt="";
let channelList={};
let minUser=200;
function addEl(type, clazz, parent, text) {
let el = document.createElement(type);
if (clazz) {
@ -65,22 +67,39 @@ function update() {
}
getJson('/api/status')
.then(function (jsonData) {
let statusPage=document.getElementById('statusPageContent');
let even=true; //first counter
for (let k in jsonData) {
if (k == "salt"){
lastSalt=jsonData[k];
continue;
}
if (k == "minUser"){
minUser=parseInt(jsonData[k]);
continue;
}
if (! statusPage) continue;
if (typeof (jsonData[k]) === 'object') {
for (let sk in jsonData[k]) {
let key = k + "." + sk;
if (typeof (jsonData[k][sk]) === 'object') {
//msg details
updateMsgDetails(key, jsonData[k][sk]);
}
else {
let el = document.getElementById(key);
if (el) el.textContent = jsonData[k][sk];
if (k.indexOf('count') == 0) {
createCounterDisplay(statusPage, k.replace("count", "").replace(/in$/," in").replace(/out$/," out"), k, even);
even = !even;
for (let sk in jsonData[k]) {
let key = k + "." + sk;
if (typeof (jsonData[k][sk]) === 'object') {
//msg details
updateMsgDetails(key, jsonData[k][sk]);
}
else {
let el = document.getElementById(key);
if (el) el.textContent = jsonData[k][sk];
}
}
}
if (k.indexOf("ch")==0){
//channel def
let name=k.substring(2);
channelList[name]=jsonData[k];
}
}
else {
let el = document.getElementById(k);
@ -167,6 +186,21 @@ function checkAdminPass(v){
return checkApPass(v);
}
function checkApIp(v,allValues){
if (! v) return "cannot be empty";
let err1="must be in the form 192.168.x.x";
if (! v.match(/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/))return err1;
let parts=v.split(".");
if (parts.length != 4) return err1;
for (let idx=0;idx < 4;idx++){
let iv=parseInt(parts[idx]);
if (iv < 0 || iv > 255) return err1;
}
}
function checkNetMask(v,allValues){
return checkApIp(v,allValues);
}
function checkIpAddress(v,allValues,def){
if (allValues.tclEnabled != "true") return;
if (! v) return "cannot be empty";
@ -212,6 +246,7 @@ function getAllConfigs(omitPass) {
let name = v.getAttribute('name');
if (!name) continue;
if (name.indexOf("_") >= 0) continue;
if (v.getAttribute('disabled')) continue;
let def = getConfigDefition(name);
if (def.type === 'password' && ( v.value == '' || omitPass)) {
continue;
@ -286,9 +321,13 @@ function factoryReset() {
.catch(function (e) { });
}
function createCounterDisplay(parent,label,key,isEven){
if (parent.querySelector("#"+key)){
return;
}
let clazz="row icon-row counter-row";
if (isEven) clazz+=" even";
let row=addEl('div',clazz,parent);
row.setAttribute("id",key);
let icon=addEl('span','icon icon-more',row);
addEl('span','label',row,label);
let value=addEl('span','value',row,'---');
@ -331,18 +370,7 @@ function updateMsgDetails(key, details) {
},frame);
});
}
let counters={
count2Kin: 'NMEA2000 in',
count2Kout: 'NMEA2000 out',
countTCPin: 'TCPserver in',
countTCPout: 'TCPserver out',
countTCPClientin: 'TCPclient in',
countTCPClientout: 'TCPclient out',
countUSBin: 'USB in',
countUSBout: 'USB out',
countSERin: 'Serial in',
countSERout: 'Serial out'
}
function showOverlay(text, isHtml) {
let el = document.getElementById('overlayContent');
if (isHtml) {
@ -432,6 +460,7 @@ function createInput(configItem, frame,clazz) {
let el;
if (configItem.type === 'boolean' || configItem.type === 'list' || configItem.type == 'boatData') {
el=addEl('select',clazz,frame);
if (configItem.readOnly) el.setAttribute('disabled',true);
el.setAttribute('name', configItem.name)
let slist = [];
if (configItem.list) {
@ -464,6 +493,7 @@ function createInput(configItem, frame,clazz) {
return createXdrInput(configItem,frame,clazz);
}
el = addEl('input',clazz,frame);
if (configItem.readOnly) el.setAttribute('disabled',true);
el.setAttribute('name', configItem.name)
if (configItem.type === 'password') {
el.setAttribute('type', 'password');
@ -579,25 +609,29 @@ function createXdrInput(configItem,frame){
{l:'bidir',v:1},
{l:'to2K',v:2},
{l:'from2K',v:3}
]
],
readOnly: configItem.readOnly
},d,'xdrdir');
d=createXdrLine(el,'Category');
let category=createInput({
type: 'list',
name: configItem.name+"_cat",
list:getXdrCategories()
list:getXdrCategories(),
readOnly: configItem.readOnly
},d,'xdrcat');
d=createXdrLine(el,'Source');
let selector=createInput({
type: 'list',
name: configItem.name+"_sel",
list:[]
list:[],
readOnly: configItem.readOnly
},d,'xdrsel');
d=createXdrLine(el,'Field');
let field=createInput({
type:'list',
name: configItem.name+'_field',
list: []
list: [],
readOnly: configItem.readOnly
},d,'xdrfield');
d=createXdrLine(el,'Instance');
let imode=createInput({
@ -608,22 +642,26 @@ function createXdrInput(configItem,frame){
{l:'single',v:0},
{l:'ignore',v:1},
{l:'auto',v:2}
]
],
readOnly: configItem.readOnly
},d,'xdrimode');
let instance=createInput({
type:'number',
name: configItem.name+"_instance",
readOnly: configItem.readOnly
},d,'xdrinstance');
d=createXdrLine(el,'Transducer');
let xdrName=createInput({
type:'text',
name: configItem.name+"_xdr"
name: configItem.name+"_xdr",
readOnly: configItem.readOnly
},d,'xdrname');
d=createXdrLine(el,'Example');
let example=addEl('div','xdrexample',d,'');
let data = addEl('input','xdrvalue',el);
data.setAttribute('type', 'hidden');
data.setAttribute('name', configItem.name);
if (configItem.readOnly) data.setAttribute('disabled',true);
let changeFunction = function () {
let parts=data.value.split(',');
direction.value=parts[1] || 0;
@ -718,16 +756,19 @@ function createFilterInput(configItem, frame) {
let ais = createInput({
type: 'list',
name: configItem.name + "_ais",
list: ['aison', 'aisoff']
list: ['aison', 'aisoff'],
readOnly: configItem.readOnly
}, el);
let mode = createInput({
type: 'list',
name: configItem.name + "_mode",
list: ['whitelist', 'blacklist']
list: ['whitelist', 'blacklist'],
readOnly: configItem.readOnly
}, el);
let sentences = createInput({
type: 'text',
name: configItem.name + "_sentences",
readOnly: configItem.readOnly
}, el);
let data = addEl('input',undefined,el);
data.setAttribute('type', 'hidden');
@ -755,6 +796,7 @@ function createFilterInput(configItem, frame) {
changeFunction();
});
data.setAttribute('name', configItem.name);
if (configItem.readOnly) data.setAttribute('disabled',true);
return data;
}
let moreicons=['icon-more','icon-less'];
@ -978,9 +1020,7 @@ function toggleClass(el,id,classList){
}
function createConfigDefinitions(parent, capabilities, defs,includeXdr) {
let category;
let categoryEl;
let categoryFrame;
let categories={};
let frame = parent.querySelector('.configFormRows');
if (!frame) throw Error("no config form");
frame.innerHTML = '';
@ -993,23 +1033,25 @@ function createConfigDefinitions(parent, capabilities, defs,includeXdr) {
}
else{
if(includeXdr) return;
}
if (item.category != category || !categoryEl) {
if (categoryFrame && ! currentCategoryPopulated){
categoryFrame.remove();
}
let catEntry;
if (categories[item.category] === undefined){
catEntry={
populated:false,
frame: undefined,
element: undefined
}
currentCategoryPopulated=false;
categoryFrame = addEl('div', 'category', frame);
categoryFrame.setAttribute('data-category',item.category)
let categoryTitle = addEl('div', 'title', categoryFrame);
categories[item.category]=catEntry
catEntry.frame = addEl('div', 'category', frame);
catEntry.frame.setAttribute('data-category',item.category)
let categoryTitle = addEl('div', 'title', catEntry.frame);
let categoryButton = addEl('span', 'icon icon-more', categoryTitle);
addEl('span', 'label', categoryTitle, item.category);
addEl('span','categoryAdd',categoryTitle);
categoryEl = addEl('div', 'content', categoryFrame);
categoryEl.classList.add('hidden');
let currentEl = categoryEl;
catEntry.element = addEl('div', 'content', catEntry.frame);
catEntry.element.classList.add('hidden');
categoryTitle.addEventListener('click', function (ev) {
let rs = currentEl.classList.toggle('hidden');
let rs = catEntry.element.classList.toggle('hidden');
if (rs) {
toggleClass(categoryButton,0,moreicons);
}
@ -1017,7 +1059,9 @@ function createConfigDefinitions(parent, capabilities, defs,includeXdr) {
toggleClass(categoryButton,1,moreicons);
}
})
category = item.category;
}
else{
catEntry=categories[item.category];
}
let showItem=true;
let itemCapabilities=item.capabilities||{};
@ -1036,17 +1080,26 @@ function createConfigDefinitions(parent, capabilities, defs,includeXdr) {
});
if (!found) showItem=false;
}
let readOnly=false;
let mode=capabilities['CFGMODE'+item.name];
if (mode == 1) {
//hide
showItem=false;
}
if (mode == 2){
readOnly=true;
}
if (showItem) {
currentCategoryPopulated=true;
let row = addEl('div', 'row', categoryEl);
item.readOnly=readOnly;
catEntry.populated=true;
let row = addEl('div', 'row', catEntry.element);
let label = item.label || item.name;
addEl('span', 'label', row, label);
let valueFrame = addEl('div', 'value', row);
let valueEl = createInput(item, valueFrame);
if (!valueEl) return;
valueEl.setAttribute('data-default', item.default);
valueEl.addEventListener('change', function (ev) {
if (! readOnly) valueEl.addEventListener('change', function (ev) {
let el = ev.target;
checkChange(el, row, item.name);
})
@ -1063,13 +1116,15 @@ function createConfigDefinitions(parent, capabilities, defs,includeXdr) {
}
if (item.check) valueEl.setAttribute('data-check', item.check);
let btContainer = addEl('div', 'buttonContainer', row);
let bt = addEl('button', 'defaultButton', btContainer, 'X');
bt.setAttribute('data-default', item.default);
bt.addEventListener('click', function (ev) {
valueEl.value = valueEl.getAttribute('data-default');
let changeEvent = new Event('change');
valueEl.dispatchEvent(changeEvent);
})
if (!readOnly) {
let bt = addEl('button', 'defaultButton', btContainer, 'X');
bt.setAttribute('data-default', item.default);
bt.addEventListener('click', function (ev) {
valueEl.value = valueEl.getAttribute('data-default');
let changeEvent = new Event('change');
valueEl.dispatchEvent(changeEvent);
})
}
bt = addEl('button', 'infoButton', btContainer, '?');
bt.addEventListener('click', function (ev) {
if (item.description) {
@ -1083,8 +1138,11 @@ function createConfigDefinitions(parent, capabilities, defs,includeXdr) {
});
}
});
if (categoryFrame && ! currentCategoryPopulated){
categoryFrame.remove();
for (let cat in categories){
let catEntry=categories[cat];
if (! catEntry.populated){
catEntry.frame.remove();
}
}
}
function loadConfigDefinitions() {
@ -1448,13 +1506,13 @@ function createDashboard() {
frame.innerHTML = '';
}
function sourceName(v){
if (v == 0) return "N2K";
if (v == 1) return "USB";
if (v == 2) return "SER";
if (v == 3) return "TCPcl"
if (v >= 4 && v <= 20) return "TCPser";
if (v >= 200) return "USER";
return "---";
for (let n in channelList){
if (v >= channelList[n].id && v <= channelList[n].max){
return n;
}
}
if (v < minUser) return "---";
return "USER["+v+"]";
}
let lastSelectList=[];
function updateDashboard(data) {
@ -1552,9 +1610,15 @@ function uploadBin(ev){
.then(function (result) {
let currentType;
let currentVersion;
let chipid;
forEl('.status-version', function (el) { currentVersion = el.textContent });
forEl('.status-fwtype', function (el) { currentType = el.textContent });
forEl('.status-chipid', function (el) { chipid = el.textContent });
let confirmText = 'Ready to update firmware?\n';
if (result.chipId != chipid){
confirmText += "WARNING: the chipid in the image ("+result.chipId;
confirmText +=") does not match the current chip id ("+chipid+").\n";
}
if (currentType != result.fwtype) {
confirmText += "WARNING: image has different type: " + result.fwtype + "\n";
confirmText += "** Really update anyway? - device can become unusable **";
@ -1631,7 +1695,8 @@ function uploadBin(ev){
let HDROFFSET=288;
let VERSIONOFFSET=16;
let NAMEOFFSET=48;
let MINSIZE=HDROFFSET+NAMEOFFSET+32;
let MINSIZE = HDROFFSET + NAMEOFFSET + 32;
let CHIPIDOFFSET=12; //2 bytes chip id here
let imageCheckBytes={
0: 0xe9, //image magic
288: 0x32, //app header magic
@ -1650,6 +1715,10 @@ function decodeFromBuffer(buffer,start,length){
start+length));
return rt;
}
function getChipId(buffer){
if (buffer.length < CHIPIDOFFSET+2) return -1;
return buffer[CHIPIDOFFSET]+256*buffer[CHIPIDOFFSET+1];
}
function checkImageFile(file){
return new Promise(function(resolve,reject){
if (! file) reject("no file");
@ -1666,9 +1735,11 @@ function checkImageFile(file){
}
let version=decodeFromBuffer(content,HDROFFSET+VERSIONOFFSET,32);
let fwtype=decodeFromBuffer(content,HDROFFSET+NAMEOFFSET,32);
let chipId=getChipId(content);
let rt={
fwtype:fwtype,
version: version,
chipId:chipId
};
resolve(rt);
});
@ -1716,13 +1787,13 @@ window.addEventListener('load', function () {
}
}catch(e){}
let statusPage=document.getElementById('statusPageContent');
if (statusPage){
/*if (statusPage){
let even=true;
for (let c in counters){
createCounterDisplay(statusPage,counters[c],c,even);
even=!even;
}
}
}*/
forEl('#uploadFile',function(el){
el.addEventListener('change',function(ev){
if (ev.target.files.length < 1) return;
@ -1730,7 +1801,9 @@ window.addEventListener('load', function () {
checkImageFile(file)
.then(function(res){
forEl('#imageProperties',function(iel){
iel.textContent=res.fwtype+", "+res.version;
let txt="["+res.chipId+"] ";
txt+=res.fwtype+", "+res.version;
iel.textContent=txt;
iel.classList.remove("error");
})
})

504
webinstall/build.yaml Normal file
View File

@ -0,0 +1,504 @@
# structure
# below config we define the structure to be displayed
# basically there are 2 object types:
# children - a list of cfg objects that define the inputs to be shown
# if the parent is selected
# parameters:
# key: unique key - defines the name(part) in the cfg
# if not set a potential "value" is taken
# null (empty) is a valid key
# label: title to be shown, if unset key will be used
# resorce: a resource that is used by the value children
# simple string use as is
# string + ':' - add value to the resource
# type: if empty or 'frame' only the children are considered
# children: only for type empty or 'frame' - list of child objects
# target: how the selected child value should be stored:
# environment - set the environment to the child value
# define - add -D<childValue> to the flags
# define:name - add -D<name>=<childValue> to the flags
# values - a list of value objects for a particular config
# if the object is just a string it is converted to an object
# with value being set to the string
# parameters:
# key: unique key, defines the name(part) and the value store in cfg
# for the parent
# if not set, value is used
# null (empty) is a valid key
# value: the value (mandatory)
# if null the value will be set to undefined and ignored
# label: text to be shown
# if not set value will be used
# description,url
# resource: for parent-target environment:
# an object with allowed resource counts
# for other values: the resource to be counted
#
#
types:
- &m5base
type: select
target: define
label: 'M5 Atom light Base'
key: m5lightbase
values:
- label: "CAN KIT"
value: M5_CAN_KIT
description: "M5 Stack CAN Kit"
url: "https://docs.m5stack.com/en/atom/atom_can"
resource: can
- value: M5_SERIAL_KIT_232
description: "M5 Stack RS232 Base"
label: "Atomic RS232 Base"
url: "https://docs.m5stack.com/en/atom/Atomic%20RS232%20Base"
resource: serial
- value: M5_SERIAL_KIT_485
description: "M5 Stack RS485 Base"
label: "Atomic RS485 Base"
url: "https://docs.m5stack.com/en/atom/Atomic%20RS485%20Base"
resource: serial
- value: M5_GPS_KIT
description: "M5 Stack Gps Kit"
label: "Gps Base"
url: "https://docs.m5stack.com/en/atom/atomicgps"
resource: serial
- &m5groovei2c
type: frame
key: m5groovei2c
label: "M5 I2C Groove Units"
children:
- label: "M5 ENV3"
type: checkbox
key: m5env3
target: define
url: "https://docs.m5stack.com/en/unit/envIII"
description: "M5 sensor module temperature, humidity, pressure"
values:
- value: M5_ENV3
key: true
- &m5groovecan
type: select
key: m5groovecan
target: define
label: "M5 Groove CAN Units"
values:
- label: "CAN Unit"
url: "https://docs.m5stack.com/en/unit/can"
description: "M5 Can unit"
value: M5_CANUNIT
resource: can
- &m5grooveserial
type: select
label: "M5 Groove Serial Unit"
target: define
key: m5grooveserial
values:
- label: "RS485"
key: unit485
value: SERIAL_GROOVE_485
description: "M5 RS485 unit"
url: "https://docs.m5stack.com/en/unit/rs485"
resource: serial
- label: "Tail485"
value: SERIAL_GROOVE_485
key: tail485
description: "M5 Tail 485"
url: "https://docs.m5stack.com/en/atom/tail485"
resource: serial
- label: "Gps Unit"
value: M5_GPS_UNIT
description: "M5 Gps Unit"
url: "https://docs.m5stack.com/en/unit/gps"
resource: serial
- &m5groove
type: select
key: m5groove
label: 'M5 groove type'
help: 'Select the functionality that should be available at the M5 groove pins'
values:
- key: 'CAN'
children:
- *m5groovecan
- key: 'I2C'
children:
- *m5groovei2c
- key: 'Serial'
children:
- *m5grooveserial
- &gpiopin
type: dropdown
resource: "gpio:"
help: 'Select the number of the GPIO pin for this function'
values:
- {label: unset,value:}
- {label: "0: Low at boot!",value: 0}
- 1
- {label: "2: Float/Low at boot!", value: 2}
- 3
- {label: "4: Strapping!",value: 4}
- {label: "5: Hight at boot!", value: 5}
- {label: "12: Low at boot!", value: 12}
- 13
- 14
- {label: "15: High at boot!", value: 15}
- 16
- 17
- 18
- 19
- 21
- 22
- 23
- 25
- 26
- 27
- 32
- 31
- 32
- 33
- 37
- 38
- &gpioinput
type: dropdown
resource: "gpio:"
help: 'Select the number of the GPIO pin for this function'
values:
- {label: unset,value:}
- {label: "0: Low at boot!",value: 0}
- 1
- {label: "2: Float/Low at boot!", value: 2}
- 3
- {label: "4: Strapping!",value: 4}
- {label: "5: Hight at boot!", value: 5}
- {label: "12: Low at boot!", value: 12}
- 13
- 14
- {label: "15: High at boot!", value: 15}
- 16
- 17
- 18
- 19
- 21
- 22
- 23
- 25
- 26
- 27
- 32
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- &serialRX
<<: *gpioinput
key: RX
help: 'number of the GPIO pin for the receive function'
target: "define:#serial#RX"
mandatory: true
- &serialTX
<<: *gpiopin
key: TX
help: 'number of the GPIO pin for the transmit function'
target: "define:#serial#TX"
mandatory: true
- &serialValues
- key: true
children:
- type: select
key: type
target: "define:#serial#TYPE"
label: "Serial Type"
values:
- key: uni
value: 1
label: "UNI"
description: "Select direction at Config UI"
help: 'On the config UI you can select if the serial should be a transmitter or a receiver'
children:
- *serialRX
- *serialTX
- key: bi
value: 2
label: "BiDir"
description: "Input and Output"
help: 'The serial device can run both receive and transmit. Typically for RS232.'
children:
- *serialRX
- *serialTX
- key: rx
value: 3
label: "RX"
description: "Input only"
children:
- *serialRX
- key: tx
value: 1
label: "TX"
description: "output only"
children:
- *serialTX
- &serial1
type: checkbox
label: 'Serial 1'
key: serial1
base:
serial: GWSERIAL_
values: *serialValues
- &serial2
type: checkbox
label: 'Serial 2'
key: serial2
base:
serial: GWSERIAL2_
values: *serialValues
- &can
type: checkbox
label: CAN(NMEA2000)
key: can
values:
- key: true
children:
- <<: *gpioinput
label: RX
key: rx
mandatory: true
help: 'set the number of the GPIO pin for the CAN(NMEA2000) RX function'
target: "define:ESP32_CAN_RX_PIN"
- <<: *gpiopin
label: TX
key: tx
mandatory: true
help: 'set the number of the GPIO pin for the CAN(NMEA2000) TX function'
target: "define:ESP32_CAN_TX_PIN"
- &resetButton
type: checkbox
label: reset button
key: resetButton
values:
- key: true
children:
- <<: *gpiopin
label: Button
key: button
target: "define:GWBUTTON_PIN"
help: 'the gpio pin for a reset to factory settings'
- type: dropdown
label: active mode
help: 'select if the button should be active high or low'
key: resetButtonMode
target: "define:GWBUTTON_ACTIVE"
values:
- label: unset
value:
- label: LOW
value: 0
- label: HIGH
value: 1
- type: checkbox
label: pullupdown
description: "pull up/pull down resistor"
key: resetButtonPUD
values:
- key: true
target: define
value: GWBUTTON_PULLUPDOWN
- &led
type: checkbox
label: Led
key: led
description: 'RGB LED'
values:
- key: true
children:
- <<: *gpiopin
label: LedPin
key: ledpin
mandatory: true
target: "define:GWLED_PIN"
- type: dropdown
label: ledtype
help: "the type of the led"
key: ledtype
target: "define:GWLED_CODE"
mandatory: true
values:
- label: unset
value:
- label: SK6812
value: 0
key: sk6812
- label: WS2812
key: ws2812
value: 1
- type: dropdown
key: ledorder
label: color order
target: "define:GWLED_SCHEMA"
mandatory: true
values:
- label: unset
value:
- label: RGB
value: 10
- label: RBG
value: 17
- label: GRB
value: 66
- label: GBR
value: 80
- label: BRG
value: 129
- label: BGR
value: 136
- type: range
label: brigthness
target: "define:GWLED_BRIGHTNESS"
key: brightness
min: 0
max: 255
- &iicsensors
type: checkbox
label: "I2C #busname#"
key: "i2c#busname#"
description: "I2C Bus #busname#"
values:
- key: true
children:
- <<: *gpiopin
label: SDA
key: sda
mandatory: true
target: "define:GWIIC_SDA#bus#"
- <<: *gpiopin
label: SCL
key: scl
mandatory: true
target: "define:GWIIC_SCL#bus#"
- type: checkbox
label: SHT3X-#busname#-1
description: "SHT30 temperature and humidity sensor 0x44"
key: sht3x1
target: define
url: "https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/docs/datasheet/unit/SHT3x_Datasheet_digital.pdf"
values:
- key: true
value: GWSHT3X#busname#1
- type: checkbox
label: SHT3X-#busname#-1
description: "SHT30 temperature and humidity sensor 0x45"
key: sht3x2
target: define
url: "https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/docs/datasheet/unit/SHT3x_Datasheet_digital.pdf"
values:
- key: true
value: GWSHT3X#busname#2
- type: checkbox
label: QMP6988-#busname#-1
description: "QMP6988 pressure sensor addr 86"
key: qmp69881
target: define
url: "https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/docs/datasheet/unit/enviii/QMP6988%20Datasheet.pdf"
values:
- key: true
value: GWQMP6988#busname#1
- type: checkbox
label: QMP6988-#busname#-2
description: "QMP6988 pressure sensor addr 112"
key: qmp69882
target: define
url: "https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/docs/datasheet/unit/enviii/QMP6988%20Datasheet.pdf"
values:
- key: true
value: GWQMP6988#busname#2
- type: checkbox
label: BME280-#busname#-1
description: "BME280 temperature/humidity/pressure sensor 0x76"
key: bme2801
target: define
url: "https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bme280-ds002.pdf"
values:
- key: true
value: GWBME280#busname#1
- type: checkbox
label: BME280-#busname#-2
description: "BME280 temperature/humidity/pressure sensor 0x77"
key: bme2802
target: define
url: "https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bme280-ds002.pdf"
values:
- key: true
value: GWBME280#busname#2
resources:
default: &esp32default
serial: 2
can: 1
i2c: 1
gpio: 1
config:
children:
- type: select
target: environment
label: 'Board'
key: board
values:
- value: m5stack-atom-generic
label: m5stack-atom
description: "M5 Stack Atom light"
url: "http://docs.m5stack.com/en/core/atom_lite"
resource: *esp32default
children:
- *m5base
- *m5groove
- value: m5stack-atoms3-generic
label: m5stack-atoms3
description: "M5 Stack AtomS3 light"
url: "http://docs.m5stack.com/en/core/AtomS3%20Lite"
resource: *esp32default
children:
- *m5base
- *m5groove
- value: m5stickc-atom-generic
label: m5stick+ atom
description: "M5 Stick C+"
url: "http://docs.m5stack.com/en/core/m5stickc_plus"
resource: *esp32default
children:
- *m5groove
- value: nodemcu-generic
label: nodemcu
description: "Node mcu esp32"
url: "https://docs.platformio.org/en/stable/boards/espressif32/nodemcu-32s.html"
resource: *esp32default
children:
- *serial1
- *serial2
- *can
- *resetButton
- *led
- <<: *iicsensors
base:
busname: "1"
bus: ""
- <<: *iicsensors
base:
busname: "2"
bus: "2"

214
webinstall/cibuild.css Normal file
View File

@ -0,0 +1,214 @@
.hidden{
display: none;
}
/* reused stuff from configui */
.configui.container{
margin-left: auto;
margin-right: auto;
position: relative;
}
.configui .info{
margin-bottom: 1em;
opacity: 0.6;
white-space: pre-line;
}
.configui .parameters {
border-bottom: 1px solid grey;
margin-bottom: 1em;
}
.configui .row input[type="checkbox"] {
flex-grow: 1;
appearance: auto;
}
.configui .row {
display: flex;
flex-direction: row;
margin: 0.5em;
flex-wrap: unset;
padding: 0;
}
.configui .row .label {
width: 10em;
opacity: 0.6;
padding: 0;
flex-shrink: 0;
}
.configui .row .value{
padding-left: 0;
}
.configui .since {
display: block;
font-size: 0.8em;
}
.configui input[type=checkbox] {
width: 1.5em;
height: 1.5em;
opacity: 1;
z-index: unset;
appearance: auto;
float:none;
margin-right: 0.5em;
}
.configui .buttons {
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.configui button {
padding: 0.5em;
}
.configui .footer {
text-align: right;
margin-top: 1em;
font-size: 0.8em;
}
.configui .visually-hidden {
position: absolute !important;
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px, 1px, 1px, 1px);
}
.configui .dialogBack {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 10;
background-color: #8a8c8ec7;
display: flex;
}
.configui .hidden{
display: none !important;
}
.configui .dialog{
max-width: 35em;
margin: auto;
background-color: white;
padding: 2em;
}
.configui #warn{
display: none;
color: red;
}
.configui .error .value{
color: red;
}
.configui #warn.warn{
display: block;
}
.configui .radioFrame {
display: flex;
flex-direction: row;
align-items: center;
}
.configui input.radioCi {
appearance: auto;
float: none;
opacity: 1;
margin-left: 0.5em;
margin-right: 0.5em;
z-index: unset;
}
.configui .selector .title {
font-weight: bold;
}
.configui .selector.level2 {
margin-left: 0.5em;
}
.configui .selector.level3 {
margin-left: 1em;
}
.configui .selector.level4 {
margin-left: 1.5em;
}
.configui .selector.tframe {
padding-bottom: 0;
border-bottom: unset;
}
.configui .childFrame {
border-top: 1px solid grey;
margin-top: 0.3em;
}
.configui .tframe>.childFrame {
border-top: unset;
margin-top: 0.3em;
}
.configui .tcheckbox>.inputFrame,
.configui .tdropdown>.inputFrame,
.configui .trange>.inputFrame {
display: flex;
flex-direction: row;
align-items: center;
}
.configui .title.tdropdown,
.configui .title.tcheckbox,
.configui .title.trange {
width: 10em;
font-weight: normal !important;
}
.configui .titleFrame {
display: flex;
flex-direction: row;
align-items: center;
}
.configui form#upload {
width: 0;
height: 0;
/* display: flex; */
overflow: hidden;
}
.configui .label {
width: 10em;
}
.configui .row input{
flex-grow: 1;
width: initial;
}
.configui .overlayContainer {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: #80808070;
display: flex;
overflow-y: auto;
padding: 0.5em;
}
.configui .overlay {
margin: auto;
background-color: white;
padding: 0.5em;
max-width: 100%;
box-sizing: border-box;
}
.configui .overlayContent {
padding: 0.5em;
}
.configui div#overlayContent.text{
white-space: pre-line;
}
.configui .overlayButtons {
border-top: 1px solid grey;
padding-top: 0.5em;
display: flex;
flex-direction: row;
justify-content: end;
}
.configui button.help {
margin-left: 1em;
width: 2em;
height: 2em;
line-height: 1em;
}

86
webinstall/cibuild.html Normal file
View File

@ -0,0 +1,86 @@
<!DOCTYPE html>
<html>
<head>
<script type="module" src="cibuild.js"></script>
<link rel="stylesheet" href="cibuild.css"/>
</head>
<body>
<div class="configui cibuild container">
<h1>Build your own ESP32-NMEA2000</h1>
<h3>New Build</h3>
<div class="row">
<span class="label" id="branchOrTag"></span>
<span class="value" id="branchOrTagValue">---</span>
</div>
<div class="row">
<span class="label" >GitSha</span>
<span class="value" id="gitSha">---</span>
</div>
<div class="row">
<span class="label" >Version</span>
<span class="value" id="buildVersion"></span>
</div>
<div class="row">
<button id="downloadConfig">SaveCfg</button>
<button id="uploadConfig">LoadCfg</button>
</div>
<div id="selectors">
</div>
<div class="row">
<span class="label">Board type</span>
<div class="value" id="environment" ></div>
</div>
<div class="row">
<span class="label">Build Flags</span>
<div class="value" id="buildflags"></div>
<button class="help" id="buildCommand">?</button>
</div>
<div class="row hidden error">
<span class="label">Error</span>
<span class="value" id="configError"></span>
</div>
<div class="row">
<button id="start">Start</button>
</div>
<h3 id="resultTitle">Last Build</h3>
<div class="row">
<span class="label">Job Id</span>
<div id="pipeline">---</div>
</div>
<div class="row">
<span class="label">Status</span>
<div id="status">---</div>
</div>
<div class="row hidden error">
<span class="label">Error</span>
<div class="value" id="error"></div>
</div>
<div class="row hidden">
<span class="label">Web Status</span>
<a target="_" id="status_url">Link</a>
</div>
<div class="row hidden">
<button id="download">Download</button>
<button id="webinstall">Install</button>
</div>
<div class="overlayContainer hidden" id="overlayContainer">
<div id="overlay" class="overlay">
<div id="overlayContent" class="overlayContent">
AHA
</div>
<div class="overlayButtons">
<button id="secondDialogButton"></button>
<button id="hideOverlay">Close</button>
</div>
</div>
</div>
<iframe id="dlframe" width="1" height="1"></iframe>
<form id="upload">
<input type="file" id="fileSelect"/>
</form>
</div>
</body>
</html>

852
webinstall/cibuild.js Normal file
View File

@ -0,0 +1,852 @@
import { addEl, setButtons,fillValues, setValue, buildUrl, fetchJson, setVisible, enableEl, setValues, getParam, fillSelect, forEachEl, readFile } from "./helper.js";
import {load as yamlLoad} from "https://cdn.skypack.dev/js-yaml@4.1.0";
import fileDownload from "https://cdn.skypack.dev/js-file-download@0.4.12"
class PipelineInfo{
constructor(id){
this.STFIELDS=['status','error','status_url'];
this.reset(id);
this.lastUpdate=0;
}
update(state){
this.lastUpdate=(new Date()).getTime();
if (state.pipeline_id !== undefined && state.pipeline_id !== this.id){
return false;
}
this.STFIELDS.forEach((i)=>{
let v=state[i];
if (v !== undefined)this[i]=v;
});
}
reset(id,opt_state){
this.id=id;
this.STFIELDS.forEach((i)=>this[i]=undefined);
this.downloadUrl=undefined;
if (opt_state) {
this.update(opt_state);
}
else{
if (id !== undefined) this.status='fetching';
}
}
valid(){
return this.id !== undefined;
}
isRunning(){
if (! this.valid()) return false;
if (this.status === undefined) return false;
return ['error','success','canceled','failed','errored'].indexOf(this.status) < 0;
}
}
(function(){
const STATUS_INTERVAL=2000;
const CURRENT_PIPELINE='pipeline';
const API="cibuild.php";
const GITAPI="install.php";
const GITUSER="wellenvogel";
const GITREPO="esp32-nmea2000";
let currentPipeline=new PipelineInfo();
let timer=undefined;
let structure=undefined;
let config={}; //values as read and stored
let configStruct={}; //complete struct merged of config and struct
let displayMode='last';
let delayedSearch=undefined;
let gitSha=undefined;
let buildVersion=undefined;
let configName="buildconfig";
let isModified=false;
const modeStrings={
last: 'Last Build',
existing: 'Existing Build',
current: 'Current Build'
};
const setDisplayMode=(mode)=>{
let old=displayMode;
let ms=modeStrings[mode];
if (ms === undefined){
return false;
}
displayMode=mode;
setValue('resultTitle',ms);
return mode !== old;
}
const updateStatus=()=>{
setValues(currentPipeline,{
id: 'pipeline'
});
setVisible('download',currentPipeline.valid() && currentPipeline.downloadUrl!==undefined,true);
setVisible('status_url',currentPipeline.valid() && currentPipeline.status_url!==undefined,true);
setVisible('error',currentPipeline.error!==undefined,true);
let values={};
fillValues(values,['configError','environment']);
setVisible('buildCommand',values.environment !== "" && ! values.configError);
if (values.configError) {
enableEl('start',false);
return;
}
if (!values.environment){
enableEl('start',false);
return;
}
if (displayMode != 'existing'){
if (currentPipeline.valid()){
//check pipeline state
if (['error','success','canceled','failed'].indexOf(currentPipeline.status) >= 0){
enableEl('start',true);
return;
}
enableEl('start',false);
return;
}
enableEl('start',true);
return;
}
//display mode existing
//allow start if either no pipeline or not running and status != success
enableEl('start',!currentPipeline.valid() || (!currentPipeline.isRunning() && currentPipeline.status != "success"));
}
const isRunning=()=>{
return currentPipeline.isRunning();
}
const fetchStatus=(initial)=>{
if (! currentPipeline.valid()){
updateStatus();
return;
}
let queryPipeline=currentPipeline.id;
fetchJson(API,{api:'status',pipeline:currentPipeline.id})
.then((st)=>{
if (queryPipeline !== currentPipeline.id) return;
let stid=st.pipeline_id||st.id;
if (currentPipeline.id !== stid) return;
if (st.status === undefined) st.status=st.state;
currentPipeline.update(st);
updateStatus();
if (st.status === 'error' || st.status === 'errored' || st.status === 'canceled'){
return;
}
if (st.status === 'success'){
fetchJson(API,{api:'artifacts',pipeline:currentPipeline.id})
.then((ar)=>{
if (! ar.items || ar.items.length < 1){
throw new Error("no download link");
}
currentPipeline.downloadUrl=buildUrl(API,{
download: currentPipeline.id
});
updateStatus();
})
.catch((err)=>{
currentPipeline.update({
status:'error',
error:"Unable to get build result: "+err
});
updateStatus();
});
return;
}
timer=window.setTimeout(fetchStatus,STATUS_INTERVAL)
})
.catch((e)=>{
timer=window.setTimeout(fetchStatus,STATUS_INTERVAL);
})
}
const setCurrentPipeline=(pipeline,doStore)=>{
currentPipeline.reset(pipeline);
if (doStore) window.localStorage.setItem(CURRENT_PIPELINE,pipeline);
};
const startBuild=()=>{
let param={};
currentPipeline.reset(undefined,{status:'requested'});
if (timer) window.clearTimeout(timer);
timer=undefined;
fillValues(param,['environment','buildflags']);
setDisplayMode('current');
updateStatus();
if (gitSha !== undefined) param.tag=gitSha;
param.config=JSON.stringify(config);
if (buildVersion !== undefined){
param.suffix="-"+buildVersion;
}
fetchJson(API,Object.assign({
api:'start'},param))
.then((json)=>{
let status=json.status || json.state|| 'error';
if (status === 'error'){
currentPipeline.update({status:status,error:json.error})
updateStatus();
throw new Error("unable to create job "+(json.error||''));
}
if (!json.id) {
let error="unable to create job, no id"
currentPipeline.update({status:'error',error:error});
updateStatus();
throw new Error(error);
}
setCurrentPipeline(json.id,true);
updateStatus();
timer=window.setTimeout(fetchStatus,STATUS_INTERVAL);
})
.catch((err)=>{
currentPipeline.update({status:'error',error:err});
updateStatus();
});
}
const runDownload=()=>{
if (! currentPipeline.downloadUrl) return;
let df=document.getElementById('dlframe');
if (df){
df.setAttribute('src',null);
df.setAttribute('src',currentPipeline.downloadUrl);
}
}
const webInstall=()=>{
if (! currentPipeline.downloadUrl) return;
let url=buildUrl("install.html",{custom:currentPipeline.downloadUrl});
window.location.href=url;
}
const uploadConfig=()=>{
let form=document.getElementById("upload");
form.reset();
let fsel=document.getElementById("fileSelect");
fsel.onchange=async ()=>{
if (fsel.files.length < 1) return;
let file=fsel.files[0];
if (! file.name.match(/json$/)){
alert("only json files");
return;
}
try{
let content=await readFile(file,true);
let newConfig=JSON.parse(content);
removeSelectors(ROOT_PATH,true);
config=newConfig;
buildSelectors(ROOT_PATH,structure.config.children,true);
findPipeline();
} catch (e){
alert("upload "+fsel.files[0].name+" failed: "+e);
}
}
fsel.click();
}
const downloadConfig=()=>{
let name=configName;
if (isModified) name=name.replace(/[0-9]*$/,'')+formatDate(undefined,true);
name+=".json";
fileDownload(JSON.stringify(config),name);
}
const showOverlay=(text, isHtml, secondButton)=>{
let el = document.getElementById('overlayContent');
if (isHtml) {
el.innerHTML = text;
el.classList.remove("text");
}
else {
el.textContent = text;
el.classList.add("text");
}
let container = document.getElementById('overlayContainer');
container.classList.remove('hidden');
let db=document.getElementById("secondDialogButton");
if (db) {
if (secondButton && secondButton.callback) {
db.classList.remove("hidden");
if (secondButton.title){db.textContent=secondButton.title}
db.onclick=secondButton.callback;
}
else {
db.classList.add("hidden");
}
}
}
const hideOverlay=()=> {
let container = document.getElementById('overlayContainer');
container.classList.add('hidden');
}
const loadConfig=async (url)=>{
let config=await fetch(url).then((r)=>{
if (!r.ok) throw new Error("unable to fetch: "+r.statusText);
return r.text()
});
let parsed=yamlLoad(config);
return parsed;
}
const showBuildCommand= async ()=>{
let v={};
fillValues(v,['environment','buildflags']);
if (v.environment !== ""){
let help="Run the build from a command line:\n";
let cmd="PLATFORMIO_BUILD_FLAGS=\"";
cmd+=v.buildflags;
cmd+="\" pio run -e "+v.environment;
help+=cmd;
showOverlay(help,false,{title:"Copy",callback:(ev)=>{
try{
navigator.clipboard.writeText(cmd);
//alert("copied:"+cmd);
}
catch (e){
alert("Unable to copy:"+e);
}
}});
}
}
const btConfig={
start:startBuild,
download:runDownload,
webinstall:webInstall,
uploadConfig: uploadConfig,
downloadConfig: downloadConfig,
hideOverlay: hideOverlay,
buildCommand: showBuildCommand
};
const PATH_ATTR='data-path';
const SEPARATOR=':';
const expandObject=(obj,parent)=>{
if (typeof(obj) !== 'object'){
obj={value:obj}
}
let rt=Object.assign({},obj);
if (rt.value === undefined && rt.key !== undefined) rt.value=rt.key;
if (rt.key === undefined) rt.key=rt.value;
if (rt.value === null) rt.value=undefined;
if (rt.label === undefined){
if (rt.value !== undefined) rt.label=rt.value;
else rt.label=rt.key;
}
if (rt.resource === undefined && typeof(parent) === 'object'){
if (parent.resource !== undefined){
if (parent.resource.match(/:$/)){
if(rt.value !== undefined && rt.value !== null){
rt.resource=parent.resource+rt.value;
}
}
else{
rt.resource=parent.resource;
}
}
}
if (rt.target === undefined && typeof(parent) === 'object'){
rt.target=parent.target;
}
if (rt.mandatory === undefined && typeof(parent) === 'object'){
rt.mandatory=parent.mandatory;
}
return rt;
}
const expandList=(lst,parent)=>{
let rt=[];
if (! lst) return rt;
lst.forEach((e)=>rt.push(expandObject(e,parent)));
return rt;
}
const addDescription=(v,frame)=>{
if (frame === undefined) return;
if (v.description){
if(v.url) {
let lnk = addEl('a', 'description', frame, v.description);
lnk.setAttribute('href', v.url);
lnk.setAttribute('target', '_');
}
else{
let de=addEl('div','description',frame,v.description);
}
}
if (v.help){
let bt=addEl('button','help',frame,'?');
bt.addEventListener('click',()=>showOverlay(v.help));
}
else if (v.helpHtml){
let bt=addEl('button','help',frame,'?');
bt.addEventListener('click',()=>showOverlay(v.helpHtml,true));
}
}
/**
*
* @param {build a selector} parent
* @param {*} config
* @param {*} name
* @param {*} current
* @param {*} callback will be called with: children,key,value,initial
* @returns
*/
const buildSelector=(parent,cfgBase,name,current,callback)=>{
let config=expandObject(cfgBase);
if (current === undefined && config.default !== undefined){
current=config.default;
}
let rep=new RegExp("[^"+SEPARATOR+"]*","g");
let level=name.replace(rep,'');
let frame=addEl('div','selector level'+level.length+' t'+config.type,parent);
frame.setAttribute(PATH_ATTR,name);
let inputFrame=addEl('div','inputFrame',frame);
let titleFrame=undefined;
if (config.label !== undefined){
titleFrame=addEl('div','titleFrame t'+config.type,inputFrame);
addEl('div','title t'+config.type,titleFrame,config.label);
}
let initialConfig=undefined
if (config.type === 'frame' || config.type === undefined){
initialConfig=config;
}
let expandedValues=expandList(config.values,config);
expandedValues.forEach((v)=>{
if (v.type !== undefined && v.type !== "frame"){
let err="value element with wrong type "+v.type+" at "+name;
alert(err);
throw new Error(err);
}
})
if (config.type === 'select') {
addDescription(config,titleFrame);
for (let idx=0;idx<expandedValues.length;idx++){
let v=expandedValues[idx];
if (v.key === undefined) continue;
let ef = addEl('div', 'radioFrame', inputFrame);
addEl('div', 'label', ef, v.label);
let re = addEl('input', 'radioCi', ef);
re.setAttribute('type', 'radio');
re.setAttribute('name', name);
re.addEventListener('change', (ev) => callback(v,false));
addDescription(v,ef);
if (v.key == current) {
re.setAttribute('checked','checked');
initialConfig=v;
}
};
}
if (expandedValues.length > 0 && config.type === 'dropdown'){
let sel=addEl('select','t'+config.type,inputFrame);
for (let idx=0;idx<expandedValues.length;idx++){
let v=expandedValues[idx];
if (v.key === undefined) continue;
let opt=addEl('option','',sel,v.label);
opt.setAttribute('value',idx);
if (v.key == current){
opt.setAttribute('selected',true);
initialConfig=v;
}
};
addDescription(config,inputFrame);
sel.addEventListener('change',(ev)=>{
let v=expandedValues[ev.target.value];
if (! v) return;
callback(v,false);
});
}
if (config.type === 'range'){
if (config.min !== undefined && config.max !== undefined) {
let min=config.min+0;
let step=1;
if (config.step !== undefined) step=config.step+0;
let max=config.max+0;
let valid=false;
if (step > 0){
if (min < max) valid=true;
}
else{
if (min > max) {
let tmp=max;
max=min;
min=tmp;
valid=true;
}
}
if (! valid){
console.log("invalid range config",config);
}
else {
let sel = addEl('select', 'tdropdown', inputFrame);
for (let idx=min;idx <=max;idx+=step){
let opt=addEl('option','',sel,idx);
opt.setAttribute('value',idx);
if (idx == current){
opt.setAttribute('selected',true);
initialConfig=expandObject({key:idx,value:idx},config);
}
}
if (! initialConfig){
initialConfig=expandObject({key:min,value:min},config);
}
addDescription(config, inputFrame);
sel.addEventListener('change', (ev) => {
let v = expandObject({ key: ev.target.value, value: ev.target.value },config);
callback(v, false);
});
}
}
}
if (expandedValues.length > 0 && config.type === 'checkbox'){
let act=undefined;
let inact=undefined;
expandedValues.forEach((ev)=>{
if (ev.key === true || ev.key === undefined){
act=ev;
if (act.key === undefined) act.key=true;
return;
}
inact=ev;
});
if (act !== undefined){
if (inact === undefined) inact={key:false};
let cb=addEl('input','t'+config.type,inputFrame);
cb.setAttribute('type','checkbox');
if (current) {
cb.setAttribute('checked',true);
initialConfig=act;
}
else{
initialConfig=inact;
}
addDescription(config,inputFrame);
cb.addEventListener('change',(ev)=>{
if (ev.target.checked){
callback(act,false);
}
else
{
callback(inact,false);
}
});
}
}
let childFrame=addEl('div','childFrame',frame);
if (initialConfig !== undefined){
callback(initialConfig,true,childFrame);
}
return childFrame;
}
const removeSelectors=(prefix,removeValues)=>{
forEachEl('.selectorFrame',(el)=>{
let path=el.getAttribute(PATH_ATTR);
if (! path) return;
if (path.indexOf(prefix) == 0){
el.remove();
}
})
if (removeValues){
let removeKeys=[];
for (let k in configStruct){
if (k.indexOf(prefix) == 0) removeKeys.push(k);
}
for (let k in config){
if (k.indexOf(prefix) == 0) removeKeys.push(k);
}
removeKeys.forEach((k)=>{
delete config[k];
delete configStruct[k];
});
}
}
const buildSelectors=(prefix,configList,initial,base,parent)=>{
if (!parent) parent=document.getElementById("selectors");;
if (!configList) return;
let frame=addEl('div','selectorFrame',parent);
frame.setAttribute(PATH_ATTR,prefix);
let expandedList=expandList(configList);
expandedList.forEach((cfg)=>{
let currentBase=Object.assign({},base,cfg.base);
cfg=replaceValues(cfg,currentBase);
if (cfg.key === undefined){
if (cfg.type !== undefined && cfg.type !== 'frame'){
console.log("config without key",cfg);
return;
}
}
let name=prefix;
if (name !== undefined){
if (cfg.key !== undefined) {
name=prefix+SEPARATOR+cfg.key;
}
}
else{
name=cfg.key;
}
let current=config[name];
let childFrame=buildSelector(frame,cfg,name,current,
(child,initial,opt_frame)=>{
if(cfg.key !== undefined) removeSelectors(name,!initial);
if (! initial) isModified=true;
buildSelectors(name,child.children,initial,currentBase,opt_frame||childFrame);
if (cfg.key !== undefined) configStruct[name]={cfg:child,base:currentBase};
buildValues(initial);
})
})
}
const replaceValues=(str,base)=>{
if (! base) return str;
if (typeof(str) === 'string'){
for (let k in base){
let r=new RegExp("#"+k+"#","g");
str=str.replace(r,base[k]);
}
return str;
}
if (str instanceof Array){
let rt=[];
str.forEach((el)=>{
rt.push(replaceValues(el,base));
})
return rt;
}
if (str instanceof Object){
let rt={};
for (let k in str){
rt[k]=replaceValues(str[k],base);
}
return rt;
}
return str;
}
const ROOT_PATH='root';
const buildValues=(initial)=>{
let environment;
let flags="";
if (! initial){
config={};
}
let allowedResources={};
let currentResources={};
let errors="";
for (let round = 0; round <= 1; round++) {
//round1: find allowed resources
//round2: really collect values
for (let k in configStruct) {
let container = configStruct[k];
if (! container.cfg) continue;
let struct=container.cfg;
if (round > 0) config[k] = struct.key;
if (struct.target !== undefined ) {
if (struct.value === undefined){
if (struct.mandatory && round > 0){
errors+=" missing value for "+k+"\n";
}
continue;
}
let target=replaceValues(struct.target,container.base);
if (target === 'environment' ) {
if (round > 0 && struct.key !== undefined) environment = struct.value;
else allowedResources=struct.resource;
continue;
}
if (round < 1) continue;
if (struct.resource){
let resList=currentResources[struct.resource];
if (! resList){
resList=[];
currentResources[struct.resource]=resList;
}
resList.push(struct);
}
if (target === 'define') {
flags += " -D" + struct.value;
continue;
}
const DEFPRFX = "define:";
if (target.indexOf(DEFPRFX) == 0) {
let def = target.substring(DEFPRFX.length);
flags += " -D" + def + "=" + struct.value;
continue;
}
}
}
}
if (buildVersion !== undefined){
flags+=" -DGWRELEASEVERSION="+buildVersion;
}
setValues({environment:environment,buildflags:flags});
//check resources
for (let k in currentResources){
let ak=k.replace(/:.*/,'');
let resList=currentResources[k];
if (allowedResources[ak] !== undefined){
if (resList.length > allowedResources[ak]){
errors+=" more than "+allowedResources[ak]+" "+k+" device(s) used";
}
}
}
if (errors){
setValue('configError',errors);
setVisible('configError',true,true);
}
else{
setValue('configError','');
setVisible('configError',false,true);
}
if (! initial) findPipeline();
updateStatus();
}
let findIdx=0;
const findPipeline=()=>{
if (delayedSearch !== undefined){
window.clearTimeout(delayedSearch);
delayedSearch=undefined;
}
if (isRunning()) {
delayedSearch=window.setTimeout(findPipeline,500);
return;
}
findIdx++;
let queryIdx=findIdx;
let param={find:1};
fillValues(param,['environment','buildflags']);
if (gitSha !== undefined) param.tag=gitSha;
fetchJson(API,param)
.then((res)=>{
if (queryIdx != findIdx) return;
setCurrentPipeline(res.pipeline);
if (res.pipeline) currentPipeline.status="found";
setDisplayMode('existing');
updateStatus();
fetchStatus(true);
})
.catch((e)=>{
console.log("findPipeline error ",e)
if (displayMode == 'existing'){
setCurrentPipeline();
updateStatus();
}
});
}
const formatDate=(opt_date,opt_includeMs)=>{
const fmt=(v)=>{
return ((v<10)?"0":"")+v;
}
let now=opt_date|| new Date();
let rt=now.getFullYear()+fmt(now.getMonth()+1)+fmt(now.getDate());
if (opt_includeMs){
rt+=fmt(now.getHours())+fmt(now.getMinutes())+fmt(now.getSeconds());
}
return rt;
}
window.onload=async ()=>{
setButtons(btConfig);
let pipeline=window.localStorage.getItem(CURRENT_PIPELINE);
setDisplayMode('last');
if (pipeline){
setCurrentPipeline(pipeline);
updateStatus();
fetchStatus(true);
}
let gitParam={user:GITUSER,repo:GITREPO};
let branch=getParam('branch');
if (branch){
try{
let info=await fetchJson(GITAPI,Object.assign({},gitParam,{branch:branch}));
if (info.object){
gitSha=info.object.sha;
setValue('branchOrTag','branch');
setValue('branchOrTagValue',branch);
}
}catch (e){
console.log("branch query error",e);
}
}
if (gitSha === undefined) {
let tag = getParam('tag');
let type="tag";
if (!tag) {
try {
let relinfo = await fetchJson(GITAPI, Object.assign({}, gitParam, { api: 1 }));
if (relinfo.tag_name) {
tag = relinfo.tag_name;
type="release";
}
else {
alert("unable to query latest release");
}
} catch (e) {
alert("unable to query release info " + e);
}
}
if (tag){
try{
let info=await fetchJson(GITAPI,Object.assign({},gitParam,{tag:tag}));
if (info.object){
gitSha=info.object.sha;
setValue('branchOrTag',type);
setValue('branchOrTagValue',tag);
}
}catch(e){
alert("cannot get sha for tag "+tag+": "+e);
}
}
}
if (gitSha === undefined){
//last resort: no sha, let the CI pick up latest
setValue('gitSha','unknown');
setValue('branchOrTag','branch');
setValue('branchOrTagValue','master');
}
else{
setValue('gitSha',gitSha);
}
let bot=document.getElementById('branchOrTag');
let botv=document.getElementById('branchOrTagValue');
if (bot && botv){
let type=bot.textContent;
let val=botv.textContent;
if (type && val){
if (type != 'release' && type != 'tag' && type != 'branch'){
val=type+val;
}
if (type == 'branch'){
val=val+formatDate();
}
val=val.replace(/[:.]/g,'_');
val=val.replace(/[^a-zA-Z0-9_]*/g,'');
if (val.length > 32){
val=val.substring(val.length-32)
}
if (val.length > 0){
buildVersion=val;
setValue('buildVersion',buildVersion);
}
}
}
if (gitSha !== undefined){
let url=buildUrl(GITAPI,Object.assign({},gitParam,{sha:gitSha,proxy:'webinstall/build.yaml'}));
try{
structure=await loadConfig(url);
}catch (e){
alert("unable to load config for selected release:\n "+e+"\n falling back to default");
}
}
if (! structure){
structure=await loadConfig("build.yaml");
}
let ucfg=getParam('config');
let loadedCfg=undefined;
if (ucfg){
ucfg=ucfg.replace(/[^.a-zA-Z_0-9-]/g,'');
if (gitSha !== undefined){
try{
loadedCfg=await fetchJson(GITAPI,Object.assign({},gitParam,{sha:gitSha,proxy:'webinstall/config/'+ucfg+".json"}));
}catch(e){
alert("unable to load config "+ucfg+" for selected release, trying latest");
}
}
if (loadedCfg === undefined){
try{
loadedCfg=await fetchJson('config/'+ucfg+".json");
}catch(e){
alert("unable to load config "+ucfg+": "+e);
}
}
if (loadedCfg !== undefined){
configName=ucfg;
config=loadedCfg;
}
}
buildSelectors(ROOT_PATH,structure.config.children,true);
if (! isRunning()) findPipeline();
updateStatus();
}
})();

325
webinstall/cibuild.php Normal file
View File

@ -0,0 +1,325 @@
<?php
include("token.php");
include("functions.php");
include("config.php");
if (! isset($CI_TOKEN)) die("no token");
const apiBase="https://circleci.com/api/v2/";
const webApp="https://app.circleci.com/";
const apiRepo="project/gh/#user#/#repo#";
const workflowName="build-workflow";
const jobName="pio-build";
const defaultBranch='master';
const defaultUser='wellenvogel';
const defaultRepo='esp32-nmea2000';
const TABLENAME="CIBUILDS";
const KEEPINTERVAL="30"; //days
function getTokenHeaders(){
global $CI_TOKEN;
return array('Circle-Token'=>$CI_TOKEN);
}
function getPipeline($pipeline){
$url=apiBase."/pipeline/$pipeline";
$token=getTokenHeaders();
return getJson($url,$token,true);
}
function getWorkflow($pipeline,$workflowName){
$url=apiBase."/pipeline/$pipeline/workflow";
$token=getTokenHeaders();
$pstate=getJson($url,$token,true);
if (! isset($pstate['items'])){
throw new Exception("no workflows in pipeline");
}
foreach ($pstate['items'] as $workflow){
if (isset($workflow['name']) && $workflow['name'] == $workflowName){
if (!isset($workflow['id'])){
throw new Exception("no workflow id found");
}
return $workflow;
}
}
throw new Exception("workflow $workflowName not found");
}
function getJob($pipeline,$workflow,$jobName){
$url=apiBase."/workflow/".$workflow."/job";
$token=getTokenHeaders();
$wstate=getJson($url,$token,true);
if (! isset($wstate['items'])){
throw new Exception("no jobs in workflow");
}
foreach ($wstate['items'] as $job){
if (isset($job['name']) && $job['name'] == $jobName){
if (! isset($job['id'])){
throw new Exception("no job id found");
}
return $job;
}
}
throw new Exception("job $jobName not found");
}
function getJobStatus($pipeline,$wf=workflowName,$job=jobName){
$pstat=getPipeline($pipeline);
if (isset($pstat['error'])){
throw new Exception($pstat["error"]);
}
if (! isset($pstat['state'])){
throw new Exception("state not set");
}
if ($pstat['state'] != 'created'){
return $pstat;
}
$pipeline_id=$pstat['id'];
$pipeline_number=$pstat['number'];
$vcs=$pstat['vcs'];
$pstat=getWorkflow($pipeline,$wf);
$workflow_id=$pstat['id'];
$workflow_number=$pstat['workflow_number'];
$pstat=getJob($pipeline,$pstat['id'],$job);
$pstat['pipeline_id']=$pipeline_id;
$pstat['pipeline_number']=$pipeline_number;
$pstat['workflow_id']=$workflow_id;
$pstat['workflow_number']=$workflow_number;
if (isset($pstat['project_slug'])){
$pstat['status_url']=webApp."/pipelines/".
preg_replace('/^gh/','github',$pstat['project_slug'])."/".
$pipeline_number."/workflows/".$workflow_id."/jobs/".$pstat['job_number'];
}
$pstat['vcs']=$vcs;
return $pstat;
}
function getArtifacts($job,$slug){
$url=apiBase."/project/$slug/$job/artifacts";
return getJson($url,getTokenHeaders(),true);
}
function insertPipeline($id,$requestParam){
$database=openDb();
if (! isset($database)) return false;
$param=$requestParam['parameters'];
try {
$status='created';
$tag=null;
if (isset($requestParam['tag'])) $tag=$requestParam['tag'];
$stmt = $database->prepare("INSERT into " . TABLENAME .
"(id,status,config,environment,buildflags,tag) VALUES (?,?,?,?,?,?)");
$stmt->bind_param("ssssss",
$id,
$status,
$param['config'],
$param['environment'],
$param['build_flags'],
$tag);
$stmt->execute();
$database->query("DELETE from ". TABLENAME. " where timestamp < NOW() - interval ". KEEPINTERVAL. " DAY");
return true;
} catch (Exception $e) {
error_log("insert pipeline $id failed: $e");
return false;
}
}
function updatePipeline($id,$status,$tag=null){
$database=openDb();
if (! isset($database)) return false;
try{
$stmt=null;
if ($tag != null){
$stmt=$database->prepare("UPDATE ".TABLENAME." SET status=?,tag=? where id=? and ( status <> ? or tag <> ?)");
$stmt->bind_param("sssss",$status,$tag,$id,$status,$tag);
$stmt->execute();
}
else{
$stmt=$database->prepare("UPDATE ".TABLENAME." SET status=? where id=? AND status <> ?");
$stmt->bind_param("sss",$status,$id,$status);
$stmt->execute();
}
}catch (Exception $e){
error_log("update pipeline $id failed: $e");
return false;
}
return true;
}
function findPipeline($param)
{
$database=openDb();
if (!isset($database))
return false;
try {
$stmt = null;
$database->query("DELETE from ". TABLENAME. " where timestamp < NOW() - interval ". KEEPINTERVAL. " DAY");
if (isset($param['tag'])) {
$stmt = $database->prepare("SELECT id,UNIX_TIMESTAMP(timestamp) from " . TABLENAME .
" where status IN('success','running','created') and environment=? and buildflags=? and tag=? order by timestamp desc");
$stmt->bind_param("sss", $param['environment'], $param['buildflags'], $param['tag']);
} else {
$stmt = $database->prepare("SELECT id,UNIX_TIMESTAMP(timestamp) from " . TABLENAME .
" where status IN('success','running','created') and environment=? and buildflags=? order by timestamp desc");
$stmt->bind_param("ss", $param['environment'], $param['buildflags']);
}
$stmt->execute();
$id=null;
$timestamp=null;
$stmt->bind_result($id,$timestamp);
if ($stmt->fetch()){
return array('pipeline'=>$id,'timestamp'=>$timestamp);
}
return false;
} catch (Exception $e) {
error_log("find pipeline failed: $e");
return false;
}
}
function getArtifactsForPipeline($pipeline,$wf=workflowName,$job=jobName){
$jstat=getJobStatus($pipeline,$wf,$job);
if (! isset($jstat['job_number'])){
throw new Exception("no job number");
}
if (! isset($jstat['status'])){
throw new Exception("no job status");
}
if ($jstat['status'] != 'success'){
throw new Exception("invalid job status ".$jstat['status']);
}
$astat=getArtifacts($jstat['job_number'],$jstat['project_slug']);
return $astat;
}
try {
if (isset($_REQUEST['api'])) {
$action = $_REQUEST['api'];
header("Content-Type: application/json");
$par = array();
if ($action == 'status') {
addVars(
$par,
['pipeline', 'workflow', 'job'],
array('workflow' => workflowName, 'job' => jobName)
);
try {
$pstat = getJobStatus($par['pipeline'], $par['workflow'], $par['job']);
if (isset($pstat['vcs'])){
updatePipeline($par['pipeline'],$pstat['status'],$pstat['vcs']['revision']);
}
else{
updatePipeline($par['pipeline'],$pstat['status']);
}
echo (json_encode($pstat));
} catch (Exception $e) {
$rt = array('status' => 'error', 'error' => $e->getMessage());
echo (json_encode($rt));
}
exit(0);
}
if ($action == 'artifacts') {
addVars(
$par,
['pipeline', 'workflow', 'job'],
array('workflow' => workflowName, 'job' => jobName)
);
try {
$astat = getArtifactsForPipeline($par['pipeline'], $par['workflow'], $par['job']);
echo (json_encode($astat));
} catch (Exception $e) {
echo (json_encode(array('status' => 'error', 'error' => $e->getMessage())));
}
exit(0);
}
if ($action == 'pipeline'){
addVars(
$par,
['number','user','repo'],
array('user'=>defaultUser,'repo'=>defaultRepo)
);
$url=apiBase."/".replaceVars(apiRepo,fillUserAndRepo(null,$par))."/pipeline/".$par['number'];
$rt=getJson($url,getTokenHeaders(),true);
echo(json_encode($rt));
exit(0);
}
if ($action == 'pipelineuuid'){
addVars(
$par,
['pipeline']
);
$url=apiBase."/pipeline/".$par['pipeline'];
$rt=getJson($url,getTokenHeaders(),true);
echo(json_encode($rt));
exit(0);
}
if ($action == 'start'){
addVars(
$par,
['environment','buildflags','config','suffix','user','repo'],
array('suffix'=>'',
'config'=>'{}',
'user'=>defaultUser,
'repo'=>defaultRepo,
'buildflags'=>''
)
);
$requestParam=array(
'parameters'=> array(
'run_build'=>true,
'environment'=>$par['environment'],
'suffix'=>$par['suffix'],
'config'=>$par['config'],
'build_flags'=>$par['buildflags']
)
);
if (isset($_REQUEST['tag'])){
$requestParam['tag']=safeName($_REQUEST['tag']);
}
else{
$requestParam['branch']=defaultBranch;
}
$userRepo=fillUserAndRepo(null,$par);
$url=apiBase."/".replaceVars(apiRepo,$userRepo)."/pipeline";
$rt=getJson($url,getTokenHeaders(),true,$requestParam);
insertPipeline($rt['id'],$requestParam);
echo (json_encode($rt));
exit(0);
}
throw new Exception("invalid api $action");
}
if (isset($_REQUEST['download'])) {
$pipeline = $_REQUEST['download'];
$par = array('pipeline' => $pipeline);
addVars(
$par,
['workflow', 'job'],
array('workflow' => workflowName, 'job' => jobName)
);
$astat = getArtifactsForPipeline($par['pipeline'], $par['workflow'], $par['job']);
if (!isset($astat['items']) || count($astat['items']) < 1) {
die("no artifacts for job");
}
$dlurl = $astat['items'][0]['url'];
#echo("DL: $dlurl\n");
header('Content-Disposition: attachment; filename="'.$astat['items'][0]['path'].'"');
proxy($dlurl);
exit(0);
}
if (isset($_REQUEST['find'])){
$par=array();
addVars($par,['environment','buildflags']);
if (isset($_REQUEST['tag'])) $par['tag']=$_REQUEST['tag'];
$rt=findPipeline($par);
header("Content-Type: application/json");
if (!$rt){
$rt=array();
}
$rt['status']='OK';
echo(json_encode($rt));
exit(0);
}
die("no action");
} catch (HTTPErrorException $h) {
header($_SERVER['SERVER_PROTOCOL'] . " " . $h->code . " " . $h->getMessage());
die($h->getMessage());
} catch (Exception $e) {
header($_SERVER['SERVER_PROTOCOL'] . ' 500 ' . $e->getMessage());
die($e->getMessage());
}
?>

29
webinstall/config.php Normal file
View File

@ -0,0 +1,29 @@
<?php
$allowed=array(
'user'=> array('wellenvogel'),
'repo'=> array('esp32-nmea2000')
);
function fillUserAndRepo($vars=null,$source=null){
global $allowed;
if ($vars == null) {
$vars=array();
}
if ($source == null){
$source=$_REQUEST;
}
foreach (array('user','repo') as $n){
if (! isset($source[$n])){
throw new Exception("missing parameter $n");
}
$v=$source[$n];
$av=$allowed[$n];
if (! in_array($v,$av)){
throw new Exception("value $v for $n not allowed");
}
$vars[$n]=$v;
}
return $vars;
}
?>

View File

@ -0,0 +1 @@
{"root:board":"m5stack-atom-generic","root:board:m5groove":"CAN","root:board:m5groove:m5groovecan":"M5_CANUNIT"}

View File

@ -0,0 +1 @@
{"root:board":"m5stack-atom-generic","root:board:m5lightbase":"M5_GPS_KIT","root:board:m5groove":"CAN","root:board:m5groove:m5groovecan":"M5_CANUNIT"}

View File

@ -0,0 +1 @@
{"root:board":"m5stack-atom-generic","root:board:m5lightbase":"M5_SERIAL_KIT_232","root:board:m5groove":"CAN","root:board:m5groove:m5groovecan":"M5_CANUNIT"}

View File

@ -0,0 +1 @@
{"root:board":"m5stack-atom-generic","root:board:m5lightbase":"M5_SERIAL_KIT_485","root:board:m5groove":"CAN","root:board:m5groove:m5groovecan":"M5_CANUNIT"}

View File

@ -0,0 +1 @@
{"root:board":"m5stack-atom-generic","root:board:m5lightbase":"M5_CAN_KIT","root:board:m5groove":"Serial","root:board:m5groove:m5grooveserial":"tail485"}

View File

@ -0,0 +1 @@
{"root:board":"m5stickc-atom-generic","root:board:m5groove":"CAN","root:board:m5groove:m5groovecan":"M5_CANUNIT"}

View File

@ -0,0 +1 @@
{"root:board:serial2":false,"root:board:led":false,"root:board":"nodemcu-generic","root:board:can":true,"root:board:can:tx":5,"root:board:can:rx":4,"root:board:resetButton":true,"root:board:resetButton:button":0,"root:board:resetButton:resetButtonMode":0,"root:board:resetButton:resetButtonPUD":true,"root:board:serial1":true,"root:board:serial1:type":"rx","root:board:serial1:type:RX":16}

28
webinstall/create_db.php Normal file
View File

@ -0,0 +1,28 @@
<?php
echo "CREATEDB\n";
include("functions.php");
$database=openDb();
if (! isset($database)){
die("ERROR: db not set\n");
}
try{
$sql="CREATE TABLE `CIBUILDS` (
`id` varchar(255) NOT NULL,
`tag` varchar(255),
`status` varchar(255) NOT NULL,
`config` longtext,
`environment` VARCHAR(255),
`buildflags` longtext,
`timestamp` timestamp ,
PRIMARY KEY (`id`)
);";
echo "executing $sql<br>";
$rt=$database->query($sql);
echo "execute OK<br>";
} catch (Exception $e){
echo "ERROR: ".$e;
}
?>

236
webinstall/functions.php Normal file
View File

@ -0,0 +1,236 @@
<?php
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
include("../../../cibuild_db.php");
function openDb(){
if (! defined('database::SERVER')) return null;
try{
$db=new mysqli(database::SERVER, database::USER, database::PASS,database::DB);
#$db->query("SET CHARACTER SET 'utf8'");
return $db;
}catch (Exception $e){
error_log("openDB error $e");
}
return null;
}
function safeName($name)
{
return preg_replace('[^0-9_a-zA-Z.-]', '', $name);
}
function replaceVars($str, $vars)
{
foreach ($vars as $n => &$v) {
$str = str_replace("#" . $n . "#", $v, $str);
}
return $str;
}
if (!function_exists('getallheaders')) {
function getallheaders()
{
$headers = [];
foreach ($_SERVER as $name => $value) {
if (substr($name, 0, 5) == 'HTTP_') {
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
}
}
return $headers;
}
}
function addVars(&$vars,$names,$defaults=null){
foreach ($names as $n){
$v=null;
if (! isset($_REQUEST[$n])){
if ($defaults == null || ! isset($defaults[$n])) throw new Exception("missing parameter $n");
$v=$defaults[$n];
}
else{
$v=safeName($_REQUEST[$n]);
}
$vars[$n]=$v;
}
return $vars;
}
function curl_exec_follow(/*resource*/ $ch, /*int*/ &$maxredirect = null) {
$mr = $maxredirect === null ? 5 : intval($maxredirect);
#echo("###handling redirects $mr\n");
if (ini_get('open_basedir') == '' && ini_get('safe_mode' == 'Off') && false) {
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, $mr > 0);
curl_setopt($ch, CURLOPT_MAXREDIRS, $mr);
} else {
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
if ($mr > 0) {
$newurl = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
$rch = curl_copy_handle($ch);
curl_setopt($rch, CURLOPT_HEADER, true);
#curl_setopt($rch, CURLOPT_NOBODY, true);
curl_setopt($rch, CURLOPT_FORBID_REUSE, false);
curl_setopt($rch, CURLOPT_RETURNTRANSFER, true);
do {
#echo("###trying $newurl\n");
curl_setopt($rch, CURLOPT_URL, $newurl);
curl_setopt($ch, CURLOPT_URL, $newurl);
$header = curl_exec($rch);
if (curl_errno($rch)) {
$code = 0;
} else {
$code = curl_getinfo($rch, CURLINFO_HTTP_CODE);
#echo("###code=$code\n");
if ($code == 301 || $code == 302) {
preg_match('/Location:(.*?)\n/', $header, $matches);
$newurl = trim(array_pop($matches));
} else {
if ($code >= 300){
trigger_error("HTTP error $code");
}
$code = 0;
}
}
} while ($code && --$mr);
curl_close($rch);
if (!$mr) {
if ($maxredirect === null) {
trigger_error('Too many redirects. When following redirects, libcurl hit the maximum amount.', E_USER_WARNING);
} else {
$maxredirect = 0;
}
return false;
}
curl_setopt($ch, CURLOPT_URL, $newurl);
}
}
curl_setopt(
$ch,
CURLOPT_HEADERFUNCTION,
function ($curl, $header) {
header($header);
return strlen($header);
}
);
curl_setopt(
$ch,
CURLOPT_WRITEFUNCTION,
function ($curl, $body) {
echo $body;
return strlen($body);
}
);
header('Access-Control-Allow-Origin:*');
return curl_exec($ch);
}
function getFwHeaders($aheaders=null){
$headers=getallheaders();
$FWHDR = ['User-Agent'];
$outHeaders = array();
foreach ($FWHDR as $k) {
if (isset($headers[$k])) {
array_push($outHeaders, "$k: $headers[$k]");
}
}
if ($aheaders != null){
foreach ($aheaders as $hk => $hv){
array_push($outHeaders,"$hk: $hv");
}
}
return $outHeaders;
}
function getJson($url,$headers=null,$doThrow=false,$jsonData=null){
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL,$url);
curl_setopt($curl,CURLOPT_RETURNTRANSFER, true);
$outHeaders=getFwHeaders($headers);
if ($jsonData != null){
$json=json_encode($jsonData);
array_push($outHeaders,"Content-Type: application/json");
array_push($outHeaders,"Content-length: ".strlen($json));
curl_setopt($curl, CURLOPT_POSTFIELDS,$json);
}
curl_setopt($curl, CURLOPT_HTTPHEADER, $outHeaders);
$response = curl_exec($curl);
$httpcode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
#echo("curl exec for $url:$response:$httpcode\n");
if($e = curl_error($curl)) {
curl_close($curl);
if ($doThrow) throw new Exception($e);
return array('error'=>$e);
} else {
if ($httpcode >= 300){
curl_close($curl);
if ($doThrow) throw new Exception("HTTP error $httpcode");
return array('error'=>"HTTP code ".$httpcode);
}
curl_close($curl);
return json_decode($response, true);
}
}
class HTTPErrorException extends Exception{
public $code=0;
public function __construct($c,$text){
parent::__construct($text);
$this->code=$c;
}
};
function proxy_impl($url, $timeout=30,$headers=null,$num = 5)
{
$nexturl=$url;
while ($num > 0 && $nexturl != null) {
$num--;
$code=0;
$ch = curl_init($nexturl);
$nexturl=null;
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
curl_setopt($ch, CURLOPT_TIMEOUT,$timeout);
if ($headers != null){
curl_setopt($ch,CURLOPT_HTTPHEADER,$headers);
}
curl_setopt(
$ch,
CURLOPT_HEADERFUNCTION,
function ($curl, $header) use(&$nexturl,&$code){
#echo ("###header:$header\n");
if ($code == 0){
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
}
#echo ("???code=$code\n");
if ($code == 301 || $code == 302) {
if(preg_match('/[Ll]ocation:(.*?)\n/', $header, $matches)){
$nexturl = trim(array_pop($matches));
#echo("???nexturl=$nexturl\n");
}
}
if ($code != 0 && $code < 300){
header($header);
}
return strlen($header);
}
);
curl_setopt(
$ch,
CURLOPT_WRITEFUNCTION,
function ($curl, $body) use(&$code) {
if ($code != 0 && $code < 300){
#echo ("### body part " . strlen($body)."\n");
echo $body;
return strlen($body);
}
return false;
}
);
$rs = curl_exec($ch);
#echo ("###code=$code\n");
curl_close($ch);
if ($nexturl == null){
if ($code != 200) throw new HTTPErrorException($code,"HTTP status $code");
return true;
}
}
throw new HTTPErrorException(500,"too many redirects");
}
function proxy($url)
{
header('Access-Control-Allow-Origin:*');
return proxy_impl($url,30,getFwHeaders());
}
?>

140
webinstall/helper.js Normal file
View File

@ -0,0 +1,140 @@
const getParam = (key,opt_default) => {
if (opt_default === undefined) opt_default="";
let value = RegExp("" + key + "[^&]+").exec(window.location.search);
// Return the unescaped value minus everything starting from the equals sign or an empty string
return decodeURIComponent(!!value ? value.toString().replace(/^[^=]+./, "") : opt_default);
};
/**
* add an HTML element
* @param {*} type
* @param {*} clazz
* @param {*} parent
* @param {*} text
* @returns
*/
const addEl = (type, clazz, parent, text) => {
let el = document.createElement(type);
if (clazz) {
if (!(clazz instanceof Array)) {
clazz = clazz.split(/ */);
}
clazz.forEach(function (ce) {
el.classList.add(ce);
});
}
if (text !== undefined) el.textContent = text;
if (parent) parent.appendChild(el);
return el;
}
/**
* call a function for each matching element
* @param {*} selector
* @param {*} cb
*/
const forEachEl = (selector, cb) => {
let arr = document.querySelectorAll(selector);
for (let i = 0; i < arr.length; i++) {
cb(arr[i]);
}
}
const setButtons=(config)=>{
for (let k in config){
let bt=document.getElementById(k);
if (bt){
bt.addEventListener('click',config[k]);
}
}
}
const fillValues=(values,items)=>{
items.forEach((it)=>{
let e=document.getElementById(it);
if (e){
if (e.tagName == 'INPUT') values[it]=e.value;
if (e.tagName == 'DIV' || e.tagName == 'SPAN') values [it]=e.textContent;
}
})
};
const setValue=(id,value)=>{
let el=document.getElementById(id);
if (! el) return;
if (el.tagName == 'DIV' || el.tagName == 'SPAN' || el.tagName == 'P'){
el.textContent=value;
return;
}
if (el.tagName == 'INPUT'){
el.value=value;
return;
}
if (el.tagName.match(/^H[0-9]/)){
el.textContent=value;
return;
}
if (el.tagName == 'A'){
el.setAttribute('href',value);
return;
}
}
const setValues=(data,translations)=>{
for (let k in data){
let id=k;
if (translations){
let t=translations[k];
if (t !== undefined) id=t;
}
setValue(id,data[k]);
}
}
const buildUrl=(url,pars)=>{
let delim=(url.match("[?]"))?"&":"?";
for (let k in pars){
url+=delim;
delim="&";
url+=encodeURIComponent(k);
url+="=";
url+=encodeURIComponent(pars[k]);
}
return url;
}
const fetchJson=(url,pars)=>{
let furl=buildUrl(url,pars);
return fetch(furl).then((rs)=>rs.json());
}
const setVisible=(el,vis,useParent)=>{
if (typeof(el) !== 'object') el=document.getElementById(el);
if (! el) return;
if (useParent) el=el.parentElement;
if (! el) return;
if (vis) el.classList.remove('hidden');
else el.classList.add('hidden');
}
const enableEl=(id,en)=>{
let el=document.getElementById(id);
if (!el) return;
if (en) el.disabled=false;
else el.disabled=true;
}
const fillSelect=(el,values)=>{
if (typeof(el) !== 'object') el=document.getElementById(el);
if (! el) return;
el.textContent='';
let kf=(values instanceof Array)?(k)=>values[k]:(k)=>k;
for (let k in values){
let o=addEl('option','',el);
o.setAttribute('value',kf(k));
o.textContent=values[k];
}
}
const readFile=(file,optAsText)=>{
return new Promise((resolve,reject)=>{
let reader = new FileReader();
reader.addEventListener('load', function (e) {
resolve(e.target.result);
});
reader.addEventListener('error',(e)=>reject(e));
if (optAsText) reader.readAsText(file);
else reader.readAsBinaryString(file);
});
}
export { readFile, getParam, addEl, forEachEl,setButtons,fillValues, setValue,setValues,buildUrl,fetchJson,setVisible, enableEl,fillSelect }

View File

@ -21,4 +21,25 @@ body {
font-size: 16px;
font-family: system-ui;
line-height: 1.5em;
}
#loading{
height: 6em;
}
#loadingText{
text-align: center;
}
#loadingFrame{
display: flex;
flex-direction: column;
align-items: center;
}
.hidden{
display: none !important;
}
.uploadFile{
width: 0;
height: 0;
}
.uploadButton{
margin-left: 0.5em;
}

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