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. +![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. 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 ![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) 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 +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; + 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 + bool set(const T &v){ + return false; + } + template + T get(int &res){ + res=-1; + return T(); + } + template + 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 +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.h or GwI.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(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(const String &task) {\ + return iclaim(#type,task);\ + }\ + +#ifndef DECLARE_TASKIF + #define DECLARE_TASKIF(type) DECLARE_TASKIF_IMPL(type) +#endif #endif diff --git a/lib/appinfo/GwAppInfo.h b/lib/appinfo/GwAppInfo.h index 12ff16d..f7eb143 100644 --- a/lib/appinfo/GwAppInfo.h +++ b/lib/appinfo/GwAppInfo.h @@ -15,4 +15,5 @@ #endif #endif -#define FIRMWARE_TYPE GWSTRINGIFY(PIO_ENV_BUILD) \ No newline at end of file +#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) \ No newline at end of file diff --git a/lib/boatData/GwBoatData.h b/lib/boatData/GwBoatData.h index 71742cf..67052dc 100644 --- a/lib/boatData/GwBoatData.h +++ b/lib/boatData/GwBoatData.h @@ -4,8 +4,9 @@ #include "GwLog.h" #include #include +#include #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 *name=new GwBoatItem(F(#name),GwBoatItemBase::fmt,time,&values) ; + static constexpr const char* _##name=#name; \ + GwBoatItem *name=new GwBoatItem(#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; diff --git a/lib/buttons/GwButtons.h b/lib/buttons/GwButtons.h deleted file mode 100644 index 6218e5d..0000000 --- a/lib/buttons/GwButtons.h +++ /dev/null @@ -1,5 +0,0 @@ -#ifndef _GWBUTTONS_H -#define _GWBUTTONS_H -//task function -void handleButtons(void *param); -#endif \ No newline at end of file diff --git a/lib/buttons/GwButtons.cpp b/lib/buttontask/GwButtonTask.cpp similarity index 63% rename from lib/buttons/GwButtons.cpp rename to lib/buttontask/GwButtonTask.cpp index c169e48..908c3fa 100644 --- a/lib/buttons/GwButtons.cpp +++ b/lib/buttontask/GwButtonTask.cpp @@ -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(taskname); } \ No newline at end of file diff --git a/lib/buttontask/GwButtonTask.h b/lib/buttontask/GwButtonTask.h new file mode 100644 index 0000000..107b831 --- /dev/null +++ b/lib/buttontask/GwButtonTask.h @@ -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 \ No newline at end of file diff --git a/lib/channel/GwChannel.cpp b/lib/channel/GwChannel.cpp index d25841d..ef9d64e 100644 --- a/lib/channel/GwChannel.cpp +++ b/lib/channel/GwChannel.cpp @@ -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("count")+name+String("in")); this->countOut=new GwCounter(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); } diff --git a/lib/channel/GwChannelList.cpp b/lib/channel/GwChannelList.cpp index 635799a..c094a0a 100644 --- a/lib/channel/GwChannelList.cpp +++ b/lib/channel/GwChannelList.cpp @@ -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;idxlogDebug(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(); diff --git a/lib/channel/GwChannelList.h b/lib/channel/GwChannelList.h index cbef1d4..1bddfca 100644 --- a/lib/channel/GwChannelList.h +++ b/lib/channel/GwChannelList.h @@ -1,19 +1,22 @@ #pragma once #include #include +#include #include #include "GwChannel.h" #include "GwLog.h" #include "GWConfig.h" #include "GwJsonDocument.h" #include "GwApi.h" +#include //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 ChannelList; ChannelList theChannels; - + std::map 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 ChannelAction; @@ -40,6 +44,6 @@ class GwChannelList{ //single channel GwChannel *getChannelById(int sourceId); void fillStatus(GwApi::Status &status); - + String getMode(int id); }; diff --git a/lib/config/GWConfig.cpp b/lib/config/GWConfig.cpp index cdbfdbe..28528c4 100644 --- a/lib/config/GWConfig.cpp +++ b/lib/config/GWConfig.cpp @@ -1,7 +1,11 @@ +#define CFG_MESSAGES +#include #include "GWConfig.h" #include #include #include +#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;igetName().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;igetName().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 GwConfigHandler::getSpecial() const{ + std::vector rt; + rt.reserve(numSpecial()); + for (int i=0L;igetType() != GwConfigInterface::NORMAL){ + rt.push_back(configs[i]->getName()); + }; + } + return rt; +} +int GwConfigHandler::numSpecial() const{ + int rt=0; + for (int i=0L;igetType() != GwConfigInterface::NORMAL) rt++; + } + return rt; +} + void GwNmeaFilter::handleToken(String token, int index){ switch(index){ case 0: diff --git a/lib/config/GWConfig.h b/lib/config/GWConfig.h index 4d0b105..521d7a2 100644 --- a/lib/config/GWConfig.h +++ b/lib/config/GWConfig.h @@ -1,19 +1,20 @@ #ifndef _GWCONFIG_H #define _GWCONFIG_H #include -#include #include "GwLog.h" #include "GwConfigItem.h" #include "GwConfigDefinitions.h" #include +#include - +class Preferences; class GwConfigHandler: public GwConfigDefinitions{ private: - Preferences prefs; + Preferences *prefs; GwLog *logger; typedef std::map 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 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 + 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 \ No newline at end of file diff --git a/lib/config/GwConfigItem.h b/lib/config/GwConfigItem.h index adfb161..12f5e97 100644 --- a/lib/config/GwConfigItem.h +++ b/lib/config/GwConfigItem.h @@ -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 \ No newline at end of file diff --git a/lib/counter/GwCounter.h b/lib/counter/GwCounter.h index 2280328..a8d4267 100644 --- a/lib/counter/GwCounter.h +++ b/lib/counter/GwCounter.h @@ -11,9 +11,12 @@ template 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(); diff --git a/lib/exampletask/GwExampleTask.cpp b/lib/exampletask/GwExampleTask.cpp index 70dc9fa..340b785 100644 --- a/lib/exampletask/GwExampleTask.cpp +++ b/lib/exampletask/GwExampleTask.cpp @@ -3,20 +3,86 @@ #ifdef BOARD_TEST #include "GwExampleTask.h" #include "GwApi.h" +#include "GWConfig.h" #include - +#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(taskName); + //not working interface + if (!api->taskInterfaces()->claim(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(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(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(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); diff --git a/lib/exampletask/GwExampleTask.h b/lib/exampletask/GwExampleTask.h index 5dbb5b6..bc65ae9 100644 --- a/lib/exampletask/GwExampleTask.h +++ b/lib/exampletask/GwExampleTask.h @@ -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 \ No newline at end of file diff --git a/lib/exampletask/Readme.md b/lib/exampletask/Readme.md index 86b15eb..965e177 100644 --- a/lib/exampletask/Readme.md +++ b/lib/exampletask/Readme.md @@ -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.
+ 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.
+ 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).
+ This gives you more flexibility - maybe you only want to start your task if certain config values are set.
+ 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.
+ 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)
+ * [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)
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.
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).
+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.
+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.
+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. diff --git a/lib/exampletask/config.json b/lib/exampletask/config.json index 9074b25..7913fb7 100644 --- a/lib/exampletask/config.json +++ b/lib/exampletask/config.json @@ -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" + } } -] \ No newline at end of file +] diff --git a/lib/exampletask/platformio.ini b/lib/exampletask/platformio.ini index b2cd394..096a7cd 100644 --- a/lib/exampletask/platformio.ini +++ b/lib/exampletask/platformio.ini @@ -7,7 +7,6 @@ board = m5stack-atom lib_deps = ${env.lib_deps} - own_lib build_flags= -D BOARD_TEST ${env.build_flags} diff --git a/lib/update/GwUpdate.cpp b/lib/gwupdate/GwUpdate.cpp similarity index 100% rename from lib/update/GwUpdate.cpp rename to lib/gwupdate/GwUpdate.cpp diff --git a/lib/update/GwUpdate.h b/lib/gwupdate/GwUpdate.h similarity index 100% rename from lib/update/GwUpdate.h rename to lib/gwupdate/GwUpdate.h diff --git a/lib/webserver/GwWebServer.cpp b/lib/gwwebserver/GwWebServer.cpp similarity index 99% rename from lib/webserver/GwWebServer.cpp rename to lib/gwwebserver/GwWebServer.cpp index 2b3a79a..6261c42 100644 --- a/lib/webserver/GwWebServer.cpp +++ b/lib/gwwebserver/GwWebServer.cpp @@ -64,7 +64,6 @@ void GwWebServer::begin(){ GwWebServer::~GwWebServer(){ server->end(); delete server; - vQueueDelete(queue); } void GwWebServer::handleAsyncWebRequest(AsyncWebServerRequest *request, GwRequestMessage *msg) { diff --git a/lib/webserver/GwWebServer.h b/lib/gwwebserver/GwWebServer.h similarity index 100% rename from lib/webserver/GwWebServer.h rename to lib/gwwebserver/GwWebServer.h diff --git a/lib/wifi/GWWifi.h b/lib/gwwifi/GWWifi.h similarity index 100% rename from lib/wifi/GWWifi.h rename to lib/gwwifi/GWWifi.h diff --git a/lib/wifi/GwWifi.cpp b/lib/gwwifi/GwWifi.cpp similarity index 70% rename from lib/wifi/GwWifi.cpp rename to lib/gwwifi/GwWifi.cpp index 2f6f556..549f99d 100644 --- a/lib/wifi/GwWifi.cpp +++ b/lib/gwwifi/GwWifi.cpp @@ -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; } diff --git a/lib/hardware/GwHardware.h b/lib/hardware/GwHardware.h index 8b6dd9f..ae4e640 100644 --- a/lib/hardware/GwHardware.h +++ b/lib/hardware/GwHardware.h @@ -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 +#include +#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 diff --git a/lib/iictask/GwBME280.cpp b/lib/iictask/GwBME280.cpp new file mode 100644 index 0000000..9804e7e --- /dev/null +++ b/lib/iictask/GwBME280.cpp @@ -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 +#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 + diff --git a/lib/iictask/GwBME280.h b/lib/iictask/GwBME280.h new file mode 100644 index 0000000..03bfe57 --- /dev/null +++ b/lib/iictask/GwBME280.h @@ -0,0 +1,5 @@ +#ifndef _GWBME280_H +#define _GWBME280_H +#include "GwIicSensors.h" +void registerBME280(GwApi *api,SensorList &sensors); +#endif \ No newline at end of file diff --git a/lib/iictask/GwIicSensors.h b/lib/iictask/GwIicSensors.h new file mode 100644 index 0000000..313e82b --- /dev/null +++ b/lib/iictask/GwIicSensors.h @@ -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 +#else + class TwoWire; +#endif + +#define CFG_GET(name,prefix) \ + cfg->getValue(name, GwConfigDefinitions::prefix ## name) + +template +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 +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 +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 +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 +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 +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{ + 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::vector; +}; + +#define CHECK_IIC1() checkDef(GWIIC_SCL,GWIIC_SDA) +#define CHECK_IIC2() checkDef(GWIIC_SCL2,GWIIC_SDA2) + +#endif \ No newline at end of file diff --git a/lib/iictask/GwIicTask.cpp b/lib/iictask/GwIicTask.cpp new file mode 100644 index 0000000..51f3633 --- /dev/null +++ b/lib/iictask/GwIicTask.cpp @@ -0,0 +1,155 @@ +#include "GwIicTask.h" +#include "GwIicSensors.h" +#include "GwHardware.h" +#include "GwBME280.h" +#include "GwQMP6988.h" +#include "GwSHT3X.h" +#include + +#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 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 diff --git a/lib/iictask/GwIicTask.h b/lib/iictask/GwIicTask.h new file mode 100644 index 0000000..e26eb28 --- /dev/null +++ b/lib/iictask/GwIicTask.h @@ -0,0 +1,6 @@ +#ifndef _GWIICTASK_H +#define _GWIICTASK_H +#include "GwApi.h" +void initIicTask(GwApi *api); +DECLARE_INITFUNCTION(initIicTask); +#endif \ No newline at end of file diff --git a/lib/iictask/GwQMP6988.cpp b/lib/iictask/GwQMP6988.cpp new file mode 100644 index 0000000..1f83199 --- /dev/null +++ b/lib/iictask/GwQMP6988.cpp @@ -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 \ No newline at end of file diff --git a/lib/iictask/GwQMP6988.h b/lib/iictask/GwQMP6988.h new file mode 100644 index 0000000..43e6426 --- /dev/null +++ b/lib/iictask/GwQMP6988.h @@ -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 \ No newline at end of file diff --git a/lib/iictask/GwSHT3X.cpp b/lib/iictask/GwSHT3X.cpp new file mode 100644 index 0000000..e98647c --- /dev/null +++ b/lib/iictask/GwSHT3X.cpp @@ -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 + + + diff --git a/lib/iictask/GwSHT3X.h b/lib/iictask/GwSHT3X.h new file mode 100644 index 0000000..085cab7 --- /dev/null +++ b/lib/iictask/GwSHT3X.h @@ -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 \ No newline at end of file diff --git a/lib/iictask/QMP6988.cpp b/lib/iictask/QMP6988.cpp new file mode 100644 index 0000000..35c3c5c --- /dev/null +++ b/lib/iictask/QMP6988.cpp @@ -0,0 +1,394 @@ +#include "GwQMP6988.h" +#ifdef _GWQMP6988 +#include +#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 \ No newline at end of file diff --git a/lib/iictask/QMP6988.h b/lib/iictask/QMP6988.h new file mode 100644 index 0000000..f02ac3f --- /dev/null +++ b/lib/iictask/QMP6988.h @@ -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 diff --git a/lib/iictask/SHT3X.cpp b/lib/iictask/SHT3X.cpp new file mode 100644 index 0000000..7830cf5 --- /dev/null +++ b/lib/iictask/SHT3X.cpp @@ -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 \ No newline at end of file diff --git a/lib/iictask/SHT3X.h b/lib/iictask/SHT3X.h new file mode 100644 index 0000000..48a33f1 --- /dev/null +++ b/lib/iictask/SHT3X.h @@ -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 diff --git a/lib/iictask/config.json b/lib/iictask/config.json new file mode 100644 index 0000000..ec7d613 --- /dev/null +++ b/lib/iictask/config.json @@ -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" + } + } + ] + } +] \ No newline at end of file diff --git a/lib/iictask/platformio.ini b/lib/iictask/platformio.ini new file mode 100644 index 0000000..db82892 --- /dev/null +++ b/lib/iictask/platformio.ini @@ -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} diff --git a/lib/led/GwLeds.cpp b/lib/led/GwLeds.cpp deleted file mode 100644 index c8758a0..0000000 --- a/lib/led/GwLeds.cpp +++ /dev/null @@ -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(leds,1); - #else - FastLED.addLeds(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 -} \ No newline at end of file diff --git a/lib/led/GwLeds.h b/lib/led/GwLeds.h deleted file mode 100644 index 0d25a9e..0000000 --- a/lib/led/GwLeds.h +++ /dev/null @@ -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 \ No newline at end of file diff --git a/lib/ledtask/GwLedTask.cpp b/lib/ledtask/GwLedTask.cpp new file mode 100644 index 0000000..5df1e9d --- /dev/null +++ b/lib/ledtask/GwLedTask.cpp @@ -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(leds,1); + #else + FastLED.addLeds(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(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 +} \ No newline at end of file diff --git a/lib/ledtask/GwLedTask.h b/lib/ledtask/GwLedTask.h new file mode 100644 index 0000000..2ae1532 --- /dev/null +++ b/lib/ledtask/GwLedTask.h @@ -0,0 +1,8 @@ +#ifndef _GWLEDS_H +#define _GWLEDS_H +#include "GwApi.h" +//task function +void handleLeds(GwApi *param); + +DECLARE_USERTASK(handleLeds); +#endif \ No newline at end of file diff --git a/lib/log/GWLog.cpp b/lib/log/GWLog.cpp index 6601f2a..4ddc8d8 100644 --- a/lib/log/GWLog.cpp +++ b/lib/log/GWLog.cpp @@ -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); } diff --git a/lib/log/GwLog.h b/lib/log/GwLog.h index e04c7b5..4958760 100644 --- a/lib/log/GwLog.h +++ b/lib/log/GwLog.h @@ -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;} diff --git a/lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h b/lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h index bd22fd6..169f181 100644 --- a/lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h +++ b/lib/nmea0183ton2k/NMEA0183AIStoNMEA2000.h @@ -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); } diff --git a/lib/nmea2000esp32/ESP32_CAN_def.h b/lib/nmea2000esp32/ESP32_CAN_def.h deleted file mode 100644 index 98eede4..0000000 --- a/lib/nmea2000esp32/ESP32_CAN_def.h +++ /dev/null @@ -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_ */ diff --git a/lib/nmea2000esp32/NMEA2000_esp32.cpp b/lib/nmea2000esp32/NMEA2000_esp32.cpp deleted file mode 100644 index 24377f9..0000000 --- a/lib/nmea2000esp32/NMEA2000_esp32.cpp +++ /dev/null @@ -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; iMBX_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; iMBX_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(); -} diff --git a/lib/nmea2000esp32/NMEA2000_esp32.h b/lib/nmea2000esp32/NMEA2000_esp32.h deleted file mode 100644 index 59d63f9..0000000 --- a/lib/nmea2000esp32/NMEA2000_esp32.h +++ /dev/null @@ -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 diff --git a/lib/nmea2000esp32/readme.txt b/lib/nmea2000esp32/readme.txt deleted file mode 100644 index 1ff5aa0..0000000 --- a/lib/nmea2000esp32/readme.txt +++ /dev/null @@ -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 \ No newline at end of file diff --git a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp index 1e61d8e..1f490df 100644 --- a/lib/nmea2kto0183/N2kDataToNMEA0183.cpp +++ b/lib/nmea2kto0183/N2kDataToNMEA0183.cpp @@ -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); diff --git a/lib/nmea2ktoais/NMEA0183AISMessages.cpp b/lib/nmea2ktoais/NMEA0183AISMessages.cpp index 8e4cd66..081a1b6 100644 --- a/lib/nmea2ktoais/NMEA0183AISMessages.cpp +++ b/lib/nmea2ktoais/NMEA0183AISMessages.cpp @@ -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; } diff --git a/lib/nmea2ktwai/Nmea2kTwai.cpp b/lib/nmea2ktwai/Nmea2kTwai.cpp new file mode 100644 index 0000000..77c51d9 --- /dev/null +++ b/lib/nmea2ktwai/Nmea2kTwai.cpp @@ -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"; +} \ No newline at end of file diff --git a/lib/nmea2ktwai/Nmea2kTwai.h b/lib/nmea2ktwai/Nmea2kTwai.h new file mode 100644 index 0000000..456e633 --- /dev/null +++ b/lib/nmea2ktwai/Nmea2kTwai.h @@ -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 \ No newline at end of file diff --git a/lib/serial/GwSerial.cpp b/lib/serial/GwSerial.cpp index 6f35856..9327881 100644 --- a/lib/serial/GwSerial.cpp +++ b/lib/serial/GwSerial.cpp @@ -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) { diff --git a/lib/serial/GwSerial.h b/lib/serial/GwSerial.h index b3880fc..d7b01ed 100644 --- a/lib/serial/GwSerial.h +++ b/lib/serial/GwSerial.h @@ -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); diff --git a/lib/socketserver/GwSocketServer.cpp b/lib/socketserver/GwSocketServer.cpp index c225726..185e3a4 100644 --- a/lib/socketserver/GwSocketServer.cpp +++ b/lib/socketserver/GwSocketServer.cpp @@ -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; diff --git a/lib/socketserver/GwTcpClient.cpp b/lib/socketserver/GwTcpClient.cpp index c8fba27..e5f914b 100644 --- a/lib/socketserver/GwTcpClient.cpp +++ b/lib/socketserver/GwTcpClient.cpp @@ -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) diff --git a/lib/timer/GwTimer.h b/lib/timer/GwTimer.h new file mode 100644 index 0000000..e1fecdd --- /dev/null +++ b/lib/timer/GwTimer.h @@ -0,0 +1,48 @@ +#ifndef _GWTIMER_H +#define _GWTIMER_H +#include +#include +#include +class GwIntervalRunner{ + public: + using RunFunction=std::function; + 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 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 \ No newline at end of file diff --git a/lib/usercode/GwUserCode.cpp b/lib/usercode/GwUserCode.cpp index 1e36e13..ef6fbd7 100644 --- a/lib/usercode/GwUserCode.cpp +++ b/lib/usercode/GwUserCode.cpp @@ -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 #include #include +#include "GwCounter.h" //user task handling @@ -11,6 +21,50 @@ std::vector userTasks; std::vector initTasks; GwUserCode::Capabilities userCapabilities; +template +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; +static RegMap ®istrations(){ + 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 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> 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("count"+name))); + } + else it->second=GwCounter("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; } \ No newline at end of file diff --git a/lib/usercode/GwUserCode.h b/lib/usercode/GwUserCode.h index 47bc07e..a218bc9 100644 --- a/lib/usercode/GwUserCode.h +++ b/lib/usercode/GwUserCode.h @@ -2,16 +2,23 @@ #define _GWUSERCODE_H #include #include +#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 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 \ No newline at end of file diff --git a/lib/xdrmappings/GwXDRMappings.cpp b/lib/xdrmappings/GwXDRMappings.cpp index 25d403f..b2eb0c2 100644 --- a/lib/xdrmappings/GwXDRMappings.cpp +++ b/lib/xdrmappings/GwXDRMappings.cpp @@ -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 +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{ diff --git a/lib/xdrmappings/GwXDRMappings.h b/lib/xdrmappings/GwXDRMappings.h index 44d1599..246ade5 100644 --- a/lib/xdrmappings/GwXDRMappings.h +++ b/lib/xdrmappings/GwXDRMappings.h @@ -1,7 +1,6 @@ #ifndef _GWXDRMAPPINGS_H #define _GWXDRMAPPINGS_H #include "GwLog.h" -#include "GWConfig.h" #include "GwBoatData.h" #include #include @@ -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 diff --git a/platformio.ini b/platformio.ini index 7625c7b..b6a00bc 100644 --- a/platformio.ini +++ b/platformio.ini @@ -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 \ No newline at end of file +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 diff --git a/post.py b/post.py index c85a740..8fe2f27 100644 --- a/post.py +++ b/post.py @@ -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 diff --git a/src/main.cpp b/src/main.cpp index 557c35a..c862767 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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 +#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 // 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 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 countNMEA2KIn("count2Kin"); -GwCounter countNMEA2KOut("count2Kout"); - +GwCounter countNMEA2KIn("countNMEA2000in"); +GwCounter 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 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"); } + diff --git a/tools/99-usb-serial.rules b/tools/99-usb-serial.rules index f1f7573..f7fe7fb 100644 --- a/tools/99-usb-serial.rules +++ b/tools/99-usb-serial.rules @@ -1 +1,3 @@ SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", SYMLINK+="esp32" +SUBSYSTEM=="tty", ATTRS{idVendor}=="303a", ATTRS{idProduct}=="1001", SYMLINK+="esp32s3" + diff --git a/tools/flashtool.pyz b/tools/flashtool.pyz index 2f50de5..e8ceb14 100644 Binary files a/tools/flashtool.pyz and b/tools/flashtool.pyz differ diff --git a/tools/flashtool/flasher.py b/tools/flashtool/flasher.py new file mode 100644 index 0000000..d291b40 --- /dev/null +++ b/tools/flashtool/flasher.py @@ -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) \ No newline at end of file diff --git a/tools/flashtool/flashtool.py b/tools/flashtool/flashtool.py index 3a30162..a28bc4f 100755 --- a/tools/flashtool/flashtool.py +++ b/tools/flashtool/flashtool.py @@ -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 diff --git a/web/config.json b/web/config.json index cee92ae..8ad556f 100644 --- a/web/config.json +++ b/web/config.json @@ -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", diff --git a/web/index.html b/web/index.html index 2ecf096..da16fc8 100644 --- a/web/index.html +++ b/web/index.html @@ -47,7 +47,7 @@ # clients --- -
+
TCP client connected ---
@@ -55,10 +55,15 @@ TCP client error ---
-
+
Free heap --- -
+
+
+ NMEA2000 State + [---]  + UNKNOWN +
@@ -95,6 +100,10 @@ firmware type --- +
+ chip type + --- +
currentVersion --- diff --git a/web/index.js b/web/index.js index 28ea82f..31ec3f9 100644 --- a/web/index.js +++ b/web/index.js @@ -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"); }) }) diff --git a/webinstall/build.yaml b/webinstall/build.yaml new file mode 100644 index 0000000..300ffcd --- /dev/null +++ b/webinstall/build.yaml @@ -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 to the flags +# define:name - add -D= 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" \ No newline at end of file diff --git a/webinstall/cibuild.css b/webinstall/cibuild.css new file mode 100644 index 0000000..ff9233f --- /dev/null +++ b/webinstall/cibuild.css @@ -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; +} \ No newline at end of file diff --git a/webinstall/cibuild.html b/webinstall/cibuild.html new file mode 100644 index 0000000..4f41c1a --- /dev/null +++ b/webinstall/cibuild.html @@ -0,0 +1,86 @@ + + + + + + + + + +
+

