diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 0000000..955589d
--- /dev/null
+++ b/.circleci/config.yml
@@ -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: /.*/
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index bf2c408..2562875 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,7 +7,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- os: [ubuntu-20.04]
+ os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
diff --git a/.gitignore b/.gitignore
index 339663e..b5ca1aa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,5 @@
.vscode/launch.json
.vscode/ipch
generated/*
-lib/generated
\ No newline at end of file
+lib/generated
+webinstall/token.php
diff --git a/Readme.md b/Readme.md
index adc8ec4..54c75b0 100644
--- a/Readme.md
+++ b/Readme.md
@@ -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.
-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 __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
diff --git a/doc/BuildService.md b/doc/BuildService.md
new file mode 100644
index 0000000..6c01aaf
--- /dev/null
+++ b/doc/BuildService.md
@@ -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.
+.
+
+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).
+
+.
+
+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.
diff --git a/doc/CiBuild1.png b/doc/CiBuild1.png
new file mode 100644
index 0000000..26c404f
Binary files /dev/null and b/doc/CiBuild1.png differ
diff --git a/doc/CiBuild2.png b/doc/CiBuild2.png
new file mode 100644
index 0000000..b1ffac2
Binary files /dev/null and b/doc/CiBuild2.png differ
diff --git a/doc/Hardware.md b/doc/Hardware.md
index 988c8d6..07d7694 100644
--- a/doc/Hardware.md
+++ b/doc/Hardware.md
@@ -78,6 +78,13 @@ Can be used e.g. as an NMEA2000 Adapter for a laptop running e.g. OpenCPN with t


+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)
diff --git a/extra_script.py b/extra_script.py
index 0331e39..ea5a8f7 100644
--- a/extra_script.py
+++ b/extra_script.py
@@ -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)
diff --git a/lib/api/GwApi.h b/lib/api/GwApi.h
index b6be1cc..95892cf 100644
--- a/lib/api/GwApi.h
+++ b/lib/api/GwApi.h
@@ -5,6 +5,10 @@
#include "NMEA0183Msg.h"
#include "GWConfig.h"
#include "GwBoatData.h"
+#include "GwXDRMappings.h"
+#include