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

41 Commits

Author SHA1 Message Date
86cd1ed97c Add missing functions to LedSpiTask 2026-03-08 15:22:26 +01:00
e33f908187 Page system rework and improvements 2026-02-25 20:03:54 +01:00
Norbert Walter
065a9807d2 Merge pull request #229 from thooge/formatter
Added format "formatXdr:A:rd" to formatter (see #159)
2026-02-25 10:21:45 +01:00
Norbert Walter
98c318f055 Merge pull request #228 from thooge/patching
System page with N2K device list by improved patching feature and patch
2026-02-25 10:21:02 +01:00
97fcebdcb7 Added format "formatXdr:A:rd" to formatter (see #159) 2026-02-23 18:56:20 +01:00
a6fd3ef599 System page with N2K device list by improved patching feature and patch 2026-02-23 15:34:10 +01:00
norbert-walter
66e71acac3 Fix for GwWifi 2026-02-20 09:52:41 +01:00
norbert-walter
b85504bf50 Merge branch 'autopilot2' 2026-02-20 09:43:17 +01:00
norbert-walter
3043be8e1d Code cleaning 2026-02-15 19:17:52 +01:00
norbert-walter
02b2c888ee Fix for GwWifi 2026-02-15 19:16:24 +01:00
Norbert Walter
00b06f458b GwWifi race condition safe, PageNavigation check the Wifi connection befor using 2026-02-15 16:11:19 +00:00
Norbert Walter
43b0a780d5 Merge pull request #227 from thooge/master
Add feature to optionally apply patches to gateway code
2026-02-15 15:53:26 +01:00
0363ba4379 Add feature to optionally apply patches to gateway code 2026-02-15 13:13:19 +01:00
Norbert Walter
4b6e2abe33 Merge pull request #226 from Scorgan01/master
Small improvements to charts + pages OneValue + TwoValues
2026-02-15 12:21:32 +01:00
Norbert Walter
0401d82b62 Merge pull request #225 from thooge/master
Make code compile for OBP60 v2.0 again
2026-02-15 12:20:41 +01:00
Scorgan01
2fecbee492 Merge branch 'norbert-walter:master' into master 2026-02-11 22:19:11 +01:00
Ulrich Meine
fc5daaba37 - change control of key settings on PageOneValue + PageTwoValues
- added taskYIELD() to chart loop to be nice to other tasks
- fix typo in config_obp40.json
- fine tune chart labels
- disable debug messages
2026-02-09 22:31:07 +01:00
Norbert Walter
04dc09e44a Fix directory path for tool installation 2026-02-09 15:25:27 +01:00
norbert-walter
6c7997e369 Fix iRTC time for N2K bus 2026-02-08 22:00:22 +01:00
1d2ba2f71d Make code compile for OBP60 v2.0 again 2026-02-08 18:02:50 +01:00
norbert-walter
7f747e9b35 Add data connections for PageAutopilot 2026-02-08 14:49:40 +01:00
Norbert Walter
71512e7262 Actualize PageAutopilot 2026-02-08 13:18:39 +00:00
norbert-walter
4468c0555b Implement rudder bargraf in PageAutopilot 2026-02-08 14:09:10 +01:00
Norbert Walter
99404991a3 Add rudder bargraf 2026-02-08 12:40:20 +00:00
Norbert Walter
ee5077e0a5 Add PageAutopilot 2026-02-07 16:59:17 +00:00
norbert-walter
bbecf5e55f Code ceaning 2026-02-07 17:36:14 +01:00
norbert-walter
ded1b2b22e Typo 2026-02-07 17:10:59 +01:00
norbert-walter
a0a88fa2c9 Modify reagatta timer funktion 2026-02-07 17:05:41 +01:00
Norbert Walter
e9bf54e99f Add sync button for Timer 2026-02-07 14:34:54 +00:00
norbert-walter
64950c3974 Merge branch 'newclock' 2026-02-06 23:07:26 +01:00
norbert-walter
fb2fbc85a4 Typo and formats for PageClock 2026-02-06 23:05:26 +01:00
norbert-walter
6b92a5e69c Add more functionality for time and date synchrinisation 2026-02-06 22:41:34 +01:00
norbert-walter
ef4546a2e6 Fix for PageClock 2026-02-06 15:20:43 +01:00
norbert-walter
6870c9b8a4 Modify button labels in PageClock 2026-02-06 12:52:14 +01:00
norbert-walter
a70d976a6e Change design for PageClock 2026-02-06 11:46:35 +01:00
norbert-walter
fbba6ffff2 Fix for PageClock 2026-02-05 23:52:10 +01:00
norbert-walter
d516c82041 New clock page with extended functions 2026-02-05 16:32:15 +01:00
norbert-walter
ccca784ac2 Delete old extra.script.py 2026-02-04 16:56:11 +01:00
norbert-walter
337214d650 Fix config problem for OBP60, missing setup values 2026-02-04 16:48:50 +01:00
Norbert Walter
744cf6469b Merge pull request #224 from thooge/master
Fix build for new gateway extra_script.py
2026-02-04 08:28:52 +01:00
6bc1b60f60 Fix build for new gateway extra_script.py 2026-02-04 08:18:39 +01:00
31 changed files with 1828 additions and 1838 deletions

View File

@@ -104,8 +104,7 @@ def writeFileIfChanged(fileName,data):
return True
def mergeConfig(base,other):
for bdir in other:
cname=os.path.join(bdir,"config.json")
for cname in other:
if os.path.exists(cname):
print("merge config %s"%cname)
with open(cname,'rb') as ah:
@@ -151,13 +150,25 @@ def expandConfig(config):
rt.append(replaceTexts(c,replace))
return rt
def generateMergedConfig(inFile,outFile,addDirs=[]):
def createUserItemList(dirs,itemName,files):
rt=[]
for d in dirs:
iname=os.path.join(d,itemName)
if os.path.exists(iname):
rt.append(iname)
for f in files:
if not os.path.exists(f):
raise Exception("user item %s not found"%f)
rt.append(f)
return rt
def generateMergedConfig(inFile,outFile,addFiles=[]):
if not os.path.exists(inFile):
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=mergeConfig(config,addFiles)
config=expandConfig(config)
data=json.dumps(config,indent=2)
writeFileIfChanged(outFile,data)
@@ -274,9 +285,9 @@ class Grove:
def _ss(self,z=False):
if z:
return self.name
return self.name if self.name is not 'Z' else ''
return self.name if self.name != 'Z' else ''
def _suffix(self):
return '_'+self.name if self.name is not 'Z' else ''
return '_'+self.name if self.name != 'Z' else ''
def replace(self,line):
if line is None:
return line
@@ -377,12 +388,7 @@ def getLibs():
def joinFiles(target,pattern,dirlist):
flist=[]
for dir in dirlist:
fn=os.path.join(dir,pattern)
if os.path.exists(fn):
flist.append(fn)
def joinFiles(target,flist):
current=False
if os.path.exists(target):
current=True
@@ -453,7 +459,28 @@ def handleDeps(env):
)
env.AddBuildMiddleware(injectIncludes)
def getOption(env,name,toArray=True):
try:
opt=env.GetProjectOption(name)
if toArray:
if opt is None:
return []
if isinstance(opt,list):
return opt
return opt.split("\n" if "\n" in opt else ",")
return opt
except:
pass
if toArray:
return []
def getFileList(files):
base=basePath()
rt=[]
for f in files:
if f is not None and f != "":
rt.append(os.path.join(base,f))
return rt
def prebuild(env):
global userTaskDirs
print("#prebuild running")
@@ -463,14 +490,18 @@ def prebuild(env):
if ldf_mode == 'off':
print("##ldf off - own dependency handling")
handleDeps(env)
extraConfigs=getOption(env,'custom_config',toArray=True)
extraJs=getOption(env,'custom_js',toArray=True)
extraCss=getOption(env,'custom_css',toArray=True)
userTaskDirs=getUserTaskDirs()
mergedConfig=os.path.join(outPath(),os.path.basename(CFG_FILE))
generateMergedConfig(os.path.join(basePath(),CFG_FILE),mergedConfig,userTaskDirs)
generateMergedConfig(os.path.join(basePath(),CFG_FILE),mergedConfig,createUserItemList(userTaskDirs,"config.json", getFileList(extraConfigs)))
compressFile(mergedConfig,mergedConfig+".gz")
generateCfg(mergedConfig,os.path.join(outPath(),CFG_INCLUDE),False)
generateCfg(mergedConfig,os.path.join(outPath(),CFG_INCLUDE_IMPL),True)
joinFiles(os.path.join(outPath(),INDEXJS+".gz"),INDEXJS,["web"]+userTaskDirs)
joinFiles(os.path.join(outPath(),INDEXCSS+".gz"),INDEXCSS,["web"]+userTaskDirs)
joinFiles(os.path.join(outPath(),INDEXJS+".gz"),createUserItemList(["web"]+userTaskDirs,INDEXJS,getFileList(extraJs)))
joinFiles(os.path.join(outPath(),INDEXCSS+".gz"),createUserItemList(["web"]+userTaskDirs,INDEXCSS,getFileList(extraCss)))
embedded=getEmbeddedFiles(env)
filedefs=[]
for ef in embedded:

View File

@@ -1,542 +0,0 @@
print("running extra...")
import gzip
import shutil
import os
import sys
import inspect
import json
import glob
from datetime import datetime
import re
import pprint
from platformio.project.config import ProjectConfig
from platformio.project.exception import InvalidProjectConfError
Import("env")
#print(env.Dump())
OWN_FILE="extra_script.py"
GEN_DIR='lib/generated'
CFG_FILE='web/config.json'
XDR_FILE='web/xdrconfig.json'
INDEXJS="index.js"
INDEXCSS="index.css"
CFG_INCLUDE='GwConfigDefinitions.h'
CFG_INCLUDE_IMPL='GwConfigDefImpl.h'
XDR_INCLUDE='GwXdrTypeMappings.h'
TASK_INCLUDE='GwUserTasks.h'
GROVE_CONFIG="GwM5GroveGen.h"
GROVE_CONFIG_IN="lib/hardware/GwM5Grove.in"
EMBEDDED_INCLUDE="GwEmbeddedFiles.h"
def getEmbeddedFiles(env):
rt=[]
efiles=env.GetProjectOption("board_build.embed_files")
for f in efiles.split("\n"):
if f == '':
continue
rt.append(f)
return rt
def basePath():
#see: https://stackoverflow.com/questions/16771894/python-nameerror-global-name-file-is-not-defined
return os.path.dirname(inspect.getfile(lambda: None))
def outPath():
return os.path.join(basePath(),GEN_DIR)
def checkDir():
dn=outPath()
if not os.path.exists(dn):
os.makedirs(dn)
if not os.path.isdir(dn):
print("unable to create %s"%dn)
return False
return True
def isCurrent(infile,outfile):
if os.path.exists(outfile):
otime=os.path.getmtime(outfile)
itime=os.path.getmtime(infile)
if (otime >= itime):
own=os.path.join(basePath(),OWN_FILE)
if os.path.exists(own):
owntime=os.path.getmtime(own)
if owntime > otime:
return False
print("%s is newer then %s, no need to recreate"%(outfile,infile))
return True
return False
def compressFile(inFile,outfile):
if isCurrent(inFile,outfile):
return
print("compressing %s"%inFile)
with open(inFile, 'rb') as f_in:
with gzip.open(outfile, 'wb') as f_out:
shutil.copyfileobj(f_in, f_out)
def generateFile(infile,outfile,callback,inMode='rb',outMode='w'):
if isCurrent(infile,outfile):
return
print("creating %s"%outfile)
oh=None
with open(infile,inMode) as ch:
with open(outfile,outMode) as oh:
try:
callback(ch,oh,inFile=infile)
oh.close()
except Exception as e:
try:
oh.close()
except:
pass
os.unlink(outfile)
raise
def writeFileIfChanged(fileName,data):
if os.path.exists(fileName):
with open(fileName,"r") as ih:
old=ih.read()
ih.close()
if old == data:
return False
print("#generating %s"%fileName)
with open(fileName,"w") as oh:
oh.write(data)
return True
def mergeConfig(base,other):
try:
customconfig = env.GetProjectOption("custom_config")
except InvalidProjectConfError:
customconfig = None
for bdir in other:
if customconfig and os.path.exists(os.path.join(bdir,customconfig)):
cname=os.path.join(bdir,customconfig)
print("merge custom config {}".format(cname))
with open(cname,'rb') as ah:
base += json.load(ah)
continue
cname=os.path.join(bdir,"config.json")
if os.path.exists(cname):
print("merge config %s"%cname)
with open(cname,'rb') as ah:
merge=json.load(ah)
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)
data=""
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,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)
data+="//generated from %s\n"%inFile
l=len(config)
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]='%(idx)
idx+=1
secret="false";
if item.get('type') == 'password':
secret="true"
data+=" new GwConfigInterface(%s,\"%s\",%s);\n"%(name,item.get('default'),secret)
data+='}\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")
first=True
for cat in jdoc:
item=jdoc[cat]
cid=item.get('id')
if cid is None:
continue
tc=item.get('type')
if tc is not None:
if first:
first=False
else:
oh.write(",\n")
oh.write(" new GwXDRTypeMapping(%d,0,%d) /*%s*/"%(cid,tc,cat))
fields=item.get('fields')
if fields is None:
continue
idx=0
for fe in fields:
if not isinstance(fe,dict):
continue
tc=fe.get('t')
id=fe.get('v')
if id is None:
id=idx
idx+=1
l=fe.get('l') or ''
if tc is None or id is None:
continue
if first:
first=False
else:
oh.write(",\n")
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
class Grove:
def __init__(self,name) -> None:
self.name=name
def _ss(self,z=False):
if z:
return self.name
return self.name if self.name != 'Z' else ''
def _suffix(self):
return '_'+self.name if self.name != 'Z' else ''
def replace(self,line):
if line is None:
return line
return line.replace('$G$',self._ss()).replace('$Z$',self._ss(True)).replace('$GS$',self._suffix())
def generateGroveDefs(inh,outh,inFile=''):
GROVES=[Grove('Z'),Grove('A'),Grove('B'),Grove('C')]
definition=[]
started=False
def writeConfig():
for grove in GROVES:
for cl in definition:
outh.write(grove.replace(cl))
for line in inh:
if re.match(" *#GROVE",line):
started=True
if len(definition) > 0:
writeConfig()
definition=[]
continue
if started:
definition.append(line)
if len(definition) > 0:
writeConfig()
userTaskDirs=[]
def getUserTaskDirs():
rt=[]
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:
#print("##taskdir=%s"%task)
base=os.path.basename(task)
includeNames=[base.lower()+".h",'gw'+base.lower()+'.h']
for f in os.listdir(task):
checkAndAdd(f,includeNames,includes)
includeData=""
for i in includes:
print("#task include %s"%i)
includeData+="#include <%s>\n"%i
writeFileIfChanged(outfile,includeData)
def generateEmbedded(elist,outFile):
content=""
for entry in elist:
content+="EMBED_GZ_FILE(\"%s\",%s,\"%s\");\n"%entry
writeFileIfChanged(outFile,content)
def getContentType(fn):
if (fn.endswith('.gz')):
fn=fn[0:-3]
if (fn.endswith('html')):
return "text/html"
if (fn.endswith('json')):
return "application/json"
if (fn.endswith('js')):
return "text/javascript"
if (fn.endswith('css')):
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
def joinFiles(target,pattern,dirlist):
flist=[]
for dir in dirlist:
fn=os.path.join(dir,pattern)
if os.path.exists(fn):
flist.append(fn)
current=False
if os.path.exists(target):
current=True
for f in flist:
if not isCurrent(f,target):
current=False
break
if current:
print("%s is up to date"%target)
return
print("creating %s"%target)
with gzip.open(target,"wb") as oh:
for fn in flist:
print("adding %s to %s"%(fn,target))
with open(fn,"rb") as rh:
shutil.copyfileobj(rh,oh)
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),False)
generateCfg(mergedConfig,os.path.join(outPath(),CFG_INCLUDE_IMPL),True)
joinFiles(os.path.join(outPath(),INDEXJS+".gz"),INDEXJS,["web"]+userTaskDirs)
joinFiles(os.path.join(outPath(),INDEXCSS+".gz"),INDEXCSS,["web"]+userTaskDirs)
embedded=getEmbeddedFiles(env)
filedefs=[]
for ef in embedded:
print("#checking embedded file %s"%ef)
(dn,fn)=os.path.split(ef)
pureName=fn
if pureName.endswith('.gz'):
pureName=pureName[0:-3]
ct=getContentType(pureName)
usname=ef.replace('/','_').replace('.','_')
filedefs.append((pureName,usname,ct))
inFile=os.path.join(basePath(),"web",pureName)
if os.path.exists(inFile):
compressFile(inFile,ef)
else:
print("#WARNING: infile %s for %s not found"%(inFile,ef))
generateEmbedded(filedefs,os.path.join(outPath(),EMBEDDED_INCLUDE))
genereateUserTasks(os.path.join(outPath(), TASK_INCLUDE))
generateFile(os.path.join(basePath(),XDR_FILE),os.path.join(outPath(),XDR_INCLUDE),generateXdrMappings)
generateFile(os.path.join(basePath(),GROVE_CONFIG_IN),os.path.join(outPath(),GROVE_CONFIG),generateGroveDefs,inMode='r')
version="dev"+datetime.now().strftime("%Y%m%d")
env.Append(CPPDEFINES=[('GWDEVVERSION',version)])
def cleangenerated(source, target, env):
od=outPath()
if os.path.isdir(od):
print("#cleaning up %s"%od)
for f in os.listdir(od):
if f == "." or f == "..":
continue
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" ],
CPPDEFINES=[(board,"1")]
)
#script does not run on clean yet - maybe in the future
env.AddPostAction("clean",cleangenerated)
#look for extra task scripts and include them here
for taskdir in userTaskDirs:
script = os.path.join(taskdir, "extra_task.py")
if os.path.isfile(script):
taskname = os.path.basename(os.path.normpath(taskdir))
print("#extra task script for '{}'".format(taskname))
with open(script) as fh:
try:
code = compile(fh.read(), taskname, 'exec')
except SyntaxError:
print("#ERROR: script does not compile")
continue
exec(code)