Build your own ESP32-NMEA2000

+

New Build

+
+ + --- +
+
+ GitSha + --- +
+
+ Version + +
+
+ + +
+
+ +
+
+ Board type +
+
+
+ Build Flags +
+ +
+ +
+ +
+

Last Build

+
+ Job Id +
---
+
+
+ Status +
---
+
+ + + + + +
+ +
+
+ + \ No newline at end of file diff --git a/webinstall/cibuild.js b/webinstall/cibuild.js new file mode 100644 index 0000000..e1ebc2f --- /dev/null +++ b/webinstall/cibuild.js @@ -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 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{ + 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(); + } +})(); \ No newline at end of file diff --git a/webinstall/cibuild.php b/webinstall/cibuild.php new file mode 100644 index 0000000..89c4f29 --- /dev/null +++ b/webinstall/cibuild.php @@ -0,0 +1,325 @@ +$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()); +} +?> \ No newline at end of file diff --git a/webinstall/config.php b/webinstall/config.php new file mode 100644 index 0000000..2a7485b --- /dev/null +++ b/webinstall/config.php @@ -0,0 +1,29 @@ + 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; +} +?> \ No newline at end of file diff --git a/webinstall/config/m5stack-atom-canunit.json b/webinstall/config/m5stack-atom-canunit.json new file mode 100644 index 0000000..f2f929c --- /dev/null +++ b/webinstall/config/m5stack-atom-canunit.json @@ -0,0 +1 @@ +{"root:board":"m5stack-atom-generic","root:board:m5groove":"CAN","root:board:m5groove:m5groovecan":"M5_CANUNIT"} \ No newline at end of file diff --git a/webinstall/config/m5stack-atom-gps-canunit.json b/webinstall/config/m5stack-atom-gps-canunit.json new file mode 100644 index 0000000..0bfd347 --- /dev/null +++ b/webinstall/config/m5stack-atom-gps-canunit.json @@ -0,0 +1 @@ +{"root:board":"m5stack-atom-generic","root:board:m5lightbase":"M5_GPS_KIT","root:board:m5groove":"CAN","root:board:m5groove:m5groovecan":"M5_CANUNIT"} \ No newline at end of file diff --git a/webinstall/config/m5stack-atom-rs232-canunit.json b/webinstall/config/m5stack-atom-rs232-canunit.json new file mode 100644 index 0000000..bdbb4dd --- /dev/null +++ b/webinstall/config/m5stack-atom-rs232-canunit.json @@ -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"} \ No newline at end of file diff --git a/webinstall/config/m5stack-atom-rs485-canunit.json b/webinstall/config/m5stack-atom-rs485-canunit.json new file mode 100644 index 0000000..cf7acec --- /dev/null +++ b/webinstall/config/m5stack-atom-rs485-canunit.json @@ -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"} \ No newline at end of file diff --git a/webinstall/config/m5stack-atom.json b/webinstall/config/m5stack-atom.json new file mode 100644 index 0000000..663b5df --- /dev/null +++ b/webinstall/config/m5stack-atom.json @@ -0,0 +1 @@ +{"root:board":"m5stack-atom-generic","root:board:m5lightbase":"M5_CAN_KIT","root:board:m5groove":"Serial","root:board:m5groove:m5grooveserial":"tail485"} \ No newline at end of file diff --git a/webinstall/config/m5stickc-atom-canunit.json b/webinstall/config/m5stickc-atom-canunit.json new file mode 100644 index 0000000..cdfcc4f --- /dev/null +++ b/webinstall/config/m5stickc-atom-canunit.json @@ -0,0 +1 @@ +{"root:board":"m5stickc-atom-generic","root:board:m5groove":"CAN","root:board:m5groove:m5groovecan":"M5_CANUNIT"} \ No newline at end of file diff --git a/webinstall/config/nodemcu-homberger.json b/webinstall/config/nodemcu-homberger.json new file mode 100644 index 0000000..f815f3e --- /dev/null +++ b/webinstall/config/nodemcu-homberger.json @@ -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} \ No newline at end of file diff --git a/webinstall/create_db.php b/webinstall/create_db.php new file mode 100644 index 0000000..a37139d --- /dev/null +++ b/webinstall/create_db.php @@ -0,0 +1,28 @@ +"; +$rt=$database->query($sql); +echo "execute OK
"; +} catch (Exception $e){ + echo "ERROR: ".$e; +} + + +?> \ No newline at end of file diff --git a/webinstall/functions.php b/webinstall/functions.php new file mode 100644 index 0000000..477cc1d --- /dev/null +++ b/webinstall/functions.php @@ -0,0 +1,236 @@ +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()); +} + +?> \ No newline at end of file diff --git a/webinstall/helper.js b/webinstall/helper.js new file mode 100644 index 0000000..91ba1dc --- /dev/null +++ b/webinstall/helper.js @@ -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 } \ No newline at end of file diff --git a/webinstall/install.css b/webinstall/install.css index 972b865..6731764 100644 --- a/webinstall/install.css +++ b/webinstall/install.css @@ -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; } \ No newline at end of file diff --git a/webinstall/install.html b/webinstall/install.html index e578a82..be9dc94 100644 --- a/webinstall/install.html +++ b/webinstall/install.html @@ -9,9 +9,15 @@ -
-
+
+
+
loading data
+ +
+
+
+ \ No newline at end of file diff --git a/webinstall/install.js b/webinstall/install.js index 0281ff4..013be33 100644 --- a/webinstall/install.js +++ b/webinstall/install.js @@ -1,10 +1,110 @@ import {XtermOutputHandler} from "./installUtil.js"; import ESPInstaller from "./installUtil.js"; +import { readFile, addEl, getParam, setValue, setVisible } from "./helper.js"; +import * as zip from "https://cdn.jsdelivr.net/npm/@zip.js/zip.js@2.7.29/+esm"; (function(){ + + const UPDATE_START=65536; + + //taken from index.js + const HDROFFSET = 288; + const VERSIONOFFSET = 16; + const NAMEOFFSET = 48; + const MINSIZE = HDROFFSET + NAMEOFFSET + 32; + const CHIPIDOFFSET=12; //2 bytes chip id here + const imageMagic=0xe9; //at byte 0 + const imageCheckBytes = { + 288: 0x32, //app header magic + 289: 0x54, + 290: 0xcd, + 291: 0xab + }; + /** + * map of our known chip ids to flash starts for full images + * see https://github.com/espressif/esptool-js/tree/main/src/targets + * IMAGE_CHIP_ID, BOOTLOADER_FLASH_OFFSET + * 0 - esp32 - starts at 0x1000 + * 9 - esp32s3 - starts at 0 + */ + const FLASHSTART={ + 0:0x1000, //ESP32 + 9:0, //ESP32S3 + 2:0x1000, //ESP32S2 + 5:0 //ESP32C3 + }; + const decodeFromBuffer=(buffer, start, length)=>{ + while (length > 0 && buffer.charCodeAt(start + length - 1) == 0) { + length--; + } + if (length <= 0) return ""; + return buffer.substr(start,length); + } + const getChipId=(buffer)=>{ + if (buffer.length < CHIPIDOFFSET+2) return -1; + return buffer.charCodeAt(CHIPIDOFFSET)+256*buffer.charCodeAt(CHIPIDOFFSET+1); + } + /** + * + * @param {string} content the content to be checked + */ + const checkImage = (content,isFull) => { + let prfx=isFull?"full":"update"; + let startOffset=0; + let flashStart=UPDATE_START; + let chipId=getChipId(content); + if (isFull){ + if (chipId < 0) throw new Error(prfx+"image: no valid chip id found"); + flashStart=FLASHSTART[chipId]; + if (flashStart === undefined) throw new Error(prfx+"image: unknown chip id "+chipId); + startOffset=UPDATE_START-flashStart; + } + if (content.length < (MINSIZE+startOffset)) { + throw new Error(prfx+"image to small, only " + content.length + " expected " + (MINSIZE+startOffset)); + } + if (content.charCodeAt(0) != imageMagic){ + throw new Error("no image magic "+imageMagic+" at start of "+prfx+"image"); + } + for (let idx in imageCheckBytes) { + let cb=content.charCodeAt(parseInt(idx)+startOffset); + if (cb != imageCheckBytes[idx]) { + throw new Error(prfx+"image: missing magic byte at position " + idx + ", expected " + + imageCheckBytes[idx] + ", got " + cb); + } + } + let version = decodeFromBuffer(content, startOffset+ HDROFFSET + VERSIONOFFSET, 32); + let fwtype = decodeFromBuffer(content, startOffset+ HDROFFSET + NAMEOFFSET, 32); + let rt = { + fwtype: fwtype, + version: version, + chipId:chipId, + flashStart: flashStart + }; + return rt; + } + + const checkImageFile=(file,isFull)=>{ + let minSize=MINSIZE+(isFull?(UPDATE_START-FULL_START):0); + return new Promise(function (resolve, reject) { + if (!file) reject("no file"); + if (file.size < minSize) reject("file is too small"); + let slice = file.slice(0, minSize); + let reader = new FileReader(); + reader.addEventListener('load', function (e) { + let content = e.target.result; + try{ + let rt=checkImage(content,isFull); + resolve(rt); + } + catch (e){ + reject(e); + } + }); + reader.readAsBinaryString(slice); + }); + } let espLoaderTerminal; let espInstaller; let releaseData={}; - const addEl=ESPInstaller.addEl; //shorter typing let showConsole; let hideConsole; const enableConsole=(enable,disableBoth)=>{ @@ -21,18 +121,19 @@ import ESPInstaller from "./installUtil.js"; alert(txt); } } - const buildHeading=(user,repo,element)=>{ + const buildHeading=(info,element)=>{ let hFrame=document.querySelector(element||'.heading'); if (! hFrame) return; hFrame.textContent=''; - let h=addEl('h2',undefined,hFrame,`ESP32 Install ${user}:${repo}`) + let h=addEl('h2',undefined,hFrame,`ESP32 Install ${info}`) } - const checkChip=(chipFamily,assetName)=>{ - //for now only ESP32 - if (chipFamily != "ESP32"){ - throw new Error(`unexpected chip family ${chipFamily}, expected ESP32`); + const checkChip= async (chipId,data,isFull)=>{ + let info=checkImage(data,isFull); + if (info.chipId != chipId){ + let res=confirm("different chip signatures - image("+chipId+"), chip ("+info.chipId+")\nUse this image any way?"); + if (! res) throw new Error("user abort"); } - return assetName; + return info; } const baudRates=[1200, 2400, @@ -68,6 +169,63 @@ import ESPInstaller from "./installUtil.js"; enableConsole(true); }) } + const handleLocalFile= async (file)=>{ + setValue('content',''); + showLoading("loading "+file.name); + try { + if (file.name.match(/.zip$/)) { + //zip + await handleZip(file); + } + else { + if (! file.name.match(/\.bin$/)){ + throw new Error("only .zip or .bin"); + } + let data=await readFile(file); + let isFull=false; + let info; + try{ + info=checkImage(data,true); + isFull=true; + }catch (e){ + try{ + info=checkImage(data); + } + catch(x){ + throw new Error(file.name+" is no image: "+x); + } + } + if (isFull){ + buildCustomButtons("dummy",undefined,data,file.name,"Local"); + } + else{ + buildCustomButtons("dummy",data,undefined,file.name,"Local"); + } + } + } catch (e) { + alert(e); + } + showLoading(); + } + const buildUploadButtons= (element)=>{ + let bFrame=document.querySelector(element||'.upload'); + if (! bFrame) return; + bFrame.textContent=''; + let it=addEl('div','item',bFrame); + addEl('div','version',it,`Local File`); + let cLine=addEl('div','buttons',it); + let fi=addEl('input','uploadFile',cLine); + fi.setAttribute('type','file'); + fi.addEventListener('change',async (ev)=>{ + let files=ev.target.files; + if (files.length < 1) return; + await handleLocalFile(files[0]); + }); + let bt=addEl('button','uploadButton',cLine,'upload'); + bt.addEventListener('click',()=>{ + fi.click(); + }); + } const buildButtons=(user,repo,element)=>{ let bFrame=document.querySelector(element||'.content'); if (! bFrame) return; @@ -78,7 +236,7 @@ import ESPInstaller from "./installUtil.js"; alert("no version found in release data"); return; } - addEl('div','version',bFrame,`Version: ${version}`); + addEl('div','version',bFrame,`Prebuild: ${version}`); let items={}; releaseData.assets.forEach((asset)=>{ let name=asset.name; @@ -100,7 +258,7 @@ import ESPInstaller from "./installUtil.js"; let line=addEl('div','item',bFrame); addEl('div','itemTitle',line,item.label); let btLine=addEl('div','buttons',line); - let tb=addEl('button','installButton',line,'Initial'); + let tb=addEl('button','installButton',btLine,'Initial'); tb.addEventListener('click',async ()=>{ enableConsole(false,true); await espInstaller.installClicked( @@ -108,12 +266,12 @@ import ESPInstaller from "./installUtil.js"; user, repo, version, - 4096, - (chip)=>checkChip(chip,item.basic) + item.basic, + checkChip ) enableConsole(true); }); - tb=addEl('button','installButton',line,'Update'); + tb=addEl('button','installButton',btLine,'Update'); tb.addEventListener('click',async ()=>{ enableConsole(false,true); await espInstaller.installClicked( @@ -121,31 +279,173 @@ import ESPInstaller from "./installUtil.js"; user, repo, version, - 65536, - (chip)=>checkChip(chip,item.update) + item.update, + checkChip ) enableConsole(true); }); } } + const buildCustomButtons = (name, updateData, fullData,info,title,element) => { + let bFrame = document.querySelector(element || '.content'); + if (!bFrame) return; + if (fullData === undefined && updateData === undefined) return; + let version; + let vinfo; + let uinfo; + if (fullData !== undefined){ + vinfo=checkImage(fullData,true); + version=vinfo.version; + } + if (updateData !== undefined){ + uinfo=checkImage(updateData); + if (version !== undefined){ + if (uinfo.version != version){ + throw new Error("different versions in full("+version+") and update("+uinfo.version+") image"); + } + } + else{ + version=uinfo.version; + } + } + bFrame.textContent = ''; + let item=addEl('div','item',bFrame); + addEl('div', 'version', item, title+" "+version); + if (info){ + addEl('div','version',item,info); + } + let btLine = addEl('div', 'buttons', item); + let tb; + if (fullData !== undefined) { + tb = addEl('button', 'installButton', btLine, 'Initial'); + tb.addEventListener('click', async () => { + enableConsole(false, true); + await espInstaller.runFlash( + true, + fullData, + version, + checkChip + ) + enableConsole(true); + }); + } + if (updateData !== undefined) { + tb = addEl('button', 'installButton', btLine, 'Update'); + tb.addEventListener('click', async () => { + enableConsole(false, true); + await espInstaller.runFlash( + false, + updateData, + version, + checkChip + ) + enableConsole(true); + }); + } + } + const showLoading=(title)=>{ + setVisible('loadingFrame',title !== undefined); + if (title){ + setValue('loadingText',title); + } + }; + class BinaryStringWriter extends zip.Writer { + + constructor() { + super(); + this.binaryString = ""; + } + + writeUint8Array(array) { + for (let indexCharacter = 0; indexCharacter < array.length; indexCharacter++) { + this.binaryString += String.fromCharCode(array[indexCharacter]); + } + } + + getData() { + return this.binaryString; + } + } + const handleZip = async (zipFile) => { + showLoading("loading zip"); + let reader; + let title="Custom"; + if (typeof(zipFile) === 'string'){ + setValue('loadingText', 'downloading custom build') + reader= new zip.HttpReader(zipFile); + } + else{ + setValue('loadingText', 'loading zip file'); + reader = new zip.BlobReader(zipFile); + title="Local"; + } + let zipReader = new zip.ZipReader(reader); + const entries = (await zipReader.getEntries()); + let fullData; + let updateData; + let base = ""; + let environment; + let buildflags; + for (let i = 0; i < entries.length; i++) { + if (entries[i].filename.match(/-all.bin$/)) { + fullData = await (entries[i].getData(new BinaryStringWriter())); + base = entries[i].filename.replace("-all.bin", ""); + } + if (entries[i].filename.match(/-update.bin$/)) { + updateData = await (entries[i].getData(new BinaryStringWriter())); + base = entries[i].filename.replace("-update.bin", ""); + } + if (entries[i].filename === 'buildconfig.txt') { + let txt = await (entries[i].getData(new zip.TextWriter())); + environment = txt.replace(/.*pio run *.e */, '').replace(/ .*/, ''); + buildflags = txt.replace(/.*PLATFORMIO_BUILD_FLAGS="/, '').replace(/".*/, ''); + } + } + let info; + if (environment !== undefined && buildflags !== undefined) { + info = `env=${environment}, flags=${buildflags}`; + } + if (updateData === undefined && fullData === undefined){ + throw new Error("no firmware files found in zip"); + } + buildCustomButtons("dummy", updateData, fullData, info,title); + showLoading(); + } window.onload = async () => { if (! ESPInstaller.checkAvailable()){ showError("your browser does not support the ESP flashing (no serial)"); return; } - let user = window.gitHubUser||ESPInstaller.getParam('user'); - let repo = window.gitHubRepo || ESPInstaller.getParam('repo'); - if (!user || !repo) { - alert("missing parameter user or repo"); + let custom=getParam('custom'); + let user; + let repo; + let errorText=`unable to query release info for user ${user}, repo ${repo}: `; + if (! custom){ + user = window.gitHubUser||getParam('user','wellenvogel'); + repo = window.gitHubRepo || getParam('repo','esp32-nmea2000'); + if (!user || !repo) { + alert("missing parameter user or repo"); + } } try { espLoaderTerminal = new XtermOutputHandler('terminal'); espInstaller = new ESPInstaller(espLoaderTerminal); - buildHeading(user, repo); buildConsoleButtons(); - releaseData = await espInstaller.getReleaseInfo(user, repo); - buildButtons(user, repo); - } catch(error){alert("unable to query release info for user "+user+", repo "+repo+": "+error)}; + buildUploadButtons(); + if (! custom){ + buildHeading(`${user}:${repo}`); + releaseData = await espInstaller.getReleaseInfo(user, repo); + buildButtons(user, repo); + showLoading(); + } + else{ + errorText="unable to download custom build"; + await handleZip(custom); + } + } catch(error){ + showLoading(); + alert(errorText+error) + }; } })(); \ No newline at end of file diff --git a/webinstall/install.php b/webinstall/install.php index e429bec..d0dea66 100644 --- a/webinstall/install.php +++ b/webinstall/install.php @@ -1,155 +1,73 @@ array('wellenvogel'), - 'repo'=> array('esp32-nmea2000') - ); - 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 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; - } - - function fillUserAndRepo($vars=null){ - global $allowed; - if ($vars == null) { - $vars=array(); - } - foreach (array('user','repo') as $n){ - if (! isset($_REQUEST[$n])){ - die("missing parameter $n"); - } - $v=$_REQUEST[$n]; - $av=$allowed[$n]; - if (! in_array($v,$av)){ - die("value $v for $n not allowed"); - } - $vars[$n]=$v; - } - return $vars; - } - function addVars($vars,$names){ - foreach ($names as $n){ - if (! isset($_REQUEST[$n])){ - die("missing parameter $n"); - } - $safe=safeName($_REQUEST[$n]); - $vars[$n]=$safe; - } - return $vars; - } - - function curl_exec_follow(/*resource*/ $ch, /*int*/ &$maxredirect = null) { - $mr = $maxredirect === null ? 5 : intval($maxredirect); - 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 { - curl_setopt($rch, CURLOPT_URL, $newurl); - $header = curl_exec($rch); - if (curl_errno($rch)) { - $code = 0; - } else { - $code = curl_getinfo($rch, CURLINFO_HTTP_CODE); - if ($code == 301 || $code == 302) { - preg_match('/Location:(.*?)\n/', $header, $matches); - $newurl = trim(array_pop($matches)); - } else { - $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 proxy($url) - { - $headers=getallheaders(); - $ch = curl_init($url); - curl_setopt_array( - $ch, - [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_CONNECTTIMEOUT => 30, - ] - ); - $FWHDR = ['User-Agent']; - $outHeaders = array(); - foreach ($FWHDR as $k) { - if (isset($headers[$k])) { - array_push($outHeaders, "$k: $headers[$k]"); - } - } - curl_setopt($ch, CURLOPT_HTTPHEADER, $outHeaders); - $response = curl_exec_follow($ch); - curl_close($ch); - } - +include("functions.php"); +include("config.php"); +const API_BASE="https://api.github.com/repos/#user#/#repo#"; +$api = API_BASE."/releases/latest"; +$branchsha=API_BASE."/git/refs/heads/#branch#"; +$tagsha=API_BASE."/git/refs/tags/#tag#"; +$download = "https://github.com/#user#/#repo#/releases/download/#dlVersion#/#dlName#"; +$manifest = "?dlName=#mName#&dlVersion=#mVersion#&user=#user#&repo=#repo#"; +$proxurl="https://raw.githubusercontent.com/#user#/#repo#/#sha#/#proxy#"; +try { if (isset($_REQUEST['api'])) { - $vars=fillUserAndRepo(); - proxy(replaceVars($api,$vars)); + $vars = fillUserAndRepo(); + proxy(replaceVars($api, $vars)); exit(0); } - if (isset($_REQUEST['dlName'])){ - $vars=fillUserAndRepo(); - $vars=addVars($vars,array('dlName','dlVersion')); - proxy(replaceVars($download,$vars)); + if (isset($_REQUEST['branch'])){ + $vars = fillUserAndRepo(); + $vars = addVars($vars, array('branch')); + proxy(replaceVars($branchsha, $vars)); exit(0); } - die("invalid request"); - ?> \ No newline at end of file + if (isset($_REQUEST['tag'])){ + $vars = fillUserAndRepo(); + $vars = addVars($vars, array('tag')); + proxy(replaceVars($tagsha, $vars)); + exit(0); + } + if (isset($_REQUEST['dlName'])) { + $vars = fillUserAndRepo(); + $vars = addVars($vars, array('dlName', 'dlVersion')); + proxy(replaceVars($download, $vars)); + exit(0); + } + if (isset($_REQUEST['flash'])) { + $vars = fillUserAndRepo(); + $json = getJson(replaceVars($api, $vars)); + $assets = $json['assets']; + $targetUrl = null; + $targetBase = $_REQUEST['flash']; + $mode = 'all'; + if (isset($_REQUEST['update'])) + $mode = 'update'; + $lb = strlen($targetBase); + foreach ($assets as &$asset) { + if (substr($asset['name'], 0, $lb) == $targetBase) { + if (!preg_match("/-$mode.bin/", $asset['name'])) + continue; + $targetUrl = $asset['browser_download_url']; + break; + } + } + if (!$targetUrl) + throw new Exception("unable to find $targetBase $mode\n"); + #echo("download for $targetBase=$targetUrl\n"); + proxy($targetUrl); + exit(0); + } + if (isset($_REQUEST['proxy'])){ + $vars = fillUserAndRepo(); + $vars = addVars($vars, array('sha', 'proxy')); + proxy(replaceVars($proxurl, $vars)); + exit(0); + } +} 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()); +} +die("invalid request"); +?> \ No newline at end of file diff --git a/webinstall/installUtil.js b/webinstall/installUtil.js index 87f91c3..14910d8 100644 --- a/webinstall/installUtil.js +++ b/webinstall/installUtil.js @@ -61,6 +61,7 @@ class ESPInstaller{ this.base=import.meta.url.replace(/[^/]*$/,"install.php"); this.consoleDevice=undefined; this.consoleReader=undefined; + this.imageChipId=undefined; } /** * get an URL query parameter @@ -72,39 +73,6 @@ class ESPInstaller{ // Return the unescaped value minus everything starting from the equals sign or an empty string return decodeURIComponent(!!value ? value.toString().replace(/^[^=]+./,"") : ""); }; - /** - * add an HTML element - * @param {*} type - * @param {*} clazz - * @param {*} parent - * @param {*} text - * @returns - */ - static 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) el.textContent = text; - if (parent) parent.appendChild(el); - return el; - } - /** - * call a function for each matching element - * @param {*} selector - * @param {*} cb - */ - static forEachEl(selector,cb){ - let arr=document.querySelectorAll(selector); - for (let i=0;i