View File

@@ -1,518 +0,0 @@
print("running extra...")
import gzip
import shutil
import os
import sys
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"
GEN_DIR='lib/generated'
CFG_FILE='web/config.json'
XDR_FILE='web/xdrconfig.json'
INDEXJS="index.js"
INDEXCSS="index.css"
CFG_INCLUDE='GwConfigDefinitions.h'
CFG_INCLUDE_IMPL='GwConfigDefImpl.h'
XDR_INCLUDE='GwXdrTypeMappings.h'
TASK_INCLUDE='GwUserTasks.h'
GROVE_CONFIG="GwM5GroveGen.h"
GROVE_CONFIG_IN="lib/hardware/GwM5Grove.in"
EMBEDDED_INCLUDE="GwEmbeddedFiles.h"
def getEmbeddedFiles(env):
rt=[]
efiles=env.GetProjectOption("board_build.embed_files")
for f in efiles.split("\n"):
if f == '':
continue
rt.append(f)
return rt
def basePath():
#see: https://stackoverflow.com/questions/16771894/python-nameerror-global-name-file-is-not-defined
return os.path.dirname(inspect.getfile(lambda: None))
def outPath():
return os.path.join(basePath(),GEN_DIR)
def checkDir():
dn=outPath()
if not os.path.exists(dn):
os.makedirs(dn)
if not os.path.isdir(dn):
print("unable to create %s"%dn)
return False
return True
def isCurrent(infile,outfile):
if os.path.exists(outfile):
otime=os.path.getmtime(outfile)
itime=os.path.getmtime(infile)
if (otime >= itime):
own=os.path.join(basePath(),OWN_FILE)
if os.path.exists(own):
owntime=os.path.getmtime(own)
if owntime > otime:
return False
print("%s is newer then %s, no need to recreate"%(outfile,infile))
return True
return False
def compressFile(inFile,outfile):
if isCurrent(inFile,outfile):
return
print("compressing %s"%inFile)
with open(inFile, 'rb') as f_in:
with gzip.open(outfile, 'wb') as f_out:
shutil.copyfileobj(f_in, f_out)
def generateFile(infile,outfile,callback,inMode='rb',outMode='w'):
if isCurrent(infile,outfile):
return
print("creating %s"%outfile)
oh=None
with open(infile,inMode) as ch:
with open(outfile,outMode) as oh:
try:
callback(ch,oh,inFile=infile)
oh.close()
except Exception as e:
try:
oh.close()
except:
pass
os.unlink(outfile)
raise
def writeFileIfChanged(fileName,data):
if os.path.exists(fileName):
with open(fileName,"r") as ih:
old=ih.read()
ih.close()
if old == data:
return False
print("#generating %s"%fileName)
with open(fileName,"w") as oh:
oh.write(data)
return True
def mergeConfig(base,other):
for bdir in other:
cname=os.path.join(bdir,"config.json")
if os.path.exists(cname):
print("merge config %s"%cname)
with open(cname,'rb') as ah:
merge=json.load(ah)
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)
data=""
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,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)
data+="//generated from %s\n"%inFile
l=len(config)
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]='%(idx)
idx+=1
secret="false";
if item.get('type') == 'password':
secret="true"
data+=" new GwConfigInterface(%s,\"%s\",%s);\n"%(name,item.get('default'),secret)
data+='}\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")
first=True
for cat in jdoc:
item=jdoc[cat]
cid=item.get('id')
if cid is None:
continue
tc=item.get('type')
if tc is not None:
if first:
first=False
else:
oh.write(",\n")
oh.write(" new GwXDRTypeMapping(%d,0,%d) /*%s*/"%(cid,tc,cat))
fields=item.get('fields')
if fields is None:
continue
idx=0
for fe in fields:
if not isinstance(fe,dict):
continue
tc=fe.get('t')
id=fe.get('v')
if id is None:
id=idx
idx+=1
l=fe.get('l') or ''
if tc is None or id is None:
continue
if first:
first=False
else:
oh.write(",\n")
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
class Grove:
def __init__(self,name) -> None:
self.name=name
def _ss(self,z=False):
if z:
return self.name
return self.name if self.name is not 'Z' else ''
def _suffix(self):
return '_'+self.name if self.name is not 'Z' else ''
def replace(self,line):
if line is None:
return line
return line.replace('$G$',self._ss()).replace('$Z$',self._ss(True)).replace('$GS$',self._suffix())
def generateGroveDefs(inh,outh,inFile=''):
GROVES=[Grove('Z'),Grove('A'),Grove('B'),Grove('C')]
definition=[]
started=False
def writeConfig():
for grove in GROVES:
for cl in definition:
outh.write(grove.replace(cl))
for line in inh:
if re.match(" *#GROVE",line):
started=True
if len(definition) > 0:
writeConfig()
definition=[]
continue
if started:
definition.append(line)
if len(definition) > 0:
writeConfig()
userTaskDirs=[]
def getUserTaskDirs():
rt=[]
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:
#print("##taskdir=%s"%task)
base=os.path.basename(task)
includeNames=[base.lower()+".h",'gw'+base.lower()+'.h']
for f in os.listdir(task):
checkAndAdd(f,includeNames,includes)
includeData=""
for i in includes:
print("#task include %s"%i)
includeData+="#include <%s>\n"%i
writeFileIfChanged(outfile,includeData)
def generateEmbedded(elist,outFile):
content=""
for entry in elist:
content+="EMBED_GZ_FILE(\"%s\",%s,\"%s\");\n"%entry
writeFileIfChanged(outFile,content)
def getContentType(fn):
if (fn.endswith('.gz')):
fn=fn[0:-3]
if (fn.endswith('html')):
return "text/html"
if (fn.endswith('json')):
return "application/json"
if (fn.endswith('js')):
return "text/javascript"
if (fn.endswith('css')):
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
def joinFiles(target,pattern,dirlist):
flist=[]
for dir in dirlist:
fn=os.path.join(dir,pattern)
if os.path.exists(fn):
flist.append(fn)
current=False
if os.path.exists(target):
current=True
for f in flist:
if not isCurrent(f,target):
current=False
break
if current:
print("%s is up to date"%target)
return
print("creating %s"%target)
with gzip.open(target,"wb") as oh:
for fn in flist:
print("adding %s to %s"%(fn,target))
with open(fn,"rb") as rh:
shutil.copyfileobj(rh,oh)
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),False)
generateCfg(mergedConfig,os.path.join(outPath(),CFG_INCLUDE_IMPL),True)
joinFiles(os.path.join(outPath(),INDEXJS+".gz"),INDEXJS,["web"]+userTaskDirs)
joinFiles(os.path.join(outPath(),INDEXCSS+".gz"),INDEXCSS,["web"]+userTaskDirs)
embedded=getEmbeddedFiles(env)
filedefs=[]
for ef in embedded:
print("#checking embedded file %s"%ef)
(dn,fn)=os.path.split(ef)
pureName=fn
if pureName.endswith('.gz'):
pureName=pureName[0:-3]
ct=getContentType(pureName)
usname=ef.replace('/','_').replace('.','_')
filedefs.append((pureName,usname,ct))
inFile=os.path.join(basePath(),"web",pureName)
if os.path.exists(inFile):
compressFile(inFile,ef)
else:
print("#WARNING: infile %s for %s not found"%(inFile,ef))
generateEmbedded(filedefs,os.path.join(outPath(),EMBEDDED_INCLUDE))
genereateUserTasks(os.path.join(outPath(), TASK_INCLUDE))
generateFile(os.path.join(basePath(),XDR_FILE),os.path.join(outPath(),XDR_INCLUDE),generateXdrMappings)
generateFile(os.path.join(basePath(),GROVE_CONFIG_IN),os.path.join(outPath(),GROVE_CONFIG),generateGroveDefs,inMode='r')
version="dev"+datetime.now().strftime("%Y%m%d")
env.Append(CPPDEFINES=[('GWDEVVERSION',version)])
def cleangenerated(source, target, env):
od=outPath()
if os.path.isdir(od):
print("#cleaning up %s"%od)
for f in os.listdir(od):
if f == "." or f == "..":
continue
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" ],
CPPDEFINES=[(board,"1")]
)
#script does not run on clean yet - maybe in the future
env.AddPostAction("clean",cleangenerated)

View File

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

View File

@@ -1,7 +1,6 @@
#include <esp_wifi.h>
#include "GWWifi.h"
GwWifi::GwWifi(const GwConfigHandler *config,GwLog *log, bool fixedApPass){
this->config=config;
this->logger=log;
@@ -9,6 +8,28 @@ GwWifi::GwWifi(const GwConfigHandler *config,GwLog *log, bool fixedApPass){
wifiSSID=config->getConfigItem(config->wifiSSID,true);
wifiPass=config->getConfigItem(config->wifiPass,true);
this->fixedApPass=fixedApPass;
wifiMutex=xSemaphoreCreateMutex();
if (wifiMutex==nullptr){
LOG_DEBUG(GwLog::ERROR,"GwWifi: unable to create mutex");
}
}
GwWifi::~GwWifi(){
if (wifiMutex!=nullptr){
vSemaphoreDelete(wifiMutex);
wifiMutex=nullptr;
}
}
bool GwWifi::acquireMutex(){
if (wifiMutex==nullptr) return false;
return xSemaphoreTake(wifiMutex,WIFI_MUTEX_TIMEOUT)==pdTRUE;
}
void GwWifi::releaseMutex(){
if (wifiMutex!=nullptr){
xSemaphoreGive(wifiMutex);
}
}
void GwWifi::setup(){
LOG_DEBUG(GwLog::LOG,"Wifi setup");
@@ -85,8 +106,14 @@ bool GwWifi::connectInternal(){
if (wifiClient->asBoolean()){
clientIsConnected=false;
LOG_DEBUG(GwLog::LOG,"creating wifiClient ssid=%s",wifiSSID->asString().c_str());
// CRITICAL SECTION: WiFi-Operationen müssen serialisiert werden
if (!acquireMutex()){
LOG_DEBUG(GwLog::ERROR,"GwWifi: mutex timeout in connectInternal");
return false;
}
WiFi.setAutoReconnect(false); //#102
wl_status_t rt=WiFi.begin(wifiSSID->asCString(),wifiPass->asCString());
releaseMutex();
LOG_DEBUG(GwLog::LOG,"wifiClient connect returns %d",(int)rt);
lastConnectStart=millis();
return true;
@@ -104,8 +131,20 @@ void GwWifi::loop(){
if (lastConnectStart > now || (lastConnectStart + RETRY_MILLIS) < now)
{
LOG_DEBUG(GwLog::LOG,"wifiClient: retry connect to %s", wifiSSID->asCString());
WiFi.disconnect();
connectInternal();
// CRITICAL SECTION: WiFi-Operationen müssen serialisiert werden
if (acquireMutex()){
WiFi.disconnect(true);
delay(300);
esp_wifi_stop();
delay(100);
esp_wifi_start();
releaseMutex();
connectInternal();
}
else{
LOG_DEBUG(GwLog::ERROR,"GwWifi: mutex timeout in loop");
}
}
}
else{
@@ -126,11 +165,42 @@ void GwWifi::loop(){
}
}
}
bool GwWifi::clientConnected(){
return WiFi.status() == WL_CONNECTED;
// CRITICAL SECTION: WiFi.status() muss geschützt werden
if (!acquireMutex()){
LOG_DEBUG(GwLog::ERROR,"GwWifi: mutex timeout in clientConnected");
return false; // Conservative: nehme an, nicht verbunden
}
bool result = WiFi.status() == WL_CONNECTED;
releaseMutex();
return result;
};
bool GwWifi::connectClient(){
// CRITICAL SECTION: Disconnect und Connect müssen atomar sein
if (!acquireMutex()){
LOG_DEBUG(GwLog::ERROR,"GwWifi: mutex timeout in connectClient");
return false;
}
WiFi.disconnect();
releaseMutex();
return connectInternal();
}
bool GwWifi::connectClientAsync(){
// Non-blocking version: Versuche Mutex zu nehmen, gib aber sofort auf
// Ideal für Tasks, die nicht blockieren dürfen
if (wifiMutex==nullptr){
LOG_DEBUG(GwLog::ERROR,"GwWifi: mutex not initialized in connectClientAsync");
return false;
}
if (xSemaphoreTake(wifiMutex, 0)!=pdTRUE){
LOG_DEBUG(GwLog::LOG,"GwWifi: connectClientAsync skipped - WiFi busy");
return false; // WiFi ist aktuell busy, versuche es später nochmal
}
WiFi.disconnect();
xSemaphoreGive(wifiMutex);
return connectInternal();
}

View File

@@ -14,6 +14,30 @@ https://controllerstech.com/ws2812-leds-using-spi/
*/
String Color::toHex() {
char hexColor[8];
sprintf(hexColor, "#%02X%02X%02X", r, g, b);
return String(hexColor);
}
String Color::toName() {
static std::map<int, String> const names = {
{0xff0000, "Red"},
{0x00ff00, "Green"},
{0x0000ff, "Blue",},
{0xff9900, "Orange"},
{0xffff00, "Yellow"},
{0x3366ff, "Aqua"},
{0xff0066, "Violet"},
{0xffffff, "White"}
};
int color = (r << 16) + (g << 8) + b;
auto it = names.find(color);
if (it == names.end()) {
return toHex();
}
return it->second;
}
static uint8_t mulcolor(uint8_t f1, uint8_t f2){
uint16_t rt=f1;
@@ -231,4 +255,4 @@ void handleSpiLeds(void *param){
void createSpiLedTask(LedTaskData *param){
xTaskCreate(handleSpiLeds,"handleLeds",4000,param,3,NULL);
}
}

View File

@@ -10,7 +10,7 @@ class Color{
uint8_t g;
uint8_t b;
Color():r(0),g(0),b(0){}
Color(uint8_t cr, uint8_t cg,uint8_t cb):
Color(uint8_t cr, uint8_t cg, uint8_t cb):
b(cb),g(cg),r(cr){}
Color(const Color &o):b(o.b),g(o.g),r(o.r){}
bool equal(const Color &o) const{
@@ -22,6 +22,8 @@ class Color{
bool operator != (const Color &other) const{
return ! equal(other);
}
String toHex();
String toName();
};
static Color COLOR_GREEN=Color(0,255,0);

View File

@@ -1,4 +1,7 @@
#include "NetworkClient.h"
#include "GWWifi.h" // WiFi management (thread-safe)
extern GwWifi gwWifi; // Extern declaration of global WiFi instance
extern "C" {
#include "puff.h"
@@ -51,8 +54,13 @@ bool NetworkClient::httpGetGzip(const String& url, uint8_t*& outData, size_t& ou
const size_t capacity = READLIMIT; // Read limit for data (can be adjusted in NetworkClient.h)
uint8_t* buffer = (uint8_t*)malloc(capacity);
if (!gwWifi.clientConnected()) {
if (DEBUGING) {Serial.println("No WiFi connection");}
return false;
}
if (!buffer) {
if (DEBUG) {Serial.println("Malloc failed (buffer");}
if (DEBUGING) {Serial.println("Malloc failed buffer");}
return false;
}
@@ -106,7 +114,7 @@ bool NetworkClient::httpGetGzip(const String& url, uint8_t*& outData, size_t& ou
len += read;
lastData = millis();
if (DEBUG) {Serial.printf("Read chunk: %d (total: %d)\n", read, (int)len);}
if (DEBUGING) {Serial.printf("Read chunk: %d (total: %d)\n", read, (int)len);}
if (len < 20) continue; // Not enough data for header
@@ -122,7 +130,7 @@ bool NetworkClient::httpGetGzip(const String& url, uint8_t*& outData, size_t& ou
int res = puff(test, &testLen, buffer + headerOffset, &srcLen);
if (res == 0) {
if (DEBUG) {Serial.printf("Decompress OK! Size: %lu bytes\n", testLen);}
if (DEBUGING) {Serial.printf("Decompress OK! Size: %lu bytes\n", testLen);}
outData = test;
outLen = testLen;
complete = true;
@@ -167,7 +175,7 @@ bool NetworkClient::fetchAndDecompressJson(const String& url) {
return false;
}
if (DEBUG) {Serial.println("JSON OK!");}
if (DEBUGING) {Serial.println("JSON OK!");}
_valid = true;
return true;
}

View File

@@ -3,7 +3,7 @@
#include <WiFi.h>
#include <HTTPClient.h>
#define DEBUG false // Debug flag for NetworkClient for more live information
#define DEBUGING false // Debug flag for NetworkClient for more live information
#define READLIMIT 200000 // HTTP read limit in byte for gzip content (can be adjusted)
#define CONNECTIONTIMEOUT 3000 // Timeout in ms for HTTP connection
#define TCPREADTIMEOUT 2000 // Timeout in ms for read HTTP client stack

View File

@@ -923,7 +923,7 @@ void solarGraphic(uint x, uint y, int pcolor, int bcolor){
}
// Generator graphic with fill level
// Generator graphic
void generatorGraphic(uint x, uint y, int pcolor, int bcolor){
// Show battery
int xb = x; // X position
@@ -940,6 +940,74 @@ void generatorGraphic(uint x, uint y, int pcolor, int bcolor){
getdisplay().print("G");
}
// Display rudder position as horizontal bargraph with configurable +/- range (degrees)
void displayRudderPosition(int rudderPosition, uint8_t rangeDeg, uint16_t cx, uint16_t cy, uint16_t fg, uint16_t bg){
const int w = 360;
const int h = 20;
const int t = 3; // Line thickness
const int halfw = w/2;
const int halfh = h/2;
// Calculate top-left of bar (cx,cy are center of 0°)
int left = int(cx) - halfw;
int top = int(cy) - halfh;
// clamp provided range to allowed bounds [10,45]
if (rangeDeg < 10) rangeDeg = 10;
if (rangeDeg > 45) rangeDeg = 45;
// Pixels per degree for +/-rangeDeg -> total span = 2*rangeDeg
const float pxPerDeg = float(w) / (2.0f * float(rangeDeg));
// Draw outer border (thickness t)
for (int i = 0; i < t; i++) {
getdisplay().drawRect(left + i, top + i, w - 2 * i, h - 2 * i, fg);
}
// Fill inner area with background
getdisplay().fillRect(left + t, top + t, w - 2 * t, h - 2 * t, bg);
// Draw center line
getdisplay().drawRect(cx - 1, top + 1, 3 , h - 2, fg);
// Clamp rudder position to -rangeDeg..rangeDeg
if (rudderPosition > (int)rangeDeg) rudderPosition = (int)rangeDeg;
if (rudderPosition < -((int)rangeDeg)) rudderPosition = -((int)rangeDeg);
// Compute fill width in pixels
int fillPx = int(round(rudderPosition * pxPerDeg)); // positive -> right
// Fill area from center to position (if non-zero)
int centerx = cx;
int innerTop = top + t;
int innerH = h - 2 * t;
if (fillPx > 0) {
// Right side
getdisplay().fillRect(centerx, innerTop, fillPx, innerH, fg);
} else if (fillPx < 0) {
// Left side
getdisplay().fillRect(centerx + fillPx, innerTop, -fillPx, innerH, fg);
}
// Draw tick marks every 5° and labels outside the bar
getdisplay().setTextColor(fg);
getdisplay().setFont(&Ubuntu_Bold8pt8b);
for (int angle = -((int)rangeDeg); angle <= (int)rangeDeg; angle += 5) {
int xpos = int(round(centerx + angle * pxPerDeg));
// Vertical tick inside bar
getdisplay().drawLine(xpos, top, xpos, top + h + 2, fg);
// Label outside: below the bar
String lbl = String(angle);
int16_t bx, by;
uint16_t bw, bh;
getdisplay().getTextBounds(lbl, 0, 0, &bx, &by, &bw, &bh);
int16_t tx = xpos - bw/2;
int16_t ty = top + h + bh + 5; // A little spacing
getdisplay().setCursor(tx, ty);
getdisplay().print(lbl);
}
}
// Function to handle HTTP image request
// http://192.168.15.1/api/user/OBP60Task/screenshot
void doImageRequest(GwApi *api, int *pageno, const PageStruct pages[MAX_PAGE_NUMBER], AsyncWebServerRequest *request) {

View File

@@ -128,6 +128,10 @@ void solarGraphic(uint x, uint y, int pcolor, int bcolor); // S
void generatorGraphic(uint x, uint y, int pcolor, int bcolor); // Generator graphic
void startLedTask(GwApi *api);
// Display rudder position as horizontal bargraph with configurable +/- range (degrees)
// 'rangeDeg' is unsigned and will be clamped to [10,45]
void displayRudderPosition(int rudderPosition, uint8_t rangeDeg, uint16_t cx, uint16_t cy, uint16_t fg, uint16_t bg);
void doImageRequest(GwApi *api, int *pageno, const PageStruct pages[MAX_PAGE_NUMBER], AsyncWebServerRequest *request);
// Icons

View File

@@ -835,7 +835,7 @@ FormattedData formatValue(GwApi::BoatValue *value, CommonData &commondata, bool
result.cvalue = dplace;
}
//########################################################
else if (value->getFormat() == "formatXdr:A:D"){
else if ((value->getFormat() == "formatXdr:A:D") || ((value->getFormat() == "formatXdr:A:rd"))){
double angle = 0;
if (usesimudata == false) {
angle = value->value;

View File

@@ -1,7 +1,7 @@
// General hardware definitions
// CAN and RS485 bus pin definitions see obp60task.h
#ifdef HARDWARE_V21
#if defined HARDWARE_V20 || HARDWARE_V21
// Direction pin for RS485 NMEA0183
#define OBP_DIRECTION_PIN 18
// I2C

View File

@@ -58,7 +58,7 @@ void initKeys(CommonData &commonData) {
commonData.keydata[5].h = height;
}
#ifdef HARDWARE_V21
#if defined HARDWARE_V20 || HARDWARE_V21
// Keypad functions for original OBP60 hardware
int readKeypad(GwLog* logger, uint thSensitivity, bool use_syspage) {

View File

@@ -135,7 +135,7 @@ bool CalibrationData::calibrateInstance(GwApi::BoatValue* boatDataValue)
double dataValue = 0;
std::string format = "";
// we test this earlier, but for safety reason ...
// we test this earlier, but for safety reasons ...
if (calibrationMap.find(instance) == calibrationMap.end()) {
LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s not in calibration list", instance.c_str());
return false;
@@ -151,7 +151,7 @@ bool CalibrationData::calibrateInstance(GwApi::BoatValue* boatDataValue)
slope = calibrationMap[instance].slope;
dataValue = boatDataValue->value;
format = boatDataValue->getFormat().c_str();
LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s: value: %f, format: %s", instance.c_str(), dataValue, format.c_str());
// LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s: value: %f, format: %s", instance.c_str(), dataValue, format.c_str());
if (format == "formatWind") { // instance is of type angle
dataValue = (dataValue * slope) + offset;
@@ -174,7 +174,7 @@ bool CalibrationData::calibrateInstance(GwApi::BoatValue* boatDataValue)
calibrationMap[instance].value = dataValue; // store the calibrated value in the list
calibrationMap[instance].isCalibrated = true;
LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s: Offset: %f, Slope: %f, Result: %f", instance.c_str(), offset, slope, calibrationMap[instance].value);
// LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s: Offset: %f, Slope: %f, Result: %f", instance.c_str(), offset, slope, calibrationMap[instance].value);
return true;
}
@@ -189,7 +189,7 @@ bool CalibrationData::smoothInstance(GwApi::BoatValue* boatDataValue)
// we test this earlier, but for safety reason ...
if (calibrationMap.find(instance) == calibrationMap.end()) {
LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s not in calibration list", instance.c_str());
// LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s not in calibration list", instance.c_str());
return false;
}
@@ -211,7 +211,7 @@ bool CalibrationData::smoothInstance(GwApi::BoatValue* boatDataValue)
calibrationMap[instance].value = dataValue; // store the smoothed value in the list
calibrationMap[instance].isCalibrated = true;
LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s: smooth: %f, oldValue: %f, result: %f", instance.c_str(), smoothFactor, oldValue, calibrationMap[instance].value);
// LOG_DEBUG(GwLog::DEBUG, "BoatDataCalibration: %s: smooth: %f, oldValue: %f, result: %f", instance.c_str(), smoothFactor, oldValue, calibrationMap[instance].value);
return true;
}
@@ -241,7 +241,7 @@ void HstryBuf::add(double value)
{
if (value >= hstryMin && value <= hstryMax) {
hstryBuf.add(value);
LOG_DEBUG(GwLog::DEBUG, "HstryBuf::add: name: %s, value: %.3f", hstryBuf.getName(), value);
// LOG_DEBUG(GwLog::DEBUG, "HstryBuf::add: name: %s, value: %.3f", hstryBuf.getName(), value);
}
}
@@ -405,7 +405,7 @@ void WindUtils::calcTwdSA(const double* AWA, const double* AWS,
double stw = -*STW;
addPolar(AWD, AWS, CTW, &stw, TWD, TWS);
// Normalize TWD and TWA to 0-360°/2PI
// Normalize TWD to [0..360°] (2PI) and TWA to [-180..180] (PI)
*TWD = to2PI(*TWD);
*TWA = toPI(*TWD - *HDT);
}
@@ -487,8 +487,8 @@ bool WindUtils::addWinds()
double hdtVal = hdtBVal->valid ? hdtBVal->value : DBL_MAX;
double hdmVal = hdmBVal->valid ? hdmBVal->value : DBL_MAX;
double varVal = varBVal->valid ? varBVal->value : DBL_MAX;
LOG_DEBUG(GwLog::DEBUG, "WindUtils:addWinds: AWA %.1f, AWS %.1f, COG %.1f, STW %.1f, SOG %.2f, HDT %.1f, HDM %.1f, VAR %.1f", awaBVal->value * RAD_TO_DEG, awsBVal->value * 3.6 / 1.852,
cogBVal->value * RAD_TO_DEG, stwBVal->value * 3.6 / 1.852, sogBVal->value * 3.6 / 1.852, hdtBVal->value * RAD_TO_DEG, hdmBVal->value * RAD_TO_DEG, varBVal->value * RAD_TO_DEG);
//LOG_DEBUG(GwLog::DEBUG, "WindUtils:addWinds: AWA %.1f, AWS %.1f, COG %.1f, STW %.1f, SOG %.2f, HDT %.1f, HDM %.1f, VAR %.1f", awaBVal->value * RAD_TO_DEG, awsBVal->value * 3.6 / 1.852,
// cogBVal->value * RAD_TO_DEG, stwBVal->value * 3.6 / 1.852, sogBVal->value * 3.6 / 1.852, hdtBVal->value * RAD_TO_DEG, hdmBVal->value * RAD_TO_DEG, varBVal->value * RAD_TO_DEG);
// Check if TWD can be calculated from TWA and HDT/HDM
if (twaBVal->valid) {
@@ -528,8 +528,8 @@ bool WindUtils::addWinds()
}
}
}
LOG_DEBUG(GwLog::DEBUG, "WindUtils:addWinds: twCalculated %d, TWD %.1f, TWA %.1f, TWS %.2f kn, AWD: %.1f", twCalculated, twdBVal->value * RAD_TO_DEG,
twaBVal->value * RAD_TO_DEG, twsBVal->value * 3.6 / 1.852, awdBVal->value * RAD_TO_DEG);
// LOG_DEBUG(GwLog::DEBUG, "WindUtils:addWinds: twCalculated %d, TWD %.1f, TWA %.1f, TWS %.2f kn, AWD: %.1f", twCalculated, twdBVal->value * RAD_TO_DEG,
// twaBVal->value * RAD_TO_DEG, twsBVal->value * 3.6 / 1.852, awdBVal->value * RAD_TO_DEG);
return twCalculated;
}

View File

@@ -49,8 +49,10 @@ void sensorTask(void *param){
// Init sensor stuff
bool oneWire_ready = false; // 1Wire initialized and ready to use
bool iRTC_ready = false; // Software RTC initialized and ready to use
bool RTC_ready = false; // DS1388 initialized and ready to use
bool GPS_ready = false; // GPS initialized and ready to use
bool N2K_GPS_ready = false; // GPS time on N2K bus
bool BME280_ready = false; // BME280 initialized and ready to use
bool BMP280_ready = false; // BMP280 initialized and ready to use
bool BMP180_ready = false; // BMP180 initialized and ready to use
@@ -369,7 +371,7 @@ void sensorTask(void *param){
GwApi::BoatValue *hdop=new GwApi::BoatValue(GwBoatData::_HDOP);
GwApi::BoatValue *valueList[]={gpsdays, gpsseconds, hdop};
// Internal RTC with NTP init
// Internal iRTC with NTP init
ESP32Time rtc(0);
if (api->getConfig()->getString(api->getConfig()->timeSource) == "iRTC") {
GwApi::Status status;
@@ -382,6 +384,7 @@ void sensorTask(void *param){
if (getLocalTime(&timeinfo)) {
api->getLogger()->logDebug(GwLog::LOG,"NTP time: %04d-%02d-%02d %02d:%02d:%02d UTC", timeinfo.tm_year+1900, timeinfo.tm_mon+1, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
rtc.setTimeStruct(timeinfo);
iRTC_ready = true;
sensors.rtcValid = true;
} else {
api->getLogger()->logDebug(GwLog::LOG,"NTP time fetch failed!");
@@ -400,7 +403,7 @@ void sensorTask(void *param){
if (millis() > starttime0 + 100)
{
starttime0 = millis();
// Send NMEA0183 GPS data on several bus systems all 100ms
// Send NMEA0183 GPS data on several bus systems (N2K an 0183) all 100ms
if (GPS_ready == true && hdop->value <= hdopAccuracy)
{
SNMEA0183Msg NMEA0183Msg;
@@ -412,9 +415,55 @@ void sensorTask(void *param){
}
// If RTC DS1388 ready, then copy GPS data to RTC all 5min
if(millis() > starttime11 + 5*60*1000){
/*
Time set logic for RTC and N2K
###############################
iRTC = Software RTC updatetd with NTP via internet
RTC = RTC chip on PCB
GPS = GPS Receiver on PCB
N2K = GPS time on N2K od 183 bus
0 = device not ready
1 = device ready
X = independend
() = source for set time N2K
-> = set RTC via iRTC
<- = set RTC via GPS
iRTC RTC GPS N2K
0 0 0 (1)
0 0 (1) X
0 (1) 0 X
0 1 <-(1) X
(1) 0 0 X
1 0 (1) X
1 ->(1) 0 X
1 1 <-(1) X
*/
// If RTC DS1388 ready, then copy iRTC and GPS data to RTC all 1 min
if(millis() > starttime11 + 1*60*1000){
starttime11 = millis();
// Set RTC chip via iRTC (NTP)
if(iRTC_ready == true && RTC_ready == true && GPS_ready == false){
GwApi::Status status;
api->getStatus(status);
// Check WiFi connection
if (status.wifiClientConnected) {
sensors.rtcTime = rtc.getTimeStruct(); // Get time from software RTC (iRTC)
DateTime now = DateTime(
sensors.rtcTime.tm_year + 1900,
sensors.rtcTime.tm_mon + 1,
sensors.rtcTime.tm_mday,
sensors.rtcTime.tm_hour,
sensors.rtcTime.tm_min,
sensors.rtcTime.tm_sec
);
ds1388.adjust(now);
}
}
// Set RTC chip via internal GPS
if(rtcOn == "DS1388" && RTC_ready == true && GPS_ready == true){
api->getBoatDataValues(3,valueList);
if(gpsdays->valid && gpsseconds->valid && hdop->valid){
@@ -422,40 +471,33 @@ void sensorTask(void *param){
// sample input: date = "Dec 26 2009", time = "12:34:56"
// ds1388.adjust(DateTime("Dec 26 2009", "12:34:56"));
DateTime adjusttime(ts);
api->getLogger()->logDebug(GwLog::LOG,"Adjust RTC time: %04d/%02d/%02d %02d:%02d:%02d",adjusttime.year(), adjusttime.month(), adjusttime.day(), adjusttime.hour(), adjusttime.minute(), adjusttime.second());
api->getLogger()->logDebug(GwLog::LOG,"Adjust RTC time via internal GPS: %04d/%02d/%02d %02d:%02d:%02d",adjusttime.year(), adjusttime.month(), adjusttime.day(), adjusttime.hour(), adjusttime.minute(), adjusttime.second());
// Adjust RTC time as unix time value
ds1388.adjust(adjusttime);
}
}
}
}
// Send 1Wire data for all temperature sensors all 2s
if(millis() > starttime13 + 2000 && String(oneWireOn) == "DS18B20" && oneWire_ready == true){
starttime13 = millis();
float tempC;
ds18b20.requestTemperatures(); // Collect all temperature values (max.8)
for(int i=0;i<numberOfDevices; i++){
// Send only one 1Wire data per loop step (time reduction)
if(i == loopCounter % numberOfDevices){
if(ds18b20.getAddress(tempDeviceAddress, i)){
// Read temperature value in Celsius
tempC = ds18b20.getTempC(tempDeviceAddress);
}
// Send to NMEA200 bus for each sensor with instance number
if(!isnan(tempC)){
sensors.onewireTemp[i] = tempC; // Save values in SensorData
api->getLogger()->logDebug(GwLog::DEBUG,"DS18B20-%1d Temp: %.1f",i,tempC);
SetN2kPGN130316(N2kMsg, 0, i, N2kts_OutsideTemperature, CToKelvin(tempC), N2kDoubleNA);
api->sendN2kMessage(N2kMsg);
}
}
}
loopCounter++;
// Set RTC chip via N2K or 183 in case the internal GPS is off (only one time)
if(N2K_GPS_ready == false && RTC_ready == true && GPS_ready == false){
api->getBoatDataValues(3,valueList);
if(gpsdays->valid && gpsseconds->valid && hdop->valid){
long ts = tNMEA0183Msg::daysToTime_t(gpsdays->value - (30*365+7))+floor(gpsseconds->value); // Adjusted to reference year 2000 (-30 years and 7 days for switch years)
// sample input: date = "Dec 26 2009", time = "12:34:56"
// ds1388.adjust(DateTime("Dec 26 2009", "12:34:56"));
DateTime adjusttime(ts);
api->getLogger()->logDebug(GwLog::LOG,"Adjust RTC time via N2K/183: %04d/%02d/%02d %02d:%02d:%02d",adjusttime.year(), adjusttime.month(), adjusttime.day(), adjusttime.hour(), adjusttime.minute(), adjusttime.second());
// Adjust RTC time as unix time value
ds1388.adjust(adjusttime);
// N2K GPS time ready
N2K_GPS_ready = true;
}
}
// Get current RTC date and time all 500ms
// Send RTC date and time to N2K all 500ms
if (millis() > starttime12 + 500) {
starttime12 = millis();
// Send date and time from RTC chip if GPS not ready
if (rtcOn == "DS1388" && RTC_ready) {
DateTime dt = ds1388.now();
sensors.rtcTime.tm_year = dt.year() - 1900; // Save values in SensorData
@@ -481,21 +523,63 @@ void sensorTask(void *param){
}
// N2K sysTime is double in n2klib
double sysTime = (dt.hour() * 3600) + (dt.minute() * 60) + dt.second();
// WHY? isnan should always fail here
//if(!isnan(daysAt1970) && !isnan(sysTime)){
//api->getLogger()->logDebug(GwLog::LOG,"RTC time: %04d/%02d/%02d %02d:%02d:%02d",sensors.rtcTime.tm_year+1900,sensors.rtcTime.tm_mon, sensors.rtcTime.tm_mday, sensors.rtcTime.tm_hour, sensors.rtcTime.tm_min, sensors.rtcTime.tm_sec);
if(!isnan(daysAt1970) && !isnan(sysTime)){
//api->getLogger()->logDebug(GwLog::LOG,"RTC time: %04d/%02d/%02d %02d:%02d:%02d",sensors.rtcTime.tm_year+1900,sensors.rtcTime.tm_mon+1, sensors.rtcTime.tm_mday, sensors.rtcTime.tm_hour, sensors.rtcTime.tm_min, sensors.rtcTime.tm_sec);
//api->getLogger()->logDebug(GwLog::LOG,"Send PGN126992: %10d %10d",daysAt1970, (uint16_t)sysTime);
SetN2kPGN126992(N2kMsg,0,daysAt1970,sysTime,N2ktimes_LocalCrystalClock);
api->sendN2kMessage(N2kMsg);
// }
}
}
} else if (sensors.rtcValid) {
// use internal rtc feature
sensors.rtcTime = rtc.getTimeStruct();
}
// Send date and time from software RTC (iRTC)
if (iRTC_ready == true && RTC_ready == false && GPS_ready == false) {
sensors.rtcTime = rtc.getTimeStruct();
const short daysOfYear[12] = {0,31,59,90,120,151,181,212,243,273,304,334};
int year = sensors.rtcTime.tm_year + 1900;
int month = sensors.rtcTime.tm_mon;
int day = sensors.rtcTime.tm_mday;
uint16_t switchYear = ((year - 1) - 1968) / 4 - ((year - 1) - 1900) / 100 + ((year - 1) - 1600) / 400;
long daysAt1970 = (year - 1970) * 365L + switchYear + daysOfYear[month] + day - 1;
// Leap day add if date is after Feb (i.e. month >= March)
if (month >= 2 && (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0))) {
daysAt1970 += 1;
}
double sysTime = sensors.rtcTime.tm_hour * 3600.0 + sensors.rtcTime.tm_min * 60.0 + sensors.rtcTime.tm_sec;
//api->getLogger()->logDebug(GwLog::LOG, "iRTC time: %04d/%02d/%02d %02d:%02d:%02d", year, month + 1, day, sensors.rtcTime.tm_hour, sensors.rtcTime.tm_min, sensors.rtcTime.tm_sec);
//api->getLogger()->logDebug(GwLog::LOG,"Send PGN126992: %10d %10d",daysAt1970, (uint16_t)sysTime);
SetN2kPGN126992(N2kMsg, 0, daysAt1970, sysTime, N2ktimes_LocalCrystalClock);
api->sendN2kMessage(N2kMsg);
}
}
// Send supply voltage value all 1s
// Send 1Wire data for all temperature sensors to N2K all 2s
if(millis() > starttime13 + 2000 && String(oneWireOn) == "DS18B20" && oneWire_ready == true){
starttime13 = millis();
float tempC;
ds18b20.requestTemperatures(); // Collect all temperature values (max.8)
for(int i=0;i<numberOfDevices; i++){
// Send only one 1Wire data per loop step (time reduction)
if(i == loopCounter % numberOfDevices){
if(ds18b20.getAddress(tempDeviceAddress, i)){
// Read temperature value in Celsius
tempC = ds18b20.getTempC(tempDeviceAddress);
}
// Send to NMEA200 bus for each sensor with instance number
if(!isnan(tempC)){
sensors.onewireTemp[i] = tempC; // Save values in SensorData
api->getLogger()->logDebug(GwLog::DEBUG,"DS18B20-%1d Temp: %.1f",i,tempC);
SetN2kPGN130316(N2kMsg, 0, i, N2kts_OutsideTemperature, CToKelvin(tempC), N2kDoubleNA);
api->sendN2kMessage(N2kMsg);
}
}
}
loopCounter++;
}
// Send supply voltage value to N2K all 1s
if(millis() > starttime5 + 1000 && String(powsensor1) == "off"){
starttime5 = millis();
float rawVoltage = 0; // Default value
@@ -565,7 +649,7 @@ void sensorTask(void *param){
#endif
}
// Send data from environment sensor all 2s
// Send data from environment sensor to N2K all 2s
if(millis() > starttime6 + 2000){
starttime6 = millis();
unsigned char TempSource = 2; // Inside temperature
@@ -630,7 +714,7 @@ void sensorTask(void *param){
}
}
// Send rotation angle all 500ms
// Send rotation angle to N2K all 500ms
if(millis() > starttime7 + 500){
starttime7 = millis();
double rotationAngle=0;
@@ -678,7 +762,7 @@ void sensorTask(void *param){
}
}
// Send battery power value all 1s
// Send battery power value to N2K all 1s
if(millis() > starttime8 + 1000 && (String(powsensor1) == "INA219" || String(powsensor1) == "INA226")){
starttime8 = millis();
if(String(powsensor1) == "INA226" && INA226_1_ready == true){
@@ -720,7 +804,7 @@ void sensorTask(void *param){
}
}
// Send solar power value all 1s
// Send solar power value to N2K all 1s
if(millis() > starttime9 + 1000 && (String(powsensor2) == "INA219" || String(powsensor2) == "INA226")){
starttime9 = millis();
if(String(powsensor2) == "INA226" && INA226_2_ready == true){
@@ -750,7 +834,7 @@ void sensorTask(void *param){
}
}
// Send generator power value all 1s
// Send generator power value to N2K all 1s
if(millis() > starttime10 + 1000 && (String(powsensor3) == "INA219" || String(powsensor3) == "INA226")){
starttime10 = millis();
if(String(powsensor3) == "INA226" && INA226_3_ready == true){

View File

@@ -161,8 +161,8 @@ bool Chart::setChartDimensions(const char direction, const int8_t size)
break;
}
}
LOG_DEBUG(GwLog::ERROR, "obp60:setChartDimensions %s: direction: %c, size: %d, dWidth: %d, dHeight: %d, timAxis: %d, valAxis: %d, cRoot{%d, %d}, top: %d, bottom: %d, hGap: %d, vGap: %d",
dataBuf.getName(), direction, size, dWidth, dHeight, timAxis, valAxis, cRoot.x, cRoot.y, top, bottom, hGap, vGap);
//LOG_DEBUG(GwLog::DEBUG, "obp60:setChartDimensions %s: direction: %c, size: %d, dWidth: %d, dHeight: %d, timAxis: %d, valAxis: %d, cRoot{%d, %d}, top: %d, bottom: %d, hGap: %d, vGap: %d",
// dataBuf.getName(), direction, size, dWidth, dHeight, timAxis, valAxis, cRoot.x, cRoot.y, top, bottom, hGap, vGap);
return true;
}
@@ -176,7 +176,7 @@ void Chart::drawChrt(const char chrtDir, const int8_t chrtIntv, GwApi::BoatValue
// LOG_DEBUG(GwLog::DEBUG, "Chart:drawChart: min: %.1f, mid: %.1f, max: %.1f, rng: %.1f", chrtMin, chrtMid, chrtMax, chrtRng);
calcChrtBorders(chrtMin, chrtMid, chrtMax, chrtRng);
chrtScale = double(valAxis) / chrtRng; // Chart scale: pixels per value step
LOG_DEBUG(GwLog::DEBUG, "Chart:drawChart: min: %.1f, mid: %.1f, max: %.1f, rng: %.1f", chrtMin, chrtMid, chrtMax, chrtRng);
// LOG_DEBUG(GwLog::DEBUG, "Chart:drawChart: min: %.1f, mid: %.1f, max: %.1f, rng: %.1f", chrtMin, chrtMid, chrtMax, chrtRng);
// Do we have valid buffer data?
if (dataBuf.getMax() == dbMAX_VAL) { // only <MAX_VAL> values in buffer -> no valid wind data available
@@ -261,8 +261,8 @@ void Chart::calcChrtBorders(double& rngMin, double& rngMid, double& rngMax, doub
}
recalcRngMid = false; // Reset flag for <rngMid> determination
LOG_DEBUG(GwLog::DEBUG, "calcChrtRange: rngMin: %.1f°, rngMid: %.1f°, rngMax: %.1f°, rng: %.1f°, rngStep: %.1f°", rngMin * RAD_TO_DEG, rngMid * RAD_TO_DEG, rngMax * RAD_TO_DEG,
rng * RAD_TO_DEG, rngStep * RAD_TO_DEG);
// LOG_DEBUG(GwLog::DEBUG, "calcChrtRange: rngMin: %.1f°, rngMid: %.1f°, rngMax: %.1f°, rng: %.1f°, rngStep: %.1f°", rngMin * RAD_TO_DEG, rngMid * RAD_TO_DEG, rngMax * RAD_TO_DEG,
// rng * RAD_TO_DEG, rngStep * RAD_TO_DEG);
}
}
@@ -287,8 +287,8 @@ void Chart::calcChrtBorders(double& rngMin, double& rngMid, double& rngMax, doub
rng = halfRng * 2.0;
LOG_DEBUG(GwLog::DEBUG, "calcChrtBorders: rngMin: %.1f°, rngMid: %.1f°, rngMax: %.1f°, tmpRng: %.1f°, rng: %.1f°, rngStep: %.1f°", rngMin * RAD_TO_DEG, rngMid * RAD_TO_DEG, rngMax * RAD_TO_DEG,
tmpRng * RAD_TO_DEG, rng * RAD_TO_DEG, rngStep * RAD_TO_DEG);
// LOG_DEBUG(GwLog::DEBUG, "calcChrtBorders: rngMin: %.1f°, rngMid: %.1f°, rngMax: %.1f°, tmpRng: %.1f°, rng: %.1f°, rngStep: %.1f°", rngMin * RAD_TO_DEG, rngMid * RAD_TO_DEG, rngMax * RAD_TO_DEG,
// tmpRng * RAD_TO_DEG, rng * RAD_TO_DEG, rngStep * RAD_TO_DEG);
} else { // chart data is of any other type
@@ -320,8 +320,8 @@ void Chart::calcChrtBorders(double& rngMin, double& rngMid, double& rngMax, doub
rngMid = (rngMin + rngMax) / 2.0;
rng = rngMax - rngMin;
LOG_DEBUG(GwLog::DEBUG, "calcChrtRange-end: currMinVal: %.1f, currMaxVal: %.1f, rngMin: %.1f, rngMid: %.1f, rngMax: %.1f, rng: %.1f, rngStep: %.1f, zeroValue: %.1f, dbMIN_VAL: %.1f",
currMinVal, currMaxVal, rngMin, rngMid, rngMax, rng, rngStep, zeroValue, dbMIN_VAL);
// LOG_DEBUG(GwLog::DEBUG, "calcChrtRange-end: currMinVal: %.1f, currMaxVal: %.1f, rngMin: %.1f, rngMid: %.1f, rngMax: %.1f, rng: %.1f, rngStep: %.1f, zeroValue: %.1f, dbMIN_VAL: %.1f",
// currMinVal, currMaxVal, rngMin, rngMid, rngMax, rng, rngStep, zeroValue, dbMIN_VAL);
}
}
@@ -397,6 +397,8 @@ void Chart::drawChartLines(const char direction, const int8_t chrtIntv, const do
}
break;
}
taskYIELD(); // we run for 50-150ms; be polite to other tasks with same priority
}
}
@@ -656,7 +658,7 @@ void Chart::prntHorizChartThreeValueAxisLabel(const GFXfont* font)
if (font == &Ubuntu_Bold10pt8b) {
xOffset = 39;
yOffset = 15;
yOffset = 16;
} else if (font == &Ubuntu_Bold12pt8b) {
xOffset = 51;
yOffset = 18;
@@ -718,7 +720,7 @@ void Chart::prntHorizChartMultiValueAxisLabel(const GFXfont* font)
axSlots = valAxis / static_cast<double>(VALAXIS_STEP); // number of axis labels (and we want to have a double calculation, no integer)
axIntv = chrtRng / axSlots;
axLabel = chrtMin + axIntv;
LOG_DEBUG(GwLog::DEBUG, "Chart::printHorizMultiValueAxisLabel: chrtRng: %.2f, th-chrtRng: %.2f, axSlots: %.2f, axIntv: %.2f, axLabel: %.2f, chrtMin: %.2f, chrtMid: %.2f, chrtMax: %.2f", chrtRng, this->chrtRng, axSlots, axIntv, axLabel, this->chrtMin, chrtMid, chrtMax);
// LOG_DEBUG(GwLog::DEBUG, "Chart::printHorizMultiValueAxisLabel: chrtRng: %.2f, th-chrtRng: %.2f, axSlots: %.2f, axIntv: %.2f, axLabel: %.2f, chrtMin: %.2f, chrtMid: %.2f, chrtMax: %.2f", chrtRng, this->chrtRng, axSlots, axIntv, axLabel, this->chrtMin, chrtMid, chrtMax);
int loopStrt, loopEnd, loopStp;
if (chrtDataFmt == SPEED || chrtDataFmt == TEMPERATURE || chrtDataFmt == OTHER) {

View File

@@ -5,8 +5,9 @@
// These constants have to match the declaration below in :
// PageDescription registerPageAutopilot(
// {"HDM","HDT", "COG", "STW", "SOG", "DBT","XTE", "DTW", "BTW"}, // Bus values we need in the page
const int HowManyValues = 9;
// {"HDM","HDT", "COG", "STW", "SOG", "DBT","XTE", "DTW", "BTW", "RPOS", "ROT"}, // Bus values we need in the page
const int HowManyValues = 11;
const int AverageValues = 4;
@@ -19,10 +20,13 @@ const int ShowDBT = 5;
const int ShowXTE = 6;
const int ShowDTW = 7;
const int ShowBTW = 8;
const int ShowRPOS = 9;
const int ShowROT = 10;
const int Compass_X0 = 200; // X center point of compass band
const int Compass_Y0 = 220; // Y position of compass lines
const int Compass_LineLength = 22; // Length of compass lines
const int Compass_Y0 = 90; // Y position of compass lines
//const int Compass_LineLength = 22; // Length of compass lines
const int Compass_LineLength = 15; // Length of compass lines
const float Compass_LineDelta = 8.0;// Compass band: 1deg = 5 Pixels, 10deg = 50 Pixels
class PageAutopilot : public Page
@@ -38,8 +42,11 @@ class PageAutopilot : public Page
virtual void setupKeys(){
Page::setupKeys();
commonData->keydata[0].label = "CMP";
commonData->keydata[1].label = "SRC";
commonData->keydata[0].label = "-10";
commonData->keydata[1].label = "-1";
commonData->keydata[2].label = "Auto";
commonData->keydata[3].label = "+1";
commonData->keydata[4].label = "+10";
}
virtual int handleKey(int key){
@@ -69,8 +76,8 @@ class PageAutopilot : public Page
GwLog *logger = commonData->logger;
// Old values for hold function
static String OldDataText[HowManyValues] = {"", "", "","", "", "","", "", ""};
static String OldDataUnits[HowManyValues] = {"", "", "","", "", "","", "", ""};
static String OldDataText[HowManyValues] = {"", "", "", "", "", "","", "", "", "", ""};
static String OldDataUnits[HowManyValues] = {"", "", "", "", "", "","", "", "", "", ""};
// Get config data
String lengthformat = config->getString(config->lengthFormat);
@@ -106,15 +113,13 @@ class PageAutopilot : public Page
setBlinkingLED(false);
setFlashLED(false);
}
if (bvalue == NULL) return PAGE_OK; // WTF why this statement?
//***********************************************************
// Set display in partial refresh mode
getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update
getdisplay().setTextColor(commonData->fgcolor);
/*
// Horizontal line 2 pix top & bottom
// Print data on top half
getdisplay().fillRect(0, 130, 400, 2, commonData->fgcolor);
@@ -138,7 +143,7 @@ class PageAutopilot : public Page
OldDataText[WhichDataDisplay] = DataText[WhichDataDisplay]; // Save the old value
OldDataUnits[WhichDataDisplay] = DataUnits[WhichDataDisplay]; // Save the old unit
}
*/
// Now draw compass band
// Get the data
double TheAngle = DataValue[WhichDataCompass];
@@ -152,13 +157,13 @@ class PageAutopilot : public Page
buffer[0]=0;
getdisplay().setFont(&Ubuntu_Bold16pt8b);
getdisplay().setCursor(10, Compass_Y0-60);
getdisplay().setCursor(10, Compass_Y0-40);
getdisplay().print(DataName[WhichDataCompass]); // Page name
// Draw compass base line and pointer
getdisplay().fillRect(0, Compass_Y0, 400, 3, commonData->fgcolor);
getdisplay().fillTriangle(Compass_X0,Compass_Y0-40,Compass_X0-10,Compass_Y0-80,Compass_X0+10,Compass_Y0-80,commonData->fgcolor);
//getdisplay().fillTriangle(Compass_X0,Compass_Y0-40,Compass_X0-10,Compass_Y0-80,Compass_X0+10,Compass_Y0-80,commonData->fgcolor);
getdisplay().fillTriangle(Compass_X0,Compass_Y0-30,Compass_X0-10,Compass_Y0-60,Compass_X0+10,Compass_Y0-60,commonData->fgcolor);
// Draw trendlines
for ( int i = 1; i < abs(TheTrend) / 2; i++){
int x1;
@@ -238,6 +243,8 @@ class PageAutopilot : public Page
// if ( x_test > 390)
// x_test = 320;
displayRudderPosition(DataValue[ShowSOG], 20, 200, 160, commonData->fgcolor, commonData->bgcolor);
return PAGE_UPDATE;
};
@@ -256,7 +263,7 @@ PageDescription registerPageAutopilot(
"Autopilot", // Page name
createPage, // Action
0, // Number of bus values depends on selection in Web configuration
{"HDM","HDT", "COG", "STW", "SOG", "DBT","XTE", "DTW", "BTW"}, // Bus values we need in the page
{"HDM","HDT", "COG", "STW", "SOG", "DBT","XTE", "DTW", "BTW", "RPOS", "ROT"}, // Bus values we need in the page
true // Show display header on/off
);

File diff suppressed because it is too large Load Diff

View File

@@ -162,9 +162,13 @@ public:
constexpr int ZOOM_KEY = 1;
#endif
if (dataHstryBuf) { // show "Mode" key only if chart supported boat data type is available
if (dataHstryBuf) { // show "Mode" key only if chart-supported boat data type is available
commonData->keydata[0].label = "MODE";
commonData->keydata[ZOOM_KEY].label = "ZOOM";
if (pageMode != VALUE) { // show "ZOOM" key only if chart is visible
commonData->keydata[ZOOM_KEY].label = "ZOOM";
} else {
commonData->keydata[ZOOM_KEY].label = "";
}
} else {
commonData->keydata[0].label = "";
commonData->keydata[ZOOM_KEY].label = "";
@@ -189,14 +193,15 @@ public:
pageMode = VALUE;
break;
}
setupKeys(); // Adjust key definition depending on <pageMode> and chart-supported boat data type
return 0; // Commit the key
}
// Set time frame to show for history chart
// Set time frame to show for chart
#if defined BOARD_OBP60S3
if (key == 5) {
if (key == 5 && pageMode != VALUE) {
#elif defined BOARD_OBP40S3
if (key == 2) {
if (key == 2 && pageMode != VALUE) {
#endif
if (dataIntv == 1) {
dataIntv = 2;
@@ -247,7 +252,7 @@ public:
}
}
setupKeys(); // adjust <mode> key depending on chart supported boat data type
setupKeys(); // Adjust key definition depending on <pageMode> and chart-supported boat data type
}
int displayPage(PageData& pageData)

View File

@@ -15,6 +15,7 @@
#include "images/logo64.xbm"
#include <esp32/clk.h>
#include "qrcode.h"
#include <vector>
#ifdef BOARD_OBP40S3
#include "dirent.h"
@@ -37,9 +38,11 @@ private:
String buzzer_mode;
uint8_t buzzer_power;
String cpuspeed;
String powermode;
String rtc_module;
String gps_module;
String env_module;
String flashLED;
String batt_sensor;
String solar_sensor;
@@ -48,15 +51,445 @@ private:
double homelat;
double homelon;
char mode = 'N'; // (N)ormal, (S)ettings, (D)evice list, (C)ard
char mode = 'N'; // (N)ormal, (S)ettings, (C)onfiguration, (D)evice list, c(A)rd
#ifdef PATCH_N2K
struct device {
uint64_t NAME;
uint8_t id;
char hex_name[17];
uint16_t manuf_code;
const char *model;
};
std::vector<device> devicelist;
#endif
void incMode() {
if (mode == 'N') { // Normal
mode = 'S';
} else if (mode == 'S') { // Settings
mode = 'C';
} else if (mode == 'C') { // Config
mode = 'D';
} else if (mode == 'D') { // Device list
if (use_sdcard) {
mode = 'A'; // SD-Card
} else {
mode = 'N';
}
} else {
mode = 'N';
}
}
void decMode() {
if (mode == 'N') {
if (use_sdcard) {
mode = 'A'; // SD-Card
} else {
mode = 'D'; // Device list
}
} else if (mode == 'S') { // Settings
mode = 'N';
} else if (mode == 'C') { // Config
mode = 'S';
} else if (mode == 'D') { // Device list
mode = 'C';
} else {
mode = 'D';
}
}
void displayModeNormal() {
// Default system page view
uint16_t y0 = 155;
getdisplay().setFont(&Ubuntu_Bold12pt8b);
getdisplay().setCursor(8, 48);
getdisplay().print("System Information");
getdisplay().drawXBitmap(320, 25, logo64_bits, logo64_width, logo64_height, commonData->fgcolor);
getdisplay().setFont(&Ubuntu_Bold8pt8b);
char ssid[13];
snprintf(ssid, 13, "%04X%08X", (uint16_t)(chipid >> 32), (uint32_t)chipid);
displayBarcode(String(ssid), 320, 200, 2);
getdisplay().setCursor(8, 70);
getdisplay().print(String("MCUDEVICE-") + String(ssid));
getdisplay().setCursor(8, 95);
getdisplay().print("Firmware version: ");
getdisplay().setCursor(150, 95);
getdisplay().print(VERSINFO);
getdisplay().setCursor(8, 113);
getdisplay().print("Board version: ");
getdisplay().setCursor(150, 113);
getdisplay().print(BOARDINFO);
getdisplay().print(String(" HW ") + String(PCBINFO));
getdisplay().setCursor(8, 131);
getdisplay().print("Display version: ");
getdisplay().setCursor(150, 131);
getdisplay().print(DISPLAYINFO);
getdisplay().print("; GxEPD2 v");
getdisplay().print(GXEPD2INFO);
getdisplay().setCursor(8, 265);
#ifdef BOARD_OBP60S3
getdisplay().print("Press STBY to enter deep sleep mode");
#endif
#ifdef BOARD_OBP40S3
getdisplay().print("Press wheel to enter deep sleep mode");
#endif
// Flash memory size
uint32_t flash_size = ESP.getFlashChipSize();
getdisplay().setCursor(8, y0);
getdisplay().print("FLASH:");
getdisplay().setCursor(90, y0);
getdisplay().print(String(flash_size / 1024) + String(" kB"));
// PSRAM memory size
uint32_t psram_size = ESP.getPsramSize();
getdisplay().setCursor(8, y0 + 16);
getdisplay().print("PSRAM:");
getdisplay().setCursor(90, y0 + 16);
getdisplay().print(String(psram_size / 1024) + String(" kB"));
// FRAM available / status
getdisplay().setCursor(8, y0 + 32);
getdisplay().print("FRAM:");
getdisplay().setCursor(90, y0 + 32);
getdisplay().print(hasFRAM ? "available" : "not found");
#ifdef BOARD_OBP40S3
// SD-Card
getdisplay().setCursor(8, y0 + 48);
getdisplay().print("SD-Card:");
getdisplay().setCursor(90, y0 + 48);
if (hasSDCard) {
uint64_t cardsize = ((uint64_t) sdcard->csd.capacity) * sdcard->csd.sector_size / (1024 * 1024);
getdisplay().printf("%llu MB", cardsize);
} else {
getdisplay().print("off");
}
#endif
// Uptime
int64_t uptime = esp_timer_get_time() / 1000000;
String uptime_unit;
if (uptime < 120) {
uptime_unit = " seconds";
} else {
if (uptime < 2 * 3600) {
uptime /= 60;
uptime_unit = " minutes";
} else if (uptime < 2 * 3600 * 24) {
uptime /= 3600;
uptime_unit = " hours";
} else {
uptime /= 86400;
uptime_unit = " days";
}
}
getdisplay().setCursor(8, y0 + 80);
getdisplay().print("Uptime:");
getdisplay().setCursor(90, y0 + 80);
getdisplay().print(uptime);
getdisplay().print(uptime_unit);
// CPU speed config / active
getdisplay().setCursor(202, y0);
getdisplay().print("CPU speed:");
getdisplay().setCursor(300, y0);
getdisplay().print(cpuspeed);
getdisplay().print(" / ");
int cpu_freq = esp_clk_cpu_freq() / 1000000;
getdisplay().print(String(cpu_freq));
// total RAM free
int Heap_free = esp_get_free_heap_size();
getdisplay().setCursor(202, y0 + 16);
getdisplay().print("Total free:");
getdisplay().setCursor(300, y0 + 16);
getdisplay().print(String(Heap_free));
// RAM free for task
int RAM_free = uxTaskGetStackHighWaterMark(NULL);
getdisplay().setCursor(202, y0 + 32);
getdisplay().print("Task free:");
getdisplay().setCursor(300, y0 + 32);
getdisplay().print(String(RAM_free));
}
void displayModeConfig() {
// Configuration interface
uint16_t x0 = 16;
uint16_t y0 = 80;
uint16_t dy = 20;
getdisplay().setFont(&Ubuntu_Bold12pt8b);
getdisplay().setCursor(8, 48);
getdisplay().print("System configuration");
getdisplay().setFont(&Ubuntu_Bold8pt8b);
getdisplay().setCursor(x0, y0);
getdisplay().print("CPU speed: 80 | 160 | 240");
getdisplay().setCursor(x0, y0 + 1 * dy);
getdisplay().print("Power mode: Max | 5V | Min");
getdisplay().setCursor(x0, y0 + 2 * dy);
getdisplay().print("Accesspoint: On | Off");
// TODO Change NVRAM-preferences settings here
getdisplay().setCursor(x0, y0 + 4 * dy);
getdisplay().print("Simulation: On | Off");
}
void displayModeSettings() {
// View some of the current settings
const uint16_t x0 = 8;
const uint16_t y0 = 72;
getdisplay().setFont(&Ubuntu_Bold12pt8b);
getdisplay().setCursor(x0, 48);
getdisplay().print("System settings");
getdisplay().setFont(&Ubuntu_Bold8pt8b);
// left column
getdisplay().setCursor(x0, y0);
getdisplay().print("Simulation:");
getdisplay().setCursor(120, y0);
getdisplay().print(simulation ? "on" : "off");
getdisplay().setCursor(x0, y0 + 16);
getdisplay().print("Environment:");
getdisplay().setCursor(120, y0 + 16);
getdisplay().print(env_module);
getdisplay().setCursor(x0, y0 + 32);
getdisplay().print("Buzzer:");
getdisplay().setCursor(120, y0 + 32);
getdisplay().print(buzzer_mode);
getdisplay().setCursor(x0, y0 + 64);
getdisplay().print("GPS:");
getdisplay().setCursor(120, y0 + 64);
getdisplay().print(gps_module);
getdisplay().setCursor(x0, y0 + 80);
getdisplay().print("RTC:");
getdisplay().setCursor(120, y0 + 80);
getdisplay().print(rtc_module);
getdisplay().setCursor(x0, y0 + 96);
getdisplay().print("Wifi:");
getdisplay().setCursor(120, y0 + 96);
getdisplay().print(commonData->status.wifiApOn ? "on" : "off");
// Home location
getdisplay().setCursor(x0, y0 + 128);
getdisplay().print("Home Lat.:");
getdisplay().setCursor(120, y0 + 128);
getdisplay().print(formatLatitude(homelat));
getdisplay().setCursor(x0, y0 + 144);
getdisplay().print("Home Lon.:");
getdisplay().setCursor(120, y0 + 144);
getdisplay().print(formatLongitude(homelon));
// Power
getdisplay().setCursor(x0, y0 + 176);
getdisplay().print("Power mode:");
getdisplay().setCursor(120, y0 + 176);
getdisplay().print(powermode);
// right column
getdisplay().setCursor(202, y0);
getdisplay().print("Batt. sensor:");
getdisplay().setCursor(320, y0);
getdisplay().print(batt_sensor);
// Solar sensor
getdisplay().setCursor(202, y0 + 16);
getdisplay().print("Solar sensor:");
getdisplay().setCursor(320, y0 + 16);
getdisplay().print(solar_sensor);
// Generator sensor
getdisplay().setCursor(202, y0 + 32);
getdisplay().print("Gen. sensor:");
getdisplay().setCursor(320, y0 + 32);
getdisplay().print(gen_sensor);
// TODO
// Gyro sensor (rotation)
getdisplay().setCursor(202, y0 + 48);
getdisplay().print("Rot. sensor:");
getdisplay().setCursor(320, y0 + 48);
getdisplay().print(rot_sensor);
// Temp.-sensor
// Power Mode
#ifdef BOARD_OBP60S3
// Backlight infos
getdisplay().setCursor(202, y0 + 64);
getdisplay().print("Backlight:");
getdisplay().setCursor(320, y0 + 64);
getdisplay().printf("%d%%", commonData->backlight.brightness);
// TODO test function with OBP60 device
getdisplay().setCursor(202, y0 + 80);
getdisplay().print("Bl color:");
getdisplay().setCursor(320, y0 + 80);
getdisplay().print(commonData->backlight.color.toName());
getdisplay().setCursor(202, y0 + 96);
getdisplay().print("Bl mode:");
getdisplay().setCursor(320, y0 + 96);
getdisplay().print(commonData->backlight.mode);
// TODO Buzzer mode and power
#endif
}
void displayModeSDCard() {
// SD Card info
uint16_t x0 = 20;
uint16_t y0 = 72;
getdisplay().setFont(&Ubuntu_Bold12pt8b);
getdisplay().setCursor(8, 48);
getdisplay().print("SD Card info");
getdisplay().setFont(&Ubuntu_Bold8pt8b);
getdisplay().setCursor(x0, y0);
#ifdef BOARD_OBP60S3
// This mode should not be callable by devices without card hardware
// In case of accidential reaching this, display a friendly message
getdisplay().print("This mode is not indended to be reached!\n");
getdisplay().print("There's nothing to see here. Move on.");
#endif
#ifdef BOARD_OBP40S3
getdisplay().print("Work in progress...");
/* TODO
this code should go somewhere else. only for testing purposes here
identify card as OBP-Card:
magic.dat
version.dat
readme.txt
IMAGES/
CHARTS/
LOGS/
DATA/
hint: file access with fopen, fgets, fread, fclose
*/
// Simple test for magic file in root
getdisplay().setCursor(x0, y0 + 32);
String file_magic = MOUNT_POINT "/magic.dat";
commonData->logger->logDebug(GwLog::LOG, "Test magicfile: %s", file_magic.c_str());
struct stat st;
if (stat(file_magic.c_str(), &st) == 0) {
getdisplay().printf("File %s exists", file_magic.c_str());
} else {
getdisplay().printf("File %s not found", file_magic.c_str());
}
// Root directory check
DIR* dir = opendir(MOUNT_POINT);
int dy = 0;
if (dir != NULL) {
commonData->logger->logDebug(GwLog::LOG, "Root directory: %s", MOUNT_POINT);
struct dirent* entry;
while (((entry = readdir(dir)) != NULL) and (dy < 140)) {
getdisplay().setCursor(x0, y0 + 64 + dy);
getdisplay().print(entry->d_name);
// type 1 is file, type 2 is dir
if (entry->d_type == 2) {
getdisplay().print("/");
}
dy += 20;
commonData->logger->logDebug(GwLog::DEBUG, " %s type %d", entry->d_name, entry->d_type);
}
closedir(dir);
} else {
commonData->logger->logDebug(GwLog::LOG, "Failed to open root directory");
}
#endif
}
void displayModeDevicelist() {
// NMEA2000 device list
getdisplay().setFont(&Ubuntu_Bold12pt8b);
getdisplay().setCursor(8, 48);
getdisplay().print("NMEA2000 device list");
getdisplay().setFont(&Ubuntu_Bold8pt8b);
getdisplay().setCursor(20, 70);
getdisplay().print("RxD: ");
getdisplay().print(String(commonData->status.n2kRx));
getdisplay().setCursor(120, 70);
getdisplay().print("TxD: ");
getdisplay().print(String(commonData->status.n2kTx));
#ifdef PATCH_N2K
uint16_t x0 = 20;
uint16_t y0 = 100;
getdisplay().setFont(&Ubuntu_Bold10pt8b);
getdisplay().setCursor(x0, y0);
getdisplay().print("ID");
getdisplay().setCursor(x0 + 50, y0);
getdisplay().print("Model");
getdisplay().setCursor(x0 + 250, y0);
getdisplay().print("Manuf.");
getdisplay().drawLine(18, y0 + 4, 360 , y0 + 4 , commonData->fgcolor);
getdisplay().setFont(&Ubuntu_Bold8pt8b);
y0 = 120;
uint8_t n_dev = 0;
for (const device& item : devicelist) {
if (n_dev > 8) {
break;
}
getdisplay().setCursor(x0, y0 + n_dev * 20);
getdisplay().print(item.id);
getdisplay().setCursor(x0 + 50, y0 + n_dev * 20);
getdisplay().print(item.model);
getdisplay().setCursor(x0 + 250, y0 + n_dev * 20);
getdisplay().print(item.manuf_code);
n_dev++;
}
getdisplay().setCursor(x0, y0 + (n_dev + 1) * 20);
if (n_dev == 0) {
getdisplay().printf("no devices found on bus");
} else {
getdisplay().drawLine(18, y0 + n_dev * 20, 360 , y0 + n_dev * 20, commonData->fgcolor);
getdisplay().printf("%d devices of %d in total", n_dev, devicelist.size());
}
#else
getdisplay().setCursor(20, 100);
getdisplay().print("NMEA2000 not exposed to obp60 task");
#endif
}
public:
PageSystem(CommonData &common){
commonData = &common;
common.logger->logDebug(GwLog::LOG,"Instantiate PageSystem");
commonData->logger->logDebug(GwLog::LOG,"Instantiate PageSystem");
if (hasFRAM) {
mode = fram.read(FRAM_SYSTEM_MODE);
common.logger->logDebug(GwLog::DEBUG, "Loaded mode '%c' from FRAM", mode);
commonData->logger->logDebug(GwLog::DEBUG, "Loaded mode '%c' from FRAM", mode);
}
chipid = ESP.getEfuseMac();
simulation = common.config->getBool(common.config->useSimuData);
@@ -67,6 +500,7 @@ public:
buzzer_mode.toLowerCase();
buzzer_power = common.config->getInt(common.config->buzzerPower);
cpuspeed = common.config->getString(common.config->cpuSpeed);
powermode = common.config->getString(common.config->powerMode);
env_module = common.config->getString(common.config->useEnvSensor);
rtc_module = common.config->getString(common.config->useRTC);
gps_module = common.config->getString(common.config->useGPS);
@@ -76,6 +510,7 @@ public:
rot_sensor = common.config->getString(common.config->useRotSensor);
homelat = common.config->getString(common.config->homeLAT).toDouble();
homelon = common.config->getString(common.config->homeLON).toDouble();
flashLED = common.config->getString(common.config->flashLED);
}
void setupKeys() {
@@ -92,19 +527,7 @@ public:
// Switch display mode
commonData->logger->logDebug(GwLog::LOG, "System keyboard handler");
if (key == 2) {
if (mode == 'N') {
mode = 'S';
} else if (mode == 'S') {
mode = 'D';
} else if (mode == 'D') {
if (hasSDCard) {
mode = 'C';
} else {
mode = 'N';
}
} else {
mode = 'N';
}
incMode();
if (hasFRAM) fram.write(FRAM_SYSTEM_MODE, mode);
return 0;
}
@@ -129,8 +552,13 @@ public:
}
#endif
#ifdef BOARD_OBP40S3
// grab cursor keys to disable page navigation
if (key == 9 or key == 10) {
// use cursor keys for local mode navigation
if (key == 9) {
incMode();
return 0;
}
if (key == 10) {
decMode();
return 0;
}
// standby / deep sleep
@@ -168,305 +596,64 @@ public:
}
}
int displayPage(PageData &pageData){
GwConfigHandler *config = commonData->config;
GwLog *logger = commonData->logger;
// Get config data
String flashLED = config->getString(config->flashLED);
// Optical warning by limit violation (unused)
if(String(flashLED) == "Limit Violation"){
void displayNew(PageData &pageData) {
#ifdef BOARD_OBP60S3
// Clear optical warning
if (flashLED == "Limit Violation") {
setBlinkingLED(false);
setFlashLED(false);
setFlashLED(false);
}
#endif
// Logging boat values
logger->logDebug(GwLog::LOG, "Drawing at PageSystem, Mode=%c", mode);
#ifdef PATCH_N2K
// load current device list
tN2kDeviceList *pDevList = pageData.api->getN2kDeviceList();
// TODO check if changed
if (pDevList->ReadResetIsListUpdated()) {
// only reload if changed
devicelist.clear();
for (uint8_t i = 0; i <= 252; i++) {
const tNMEA2000::tDevice *d = pDevList->FindDeviceBySource(i);
if (d == nullptr) {
continue;
}
device dev;
dev.id = i;
dev.NAME = d->GetName();
snprintf(dev.hex_name, sizeof(dev.hex_name), "%08X%08X", (uint32_t)(dev.NAME >> 32), (uint32_t)(dev.NAME & 0xFFFFFFFF));
dev.manuf_code = d->GetManufacturerCode();
dev.model = d->GetModelID();
devicelist.push_back(dev);
}
}
#endif
};
// Draw page
//***********************************************************
int displayPage(PageData &pageData){
uint16_t x0 = 8; // left column
uint16_t y0 = 48; // data table starts here
// Logging page information
commonData->logger->logDebug(GwLog::LOG, "Drawing at PageSystem, Mode=%c", mode);
// Set display in partial refresh mode
getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update
if (mode == 'N') {
getdisplay().setFont(&Ubuntu_Bold12pt8b);
getdisplay().setCursor(8, 48);
getdisplay().print("System Information");
getdisplay().drawXBitmap(320, 25, logo64_bits, logo64_width, logo64_height, commonData->fgcolor);
getdisplay().setFont(&Ubuntu_Bold8pt8b);
y0 = 155;
char ssid[13];
snprintf(ssid, 13, "%04X%08X", (uint16_t)(chipid >> 32), (uint32_t)chipid);
displayBarcode(String(ssid), 320, 200, 2);
getdisplay().setCursor(8, 70);
getdisplay().print(String("MCUDEVICE-") + String(ssid));
getdisplay().setCursor(8, 95);
getdisplay().print("Firmware version: ");
getdisplay().setCursor(150, 95);
getdisplay().print(VERSINFO);
getdisplay().setCursor(8, 113);
getdisplay().print("Board version: ");
getdisplay().setCursor(150, 113);
getdisplay().print(BOARDINFO);
getdisplay().print(String(" HW ") + String(PCBINFO));
getdisplay().setCursor(8, 131);
getdisplay().print("Display version: ");
getdisplay().setCursor(150, 131);
getdisplay().print(DISPLAYINFO);
getdisplay().print("; GxEPD2 v");
getdisplay().print(GXEPD2INFO);
getdisplay().setCursor(8, 265);
#ifdef BOARD_OBP60S3
getdisplay().print("Press STBY to enter deep sleep mode");
#endif
#ifdef BOARD_OBP40S3
getdisplay().print("Press wheel to enter deep sleep mode");
#endif
// Flash memory size
uint32_t flash_size = ESP.getFlashChipSize();
getdisplay().setCursor(8, y0);
getdisplay().print("FLASH:");
getdisplay().setCursor(90, y0);
getdisplay().print(String(flash_size / 1024) + String(" kB"));
// PSRAM memory size
uint32_t psram_size = ESP.getPsramSize();
getdisplay().setCursor(8, y0 + 16);
getdisplay().print("PSRAM:");
getdisplay().setCursor(90, y0 + 16);
getdisplay().print(String(psram_size / 1024) + String(" kB"));
// FRAM available / status
getdisplay().setCursor(8, y0 + 32);
getdisplay().print("FRAM:");
getdisplay().setCursor(90, y0 + 32);
getdisplay().print(hasFRAM ? "available" : "not found");
#ifdef BOARD_OBP40S3
// SD-Card
getdisplay().setCursor(8, y0 + 48);
getdisplay().print("SD-Card:");
getdisplay().setCursor(90, y0 + 48);
if (hasSDCard) {
uint64_t cardsize = ((uint64_t) sdcard->csd.capacity) * sdcard->csd.sector_size / (1024 * 1024);
getdisplay().printf("%llu MB", cardsize);
} else {
getdisplay().print("off");
}
#endif
// Uptime
int64_t uptime = esp_timer_get_time() / 1000000;
String uptime_unit;
if (uptime < 120) {
uptime_unit = " seconds";
} else {
if (uptime < 2 * 3600) {
uptime /= 60;
uptime_unit = " minutes";
} else if (uptime < 2 * 3600 * 24) {
uptime /= 3600;
uptime_unit = " hours";
} else {
uptime /= 86400;
uptime_unit = " days";
}
}
getdisplay().setCursor(8, y0 + 80);
getdisplay().print("Uptime:");
getdisplay().setCursor(90, y0 + 80);
getdisplay().print(uptime);
getdisplay().print(uptime_unit);
// CPU speed config / active
getdisplay().setCursor(202, y0);
getdisplay().print("CPU speed:");
getdisplay().setCursor(300, y0);
getdisplay().print(cpuspeed);
getdisplay().print(" / ");
int cpu_freq = esp_clk_cpu_freq() / 1000000;
getdisplay().print(String(cpu_freq));
// total RAM free
int Heap_free = esp_get_free_heap_size();
getdisplay().setCursor(202, y0 + 16);
getdisplay().print("Total free:");
getdisplay().setCursor(300, y0 + 16);
getdisplay().print(String(Heap_free));
// RAM free for task
int RAM_free = uxTaskGetStackHighWaterMark(NULL);
getdisplay().setCursor(202, y0 + 32);
getdisplay().print("Task free:");
getdisplay().setCursor(300, y0 + 32);
getdisplay().print(String(RAM_free));
} else if (mode == 'S') {
// Settings
getdisplay().setFont(&Ubuntu_Bold12pt8b);
getdisplay().setCursor(x0, 48);
getdisplay().print("System settings");
getdisplay().setFont(&Ubuntu_Bold8pt8b);
x0 = 8;
y0 = 72;
// left column
getdisplay().setCursor(x0, y0);
getdisplay().print("Simulation:");
getdisplay().setCursor(120, y0);
getdisplay().print(simulation ? "on" : "off");
getdisplay().setCursor(x0, y0 + 16);
getdisplay().print("Environment:");
getdisplay().setCursor(120, y0 + 16);
getdisplay().print(env_module);
getdisplay().setCursor(x0, y0 + 32);
getdisplay().print("Buzzer:");
getdisplay().setCursor(120, y0 + 32);
getdisplay().print(buzzer_mode);
getdisplay().setCursor(x0, y0 + 64);
getdisplay().print("GPS:");
getdisplay().setCursor(120, y0 + 64);
getdisplay().print(gps_module);
getdisplay().setCursor(x0, y0 + 80);
getdisplay().print("RTC:");
getdisplay().setCursor(120, y0 + 80);
getdisplay().print(rtc_module);
getdisplay().setCursor(x0, y0 + 96);
getdisplay().print("Wifi:");
getdisplay().setCursor(120, y0 + 96);
getdisplay().print(commonData->status.wifiApOn ? "on" : "off");
// Home location
getdisplay().setCursor(x0, y0 + 128);
getdisplay().print("Home Lat.:");
getdisplay().setCursor(120, y0 + 128);
getdisplay().print(formatLatitude(homelat));
getdisplay().setCursor(x0, y0 + 144);
getdisplay().print("Home Lon.:");
getdisplay().setCursor(120, y0 + 144);
getdisplay().print(formatLongitude(homelon));
// right column
getdisplay().setCursor(202, y0);
getdisplay().print("Batt. sensor:");
getdisplay().setCursor(320, y0);
getdisplay().print(batt_sensor);
// Solar sensor
getdisplay().setCursor(202, y0 + 16);
getdisplay().print("Solar sensor:");
getdisplay().setCursor(320, y0 + 16);
getdisplay().print(solar_sensor);
// Generator sensor
getdisplay().setCursor(202, y0 + 32);
getdisplay().print("Gen. sensor:");
getdisplay().setCursor(320, y0 + 32);
getdisplay().print(gen_sensor);
// Gyro sensor
} else if (mode == 'C') {
// Card info
getdisplay().setFont(&Ubuntu_Bold12pt8b);
getdisplay().setCursor(8, 48);
getdisplay().print("SD Card info");
getdisplay().setFont(&Ubuntu_Bold8pt8b);
x0 = 20;
y0 = 72;
getdisplay().setCursor(x0, y0);
#ifdef BOARD_OBP60S3
// This mode should not be callable by devices without card hardware
// In case of accidential reaching this, display a friendly message
getdisplay().print("This mode is not indended to be reached!\n");
getdisplay().print("There's nothing to see here. Move on.");
#endif
#ifdef BOARD_OBP40S3
getdisplay().print("Work in progress...");
/* TODO
this code should go somewhere else. only for testing purposes here
identify card as OBP-Card:
magic.dat
version.dat
readme.txt
IMAGES/
CHARTS/
LOGS/
DATA/
hint: file access with fopen, fgets, fread, fclose
*/
// Simple test for magic file in root
getdisplay().setCursor(x0, y0 + 32);
String file_magic = MOUNT_POINT "/magic.dat";
logger->logDebug(GwLog::LOG, "Test magicfile: %s", file_magic.c_str());
struct stat st;
if (stat(file_magic.c_str(), &st) == 0) {
getdisplay().printf("File %s exists", file_magic.c_str());
} else {
getdisplay().printf("File %s not found", file_magic.c_str());
}
// Root directory check
DIR* dir = opendir(MOUNT_POINT);
int dy = 0;
if (dir != NULL) {
logger->logDebug(GwLog::LOG, "Root directory: %s", MOUNT_POINT);
struct dirent* entry;
while (((entry = readdir(dir)) != NULL) and (dy < 140)) {
getdisplay().setCursor(x0, y0 + 64 + dy);
getdisplay().print(entry->d_name);
// type 1 is file, type 2 is dir
if (entry->d_type == 2) {
getdisplay().print("/");
}
dy += 20;
logger->logDebug(GwLog::DEBUG, " %s type %d", entry->d_name, entry->d_type);
}
closedir(dir);
} else {
logger->logDebug(GwLog::LOG, "Failed to open root directory");
}
#endif
} else {
// NMEA2000 device list
getdisplay().setFont(&Ubuntu_Bold12pt8b);
getdisplay().setCursor(8, 48);
getdisplay().print("NMEA2000 device list");
getdisplay().setFont(&Ubuntu_Bold8pt8b);
getdisplay().setCursor(20, 80);
getdisplay().print("RxD: ");
getdisplay().print(String(commonData->status.n2kRx));
getdisplay().setCursor(20, 100);
getdisplay().print("TxD: ");
getdisplay().print(String(commonData->status.n2kTx));
// call current system page
switch (mode) {
case 'N':
displayModeNormal();
break;
case 'S':
displayModeSettings();
break;
case 'C':
displayModeConfig();
break;
case 'A':
displayModeSDCard();
break;
case 'D':
displayModeDevicelist();
break;
}
// Update display

View File

@@ -114,7 +114,7 @@ private:
}
if (numValues == 2 && mode == FULL) { // print line only, if we want to show 2 data values
getdisplay().fillRect(0, 145, width, 3, commonData->fgcolor); // Horizontal line 3 pix
getdisplay().fillRect(0, 145, width, 3, commonData->fgcolor); // Horizontal line 3 pix
}
}
@@ -149,7 +149,11 @@ public:
if (dataHstryBuf[0] || dataHstryBuf[1]) { // show "Mode" key only if at least 1 chart supported boat data type is available
commonData->keydata[0].label = "MODE";
commonData->keydata[ZOOM_KEY].label = "ZOOM";
if (pageMode != VALUES) { // show "ZOOM" key only if chart is visible
commonData->keydata[ZOOM_KEY].label = "ZOOM";
} else {
commonData->keydata[ZOOM_KEY].label = "";
}
} else {
commonData->keydata[0].label = "";
commonData->keydata[ZOOM_KEY].label = "";
@@ -191,14 +195,15 @@ public:
pageMode = VALUES;
break;
}
setupKeys(); // Adjust key definition depending on <pageMode> and chart-supported boat data type
return 0; // Commit the key
}
// Set time frame to show for history chart
// Set time frame to show for chart
#if defined BOARD_OBP60S3
if (key == 5) {
if (key == 5 && pageMode != VALUES) {
#elif defined BOARD_OBP40S3
if (key == 2) {
if (key == 2 && pageMode != VALUES) {
#endif
if (dataIntv == 1) {
dataIntv = 2;
@@ -251,7 +256,7 @@ public:
}
}
setupKeys(); // adjust <mode> key depending on chart supported boat data type
setupKeys(); // Adjust key definition depending on <pageMode> and chart-supported boat data type
}
int displayPage(PageData& pageData)
@@ -285,13 +290,13 @@ public:
showData(bValue, FULL);
} else if (pageMode == VAL1_CHART) { // show data value 1 and chart
showData({bValue[0]}, HALF);
showData({ bValue[0] }, HALF);
if (dataChart[0]) {
dataChart[0]->showChrt(HORIZONTAL, HALF_SIZE_BOTTOM, dataIntv, NO_PRNT_NAME, NO_PRNT_VALUE, *bValue[0]);
}
} else if (pageMode == VAL2_CHART) { // show data value 2 and chart
showData({bValue[1]}, HALF);
showData({ bValue[1] }, HALF);
if (dataChart[1]) {
dataChart[1]->showChrt(HORIZONTAL, HALF_SIZE_BOTTOM, dataIntv, NO_PRNT_NAME, NO_PRNT_VALUE, *bValue[1]);
}

View File

@@ -196,7 +196,7 @@ public:
int displayPage(PageData& pageData)
{
LOG_DEBUG(GwLog::LOG, "Display PageWindPlot");
ulong pageTime = millis();
// ulong pageTime = millis();
if (showTruW != oldShowTruW) {
@@ -243,7 +243,7 @@ public:
}
}
LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: page time %ldms", millis() - pageTime);
// LOG_DEBUG(GwLog::DEBUG, "PageWindPlot: page time %ldms", millis() - pageTime);
return PAGE_UPDATE;
}
};

View File

@@ -51,7 +51,7 @@ typedef struct{
double rotationAngle = 0; // Rotation angle in radiant
bool validRotAngle = false; // Valid flag magnet present for rotation sensor
struct tm rtcTime; // UTC time from internal RTC
bool rtcValid = false;
bool rtcValid = false; // Internal RTC chip
int sunsetHour = 0;
int sunsetMinute = 0;
int sunriseHour = 0;

View File

@@ -1245,8 +1245,8 @@
"name": "timeSource",
"label": "Status Time Source",
"type": "list",
"default": "GPS",
"description": "Data source for date and time display in status line [RTC|iRTC|GPS]",
"default": "iRTC",
"description": "Data source for date and time display in status line [iRTC|RTC|GPS]",
"list": [
{"l":"Internal real time clock (iRTC)","v":"iRTC"},
{"l":"External real time clock (RTC)","v":"RTC"},
@@ -1517,6 +1517,7 @@
"default": "Voltage",
"description": "Type of page for page 1",
"list": [
"Autopilot",
"BME280",
"Battery",
"Battery2",
@@ -1818,7 +1819,7 @@
"description": "Wind source for page 1: [true|apparent]",
"list": [
"True wind",
"apparent wind"
"Apparent wind"
],
"category": "OBP40 Page 1",
"capabilities": {
@@ -1847,6 +1848,7 @@
"default": "WindRose",
"description": "Type of page for page 2",
"list": [
"Autopilot",
"BME280",
"Battery",
"Battery2",
@@ -2140,7 +2142,7 @@
"description": "Wind source for page 2: [true|apparent]",
"list": [
"True wind",
"apparent wind"
"Apparent wind"
],
"category": "OBP40 Page 2",
"capabilities": {
@@ -2168,6 +2170,7 @@
"default": "OneValue",
"description": "Type of page for page 3",
"list": [
"Autopilot",
"BME280",
"Battery",
"Battery2",
@@ -2453,7 +2456,7 @@
"description": "Wind source for page 3: [true|apparent]",
"list": [
"True wind",
"apparent wind"
"Apparent wind"
],
"category": "OBP40 Page 3",
"capabilities": {
@@ -2480,6 +2483,7 @@
"default": "TwoValues",
"description": "Type of page for page 4",
"list": [
"Autopilot",
"BME280",
"Battery",
"Battery2",
@@ -2757,7 +2761,7 @@
"description": "Wind source for page 4: [true|apparent]",
"list": [
"True wind",
"apparent wind"
"Apparent wind"
],
"category": "OBP40 Page 4",
"capabilities": {
@@ -2783,6 +2787,7 @@
"default": "ThreeValues",
"description": "Type of page for page 5",
"list": [
"Autopilot",
"BME280",
"Battery",
"Battery2",
@@ -3052,7 +3057,7 @@
"description": "Wind source for page 5: [true|apparent]",
"list": [
"True wind",
"apparent wind"
"Apparent wind"
],
"category": "OBP40 Page 5",
"capabilities": {
@@ -3077,6 +3082,7 @@
"default": "FourValues",
"description": "Type of page for page 6",
"list": [
"Autopilot",
"BME280",
"Battery",
"Battery2",
@@ -3338,7 +3344,7 @@
"description": "Wind source for page 6: [true|apparent]",
"list": [
"True wind",
"apparent wind"
"Apparent wind"
],
"category": "OBP40 Page 6",
"capabilities": {
@@ -3362,6 +3368,7 @@
"default": "FourValues2",
"description": "Type of page for page 7",
"list": [
"Autopilot",
"BME280",
"Battery",
"Battery2",
@@ -3615,7 +3622,7 @@
"description": "Wind source for page 7: [true|apparent]",
"list": [
"True wind",
"apparent wind"
"Apparent wind"
],
"category": "OBP40 Page 7",
"capabilities": {
@@ -3638,6 +3645,7 @@
"default": "Clock",
"description": "Type of page for page 8",
"list": [
"Autopilot",
"BME280",
"Battery",
"Battery2",
@@ -3883,7 +3891,7 @@
"description": "Wind source for page 8: [true|apparent]",
"list": [
"True wind",
"apparent wind"
"Apparent wind"
],
"category": "OBP40 Page 8",
"capabilities": {
@@ -3905,6 +3913,7 @@
"default": "RollPitch",
"description": "Type of page for page 9",
"list": [
"Autopilot",
"BME280",
"Battery",
"Battery2",
@@ -4142,7 +4151,7 @@
"description": "Wind source for page 9: [true|apparent]",
"list": [
"True wind",
"apparent wind"
"Apparent wind"
],
"category": "OBP40 Page 9",
"capabilities": {
@@ -4163,6 +4172,7 @@
"default": "Battery2",
"description": "Type of page for page 10",
"list": [
"Autopilot",
"BME280",
"Battery",
"Battery2",
@@ -4392,7 +4402,7 @@
"description": "Wind source for page 10: [true|apparent]",
"list": [
"True wind",
"apparent wind"
"Apparent wind"
],
"category": "OBP40 Page 10",
"capabilities": {

View File

@@ -37,7 +37,7 @@
"name": "homeLAT",
"label": "Home latitude",
"type": "number",
"default": "",
"default": "0.00000",
"check": "checkMinMax",
"min": -90.0,
"max": 90.0,
@@ -51,7 +51,7 @@
"name": "homeLON",
"label": "Home longitude",
"type": "number",
"default": "",
"default": "0.00000",
"check": "checkMinMax",
"min": -180.0,
"max": 180.0,
@@ -1235,8 +1235,9 @@
"label": "Status Time Source",
"type": "list",
"default": "GPS",
"description": "Data source for date and time display in status line [RTC|GPS]",
"description": "Data source for date and time display in status line [iRTC|RTC|GPS]",
"list": [
{"l":"Internal real time clock (iRTC)","v":"iRTC"},
{"l":"Real time clock (RTC)","v":"RTC"},
{"l":"Time via bus (GPS)","v":"GPS"}
],
@@ -1494,6 +1495,7 @@
"default": "Voltage",
"description": "Type of page for page 1",
"list": [
"Autopilot",
"BME280",
"Battery",
"Battery2",
@@ -1794,6 +1796,7 @@
"default": "WindRose",
"description": "Type of page for page 2",
"list": [
"Autopilot",
"BME280",
"Battery",
"Battery2",
@@ -2086,6 +2089,7 @@
"default": "OneValue",
"description": "Type of page for page 3",
"list": [
"Autopilot",
"BME280",
"Battery",
"Battery2",
@@ -2370,6 +2374,7 @@
"default": "TwoValues",
"description": "Type of page for page 4",
"list": [
"Autopilot",
"BME280",
"Battery",
"Battery2",
@@ -2646,6 +2651,7 @@
"default": "ThreeValues",
"description": "Type of page for page 5",
"list": [
"Autopilot",
"BME280",
"Battery",
"Battery2",
@@ -2914,6 +2920,7 @@
"default": "FourValues",
"description": "Type of page for page 6",
"list": [
"Autopilot",
"BME280",
"Battery",
"Battery2",
@@ -3174,6 +3181,7 @@
"default": "FourValues2",
"description": "Type of page for page 7",
"list": [
"Autopilot",
"BME280",
"Battery",
"Battery2",
@@ -3426,6 +3434,7 @@
"default": "Clock",
"description": "Type of page for page 8",
"list": [
"Autopilot",
"BME280",
"Battery",
"Battery2",
@@ -3670,6 +3679,7 @@
"default": "RollPitch",
"description": "Type of page for page 9",
"list": [
"Autopilot",
"BME280",
"Battery",
"Battery2",
@@ -3906,6 +3916,7 @@
"default": "Battery2",
"description": "Type of page for page 10",
"list": [
"Autopilot",
"BME280",
"Battery",
"Battery2",

View File

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

View File

@@ -332,7 +332,7 @@ void OBP60Task(GwApi *api){
// return;
GwLog *logger=api->getLogger();
GwConfigHandler *config=api->getConfig();
#ifdef HARDWARE_V21
#if defined HARDWARE_V20 || HARDWARE_V21
startLedTask(api);
#endif
PageList allPages;
@@ -341,7 +341,7 @@ void OBP60Task(GwApi *api){
commonData.logger=logger;
commonData.config=config;
#ifdef HARDWARE_V21
#if defined HARDWARE_V20 || HARDWARE_V21
// Keyboard coordinates for page footer
initKeys(commonData);
#endif
@@ -432,7 +432,7 @@ void OBP60Task(GwApi *api){
#endif
LOG_DEBUG(GwLog::LOG,"...done");
int lastPage=-1; // initialize with an impiossible value, so we can detect wether we are during startup and no page has been displayed yet
int lastPage=-1; // initialize with an impossible value, so we can detect wether we are during startup and no page has been displayed yet
BoatValueList boatValues; //all the boat values for the api query
HstryBuffers hstryBufferList(1920, &boatValues, logger); // Create empty list of boat data history buffers (1.920 values = seconds = 32 min.)
@@ -729,7 +729,7 @@ void OBP60Task(GwApi *api){
else{
getdisplay().fillScreen(commonData.fgcolor); // Clear display
#ifdef DISPLAY_GDEY042T81
getdisplay().hibernate(); // Set display in hybenate mode
getdisplay().hibernate(); // Set display in hibenate mode
getdisplay().init(115200, true, 2, false); // Init for Waveshare boards with "clever" reset circuit, 2ms reset pulse
#else
getdisplay().init(115200); // Init for normal displays
@@ -757,7 +757,7 @@ void OBP60Task(GwApi *api){
else{
getdisplay().fillScreen(commonData.fgcolor); // Clear display
#ifdef DISPLAY_GDEY042T81
getdisplay().hibernate(); // Set display in hybenate mode
getdisplay().hibernate(); // Set display in hibernate mode
getdisplay().init(115200, true, 2, false); // Init for Waveshare boards with "clever" reset circuit, 2ms reset pulse
#else
getdisplay().init(115200); // Init for normal displays
@@ -782,7 +782,7 @@ void OBP60Task(GwApi *api){
else{
getdisplay().fillScreen(commonData.fgcolor); // Clear display
#ifdef DISPLAY_GDEY042T81
getdisplay().hibernate(); // Set display in hybenate mode
getdisplay().hibernate(); // Set display in hibernate mode
getdisplay().init(115200, true, 2, false); // Init for Waveshare boards with "clever" reset circuit, 2ms reset pulse
#else
getdisplay().init(115200); // Init for normal displays

View File

@@ -0,0 +1,103 @@
diff --git a/lib/api/GwApi.h b/lib/api/GwApi.h
index 88f9690..9663a65 100644
--- a/lib/api/GwApi.h
+++ b/lib/api/GwApi.h
@@ -2,6 +2,8 @@
#define _GWAPI_H
#include "GwMessage.h"
#include "N2kMsg.h"
+#include "Nmea2kTwai.h"
+#include "N2kDeviceList.h"
#include "NMEA0183Msg.h"
#include "GWConfig.h"
#include "GwBoatData.h"
@@ -222,6 +224,8 @@ class GwApi{
* accessing boat data must only be executed from within the main thread
* you need to use the request pattern as shown in GwExampleTask.cpp
*/
+ virtual Nmea2kTwai *getNMEA2000()=0;
+ virtual tN2kDeviceList *getN2kDeviceList()=0;
virtual GwBoatData *getBoatData()=0;
virtual ~GwApi(){}
};
diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h
index 604c356..2fe4496 100644
--- a/lib/obp60task/OBP60Extensions.h
+++ b/lib/obp60task/OBP60Extensions.h
@@ -15,6 +15,9 @@
#define MOUNT_POINT "/sdcard"
#endif
+// Patches to apply to gateway code
+#define PATCH_N2K
+
// FRAM address reservations 32kB: 0x0000 - 0x7FFF
// 0x0000 - 0x03ff: single variables
#define FRAM_PAGE_NO 0x0002
diff --git a/lib/usercode/GwUserCode.cpp b/lib/usercode/GwUserCode.cpp
index 1b007f8..90087d4 100644
--- a/lib/usercode/GwUserCode.cpp
+++ b/lib/usercode/GwUserCode.cpp
@@ -216,6 +216,14 @@ public:
{
return api->getLogger();
}
+ virtual Nmea2kTwai *getNMEA2000()
+ {
+ return api->getNMEA2000();
+ }
+ virtual tN2kDeviceList *getN2kDeviceList()
+ {
+ return api->getN2kDeviceList();
+ }
virtual GwBoatData *getBoatData()
{
return api->getBoatData();
@@ -428,4 +436,4 @@ void GwUserCode::handleWebRequest(const String &url,AsyncWebServerRequest *req){
}
LOG_DEBUG(GwLog::DEBUG,"no task found for web request %s[%s]",url.c_str(),tname.c_str());
req->send(404, "text/plain", "not found");
-}
\ No newline at end of file
+}
diff --git a/src/main.cpp b/src/main.cpp
index 44c715f..fdb0366 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -100,6 +100,7 @@ GwLog logger(LOGLEVEL,NULL);
GwConfigHandler config(&logger);
#include "Nmea2kTwai.h"
+#include <N2kDeviceList.h>
static const unsigned long CAN_RECOVERY_PERIOD=3000; //ms
static const unsigned long NMEA2000_HEARTBEAT_INTERVAL=5000;
class Nmea2kTwaiLog : public Nmea2kTwai{
@@ -126,6 +127,7 @@ class Nmea2kTwaiLog : public Nmea2kTwai{
#endif
Nmea2kTwai &NMEA2000=*(new Nmea2kTwaiLog((gpio_num_t)ESP32_CAN_TX_PIN,(gpio_num_t)ESP32_CAN_RX_PIN,CAN_RECOVERY_PERIOD,&logger));
+tN2kDeviceList *pN2kDeviceList;
#ifdef GWBUTTON_PIN
bool fixedApPass=false;
@@ -333,6 +335,12 @@ public:
status.n2kTx=countNMEA2KOut.getGlobal();
channels.fillStatus(status);
}
+ virtual Nmea2kTwai *getNMEA2000(){
+ return &NMEA2000;
+ }
+ virtual tN2kDeviceList *getN2kDeviceList(){
+ return pN2kDeviceList;
+ }
virtual GwBoatData *getBoatData(){
return &boatData;
}
@@ -935,6 +943,7 @@ void setup() {
NMEA2000.SetMsgHandler([](const tN2kMsg &n2kMsg){
handleN2kMessage(n2kMsg,N2K_CHANNEL_ID);
});
+ pN2kDeviceList = new tN2kDeviceList(&NMEA2000);
NMEA2000.Open();
logger.logDebug(GwLog::LOG,"starting addon tasks");
logger.flush();

View File

@@ -16,6 +16,8 @@ board_build.variants_dir = variants
board = obp60_s3_n16r8 #ESP32-S3 N16R8, 16MB flash, 8MB PSRAM, production series
#board_build.partitions = default_8MB.csv #ESP32-S3 N8, 8MB flash
board_build.partitions = default_16MB.csv #ESP32-S3 N16, 16MB flash
custom_config = lib/obp60task/config_obp60.json
custom_script = lib/obp60task/extra_task.py
framework = arduino
lib_deps =
${basedeps.lib_deps}
@@ -56,6 +58,7 @@ build_flags=
# -D DISPLAY_GYE042A87 #alternativ E-Ink display from Genyo Optical, R10 2.2 ohm - medium
# -D DISPLAY_SE0420NQ04 #alternativ E-Ink display from SID Technology, R10 2.2 ohm - bad (burn in effects)
# -D DISPLAY_ZJY400300-042CAAMFGN #alternativ E-Ink display from ZZE Technology, R10 2.2 ohm - very good
# -D ENABLE_PATCHES #enable patching of gateway code
${env.build_flags}
#CONFIG_ESP_TASK_WDT_TIMEOUT_S = 10 #Task Watchdog timeout period (seconds) [1...60] 5 default
upload_port = /dev/ttyACM0 #OBP60 download via USB-C direct
@@ -68,7 +71,8 @@ platform = espressif32@6.8.1
board_build.variants_dir = variants
board = obp40_s3_n8r8 #ESP32-S3 N8R8, 8MB flash, 8MB PSRAM, OBP60 clone (CrowPanel 4.2)
board_build.partitions = default_8MB.csv #ESP32-S3 N8, 8MB flash
custom_config = config_obp40.json
custom_config = lib/obp60task/config_obp40.json
custom_script = lib/obp60task/extra_task.py
framework = arduino
lib_deps =
${basedeps.lib_deps}
@@ -105,6 +109,7 @@ build_flags=
#-D DISPLAY_ZJY400300-042CAAMFGN #alternativ E-Ink display from ZZE Technology, R10 2.2 ohm - very good
-D LIPO_ACCU_1200 #Hardware extension, LiPo accu 3,7V 1200mAh
-D VOLTAGE_SENSOR #Hardware extension, LiPo voltage sensor with two resistors
#-D ENABLE_PATCHES #enable patching of gateway code
${env.build_flags}
upload_port = /dev/ttyUSB0 #OBP40 download via external USB/Serail converter
upload_protocol = esptool #firmware upload via USB OTG seriell, by first upload need to set the ESP32-S3 in the upload mode with shortcut GND to Pin27

View File

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