diff --git a/boards/obp40_s3_n8r8.json b/boards/obp40_s3_n8r8.json index 260050c..2ac8c3e 100644 --- a/boards/obp40_s3_n8r8.json +++ b/boards/obp40_s3_n8r8.json @@ -19,8 +19,8 @@ "flash_mode": "qio", "hwids": [ [ - "0x303A", - "0x1001" + "0x1A86", + "0x7523" ] ], "mcu": "esp32s3", diff --git a/extra_script.py b/extra_script.py index ed62c64..b185a93 100644 --- a/extra_script.py +++ b/extra_script.py @@ -10,7 +10,7 @@ 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()) @@ -104,7 +104,17 @@ def writeFileIfChanged(fileName,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) @@ -274,9 +284,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 diff --git a/extra_script.py.old b/extra_script.py.old new file mode 100644 index 0000000..ed62c64 --- /dev/null +++ b/extra_script.py.old @@ -0,0 +1,518 @@ +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) diff --git a/lib/obp60task/OBP60Extensions.cpp b/lib/obp60task/OBP60Extensions.cpp index 9f7df24..c589feb 100644 --- a/lib/obp60task/OBP60Extensions.cpp +++ b/lib/obp60task/OBP60Extensions.cpp @@ -141,12 +141,12 @@ void deepSleep(CommonData &common){ getdisplay().print("Sleep Mode"); getdisplay().setFont(&Ubuntu_Bold8pt7b); getdisplay().setCursor(65, 175); - getdisplay().print("For wakeup press key and wait 5s"); + getdisplay().print("To wake up press key and wait 5s"); getdisplay().nextPage(); // Update display contents getdisplay().powerOff(); // Display power off setPortPin(OBP_POWER_50, false); // Power off ePaper display // Stop system - esp_deep_sleep_start(); // Deep Sleep with weakup via touch pin + esp_deep_sleep_start(); // Deep Sleep with weakup via touch pin } #endif #ifdef BOARD_OBP40S3 @@ -166,7 +166,7 @@ void deepSleep(CommonData &common){ getdisplay().print("Sleep Mode"); getdisplay().setFont(&Ubuntu_Bold8pt7b); getdisplay().setCursor(65, 175); - getdisplay().print("For wakeup press wheel and wait 5s"); + getdisplay().print("To wake up press wheel and wait 5s"); getdisplay().nextPage(); // Partial update getdisplay().powerOff(); // Display power off setPortPin(OBP_POWER_EPD, false); // Power off ePaper display @@ -341,13 +341,9 @@ void displayHeader(CommonData &commonData, GwApi::BoatValue *date, GwApi::BoatVa static unsigned long tcpClTxOld = 0; static unsigned long n2kRxOld = 0; static unsigned long n2kTxOld = 0; - int textcolor = GxEPD_BLACK; if(commonData.config->getBool(commonData.config->statusLine) == true){ - // Header separator line (optional) - // getdisplay().drawLine(0, 19, 399, 19, commonData.fgcolor); - // Show status info getdisplay().setTextColor(commonData.fgcolor); getdisplay().setFont(&Ubuntu_Bold8pt7b); @@ -392,13 +388,37 @@ void displayHeader(CommonData &commonData, GwApi::BoatValue *date, GwApi::BoatVa getdisplay().drawXBitmap(166, 1, swipe_bits, swipe_width, swipe_height, commonData.fgcolor); } #endif +#ifdef LIPO_ACCU_1200 + if (commonData.data.BatteryChargeStatus == 1) { + getdisplay().drawXBitmap(170, 1, battery_loading_bits, battery_width, battery_height, commonData.fgcolor); + } else { +#ifdef VOLTAGE_SENSOR + if (commonData.data.batteryLevelLiPo < 10) { + getdisplay().drawXBitmap(170, 1, battery_0_bits, battery_width, battery_height, commonData.fgcolor); + } else if (commonData.data.batteryLevelLiPo < 25) { + getdisplay().drawXBitmap(170, 1, battery_25_bits, battery_width, battery_height, commonData.fgcolor); + } else if (commonData.data.batteryLevelLiPo < 50) { + getdisplay().drawXBitmap(170, 1, battery_50_bits, battery_width, battery_height, commonData.fgcolor); + } else if (commonData.data.batteryLevelLiPo < 75) { + getdisplay().drawXBitmap(170, 1, battery_75_bits, battery_width, battery_height, commonData.fgcolor); + } else { + getdisplay().drawXBitmap(170, 1, battery_100_bits, battery_width, battery_height, commonData.fgcolor); + } +#endif // VOLTAGE_SENSOR + } +#endif // LIPO_ACCU_1200 - // Heartbeat as dot - getdisplay().setTextColor(commonData.fgcolor); - getdisplay().setFont(&Ubuntu_Bold32pt7b); - getdisplay().setCursor(205, 14); - getdisplay().print(heartbeat ? "." : " "); - heartbeat = !heartbeat; + // Heartbeat as page number + if (heartbeat) { + getdisplay().setTextColor(commonData.bgcolor); + getdisplay().fillRect(201, 0, 23, 19, commonData.fgcolor); + } else { + getdisplay().setTextColor(commonData.fgcolor); + getdisplay().drawRect(201, 0, 23, 19, commonData.fgcolor); + } + getdisplay().setFont(&Ubuntu_Bold8pt7b); + drawTextCenter(211, 9, String(commonData.data.actpage)); + heartbeat = !heartbeat; // Date and time getdisplay().setTextColor(commonData.fgcolor); @@ -442,17 +462,16 @@ void displayFooter(CommonData &commonData) { // horizontal elements const uint16_t top = 280; const uint16_t bottom = 299; + // horizontal stub lines getdisplay().drawLine(commonData.keydata[0].x, top, commonData.keydata[0].x+10, top, commonData.fgcolor); - getdisplay().drawLine(commonData.keydata[1].x-10, top, commonData.keydata[1].x+10, top, commonData.fgcolor); - getdisplay().drawLine(commonData.keydata[2].x-10, top, commonData.keydata[2].x+10, top, commonData.fgcolor); - getdisplay().drawLine(commonData.keydata[4].x-10, top, commonData.keydata[4].x+10, top, commonData.fgcolor); - getdisplay().drawLine(commonData.keydata[5].x-10, top, commonData.keydata[5].x+10, top, commonData.fgcolor); + for (int i = 1; i <= 5; i++) { + getdisplay().drawLine(commonData.keydata[i].x-10, top, commonData.keydata[i].x+10, top, commonData.fgcolor); + } getdisplay().drawLine(commonData.keydata[5].x + commonData.keydata[5].w - 10, top, commonData.keydata[5].x + commonData.keydata[5].w + 1, top, commonData.fgcolor); // vertical key separators - getdisplay().drawLine(commonData.keydata[0].x + commonData.keydata[0].w, top, commonData.keydata[0].x + commonData.keydata[0].w, bottom, commonData.fgcolor); - getdisplay().drawLine(commonData.keydata[1].x + commonData.keydata[1].w, top, commonData.keydata[1].x + commonData.keydata[1].w, bottom, commonData.fgcolor); - getdisplay().drawLine(commonData.keydata[3].x + commonData.keydata[3].w, top, commonData.keydata[3].x + commonData.keydata[3].w, bottom, commonData.fgcolor); - getdisplay().drawLine(commonData.keydata[4].x + commonData.keydata[4].w, top, commonData.keydata[4].x + commonData.keydata[4].w, bottom, commonData.fgcolor); + for (int i = 0; i <= 4; i++) { + getdisplay().drawLine(commonData.keydata[i].x + commonData.keydata[i].w, top, commonData.keydata[i].x + commonData.keydata[i].w, bottom, commonData.fgcolor); + } for (int i = 0; i < 6; i++) { uint16_t x, y; if (commonData.keydata[i].label.length() > 0) { @@ -476,9 +495,6 @@ void displayFooter(CommonData &commonData) { } } } - // Current page number in a small box - getdisplay().drawRect(190, 280, 23, 19, commonData.fgcolor); - drawTextCenter(200, 289, String(commonData.data.actpage)); } else { getdisplay().setCursor(65, 295); getdisplay().print("Press 1 and 6 fast to unlock keys"); diff --git a/lib/obp60task/OBP60Extensions.h b/lib/obp60task/OBP60Extensions.h index b6f653f..4118623 100644 --- a/lib/obp60task/OBP60Extensions.h +++ b/lib/obp60task/OBP60Extensions.h @@ -100,8 +100,8 @@ void displayFooter(CommonData &commonData); SunData calcSunsetSunrise(GwApi *api, double time, double date, double latitude, double longitude, double timezone); // Calulate sunset and sunrise void batteryGraphic(uint x, uint y, float percent, int pcolor, int bcolor); // Battery graphic with fill level -void solarGraphic(uint x, uint y, int pcolor, int bcolor); // Solar graphic with fill level -void generatorGraphic(uint x, uint y, int pcolor, int bcolor); // Generator graphic with fill level +void solarGraphic(uint x, uint y, int pcolor, int bcolor); // Solar graphic +void generatorGraphic(uint x, uint y, int pcolor, int bcolor); // Generator graphic void startLedTask(GwApi *api); void doImageRequest(GwApi *api, int *pageno, const PageStruct pages[MAX_PAGE_NUMBER], AsyncWebServerRequest *request); @@ -160,6 +160,46 @@ static std::map iconmap = { {"AP", ap_bits} }; +// Battery +#define battery_width 24 +#define battery_height 16 + +static unsigned char battery_0_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0xff, 0x0f, 0xff, 0xff, 0x1f, + 0x03, 0x00, 0x18, 0x03, 0x00, 0x78, 0x03, 0x00, 0xf8, 0x03, 0x00, 0xd8, + 0x03, 0x00, 0xd8, 0x03, 0x00, 0xd8, 0x03, 0x00, 0xf8, 0x03, 0x00, 0x78, + 0x03, 0x00, 0x18, 0xff, 0xff, 0x1f, 0xfe, 0xff, 0x0f, 0x00, 0x00, 0x00 }; + +static unsigned char battery_25_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0xff, 0x0f, 0xff, 0xff, 0x1f, + 0x03, 0x00, 0x18, 0x3b, 0x00, 0x78, 0x3b, 0x00, 0xf8, 0x3b, 0x00, 0xd8, + 0x3b, 0x00, 0xd8, 0x3b, 0x00, 0xd8, 0x3b, 0x00, 0xf8, 0x3b, 0x00, 0x78, + 0x03, 0x00, 0x18, 0xff, 0xff, 0x1f, 0xfe, 0xff, 0x0f, 0x00, 0x00, 0x00 }; + +static unsigned char battery_50_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0xff, 0x0f, 0xff, 0xff, 0x1f, + 0x03, 0x00, 0x18, 0xbb, 0x03, 0x78, 0xbb, 0x03, 0xf8, 0xbb, 0x03, 0xd8, + 0xbb, 0x03, 0xd8, 0xbb, 0x03, 0xd8, 0xbb, 0x03, 0xf8, 0xbb, 0x03, 0x78, + 0x03, 0x00, 0x18, 0xff, 0xff, 0x1f, 0xfe, 0xff, 0x0f, 0x00, 0x00, 0x00 }; + +static unsigned char battery_75_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0xff, 0x0f, 0xff, 0xff, 0x1f, + 0x03, 0x00, 0x18, 0xbb, 0x3b, 0x78, 0xbb, 0x3b, 0xf8, 0xbb, 0x3b, 0xd8, + 0xbb, 0x3b, 0xd8, 0xbb, 0x3b, 0xd8, 0xbb, 0x3b, 0xf8, 0xbb, 0x3b, 0x78, + 0x03, 0x00, 0x18, 0xff, 0xff, 0x1f, 0xfe, 0xff, 0x0f, 0x00, 0x00, 0x00 }; + +static unsigned char battery_100_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0xff, 0x0f, 0xff, 0xff, 0x1f, + 0x03, 0x00, 0x18, 0xbb, 0xbb, 0x7b, 0xbb, 0xbb, 0xfb, 0xbb, 0xbb, 0xdb, + 0xbb, 0xbb, 0xdb, 0xbb, 0xbb, 0xdb, 0xbb, 0xbb, 0xfb, 0xbb, 0xbb, 0x7b, + 0x03, 0x00, 0x18, 0xff, 0xff, 0x1f, 0xfe, 0xff, 0x0f, 0x00, 0x00, 0x00 }; + +static unsigned char battery_loading_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0xfe, 0xe4, 0x0f, 0xff, 0xec, 0x1f, + 0x03, 0x08, 0x18, 0x03, 0x18, 0x78, 0x03, 0x30, 0xf8, 0x83, 0x3f, 0xd8, + 0x03, 0x7f, 0xd8, 0x03, 0x03, 0xd8, 0x03, 0x06, 0xf8, 0x03, 0x04, 0x78, + 0x03, 0x0c, 0x18, 0xff, 0xcb, 0x1f, 0xfe, 0xd3, 0x0f, 0x00, 0x10, 0x00 }; + // Other symbols #define swipe_width 24 #define swipe_height 16 diff --git a/lib/obp60task/OBP60Keypad.h b/lib/obp60task/OBP60Keypad.h index 937f44e..eafe3a2 100644 --- a/lib/obp60task/OBP60Keypad.h +++ b/lib/obp60task/OBP60Keypad.h @@ -277,8 +277,8 @@ void initKeys(CommonData &commonData) { starttime = millis(); // Start key pressed keycodeold = keycode; } - // If key pressed longer than 200ms - if(millis() > starttime + 200 && keycode == keycodeold) { + // If key pressed longer than 100ms + if(millis() > starttime + 100 && keycode == keycodeold) { if (use_syspage and keycode == 3) { keystatus = 12; } else { diff --git a/lib/obp60task/OBPSensorTask.cpp b/lib/obp60task/OBPSensorTask.cpp index 99071ab..445fb21 100644 --- a/lib/obp60task/OBPSensorTask.cpp +++ b/lib/obp60task/OBPSensorTask.cpp @@ -88,8 +88,16 @@ void sensorTask(void *param){ double voffset = (api->getConfig()->getConfigItem(api->getConfig()->vOffset,true)->asString()).toFloat(); double vslope = (api->getConfig()->getConfigItem(api->getConfig()->vSlope,true)->asString()).toFloat(); if(String(powsensor1) == "off"){ - sensors.batteryVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.17) * 20; // Vin = 1/20 - sensors.batteryVoltage = sensors.batteryVoltage * vslope + voffset; // Calibration + #ifdef VOLTAGE_SENSOR + float rawVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.53) * 2; // Vin = 1/2 for OBP40 + #else + float rawVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.17) * 20; // Vin = 1/20 for OBP60 + #endif + sensors.batteryVoltage = rawVoltage * vslope + voffset; // Calibration + #ifdef LIPO_ACCU_1200 + sensors.BatteryChargeStatus = 0; // Set to discharging + sensors.batteryLevelLiPo = 0; // Level 0...100% + #endif sensors.batteryCurrent = 0; sensors.batteryPower = 0; // Fill average arrays with start values @@ -459,19 +467,61 @@ void sensorTask(void *param){ // Send supply voltage value all 1s if(millis() > starttime5 + 1000 && String(powsensor1) == "off"){ starttime5 = millis(); - sensors.batteryVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.17) * 20; // Vin = 1/20 - sensors.batteryVoltage = sensors.batteryVoltage * vslope + voffset; // Calibration + #ifdef VOLTAGE_SENSOR + float rawVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.53) * 2; // Vin = 1/2 for OBP40 + #else + float rawVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.17) * 20; // Vin = 1/20 for OBP60 + #endif + sensors.batteryVoltage = rawVoltage * vslope + voffset; // Calibration // Save new data in average array batV.reading(int(sensors.batteryVoltage * 100)); // Calculate the average values for different time lines from integer values sensors.batteryVoltage10 = batV.getAvg(10) / 100.0; sensors.batteryVoltage60 = batV.getAvg(60) / 100.0; sensors.batteryVoltage300 = batV.getAvg(300) / 100.0; + #if defined LIPO_ACCU_1200 && defined VOLTAGE_SENSOR + // Polynomfit for LiPo capacity calculation for 3,7V LiPo accus, 0...100% + sensors.batteryLevelLiPo = sensors.batteryVoltage60 * 203.8312 -738.1635; + // Limiter + if(sensors.batteryLevelLiPo > 100){ + sensors.batteryLevelLiPo = 100; + } + if(sensors.batteryLevelLiPo < 0){ + sensors.batteryLevelLiPo = 0; + } + // Charging detection + float deltaV = sensors.batteryVoltage - sensors.batteryVoltage10; + // Higher limits for lower voltages + if(sensors.batteryVoltage10 < 4.0){ + if(deltaV > 0.045 && deltaV < 4.15){ + sensors.BatteryChargeStatus = 1; // Charging active + } + if(deltaV < -0.04 || deltaV >= 4.15){ // Charging stops by grater than 4,15V + sensors.BatteryChargeStatus = 0; // Discharging + } + } + // Lower limits for higher voltages + else{ + if(deltaV > 0.03 && deltaV < 4.15){ + sensors.BatteryChargeStatus = 1; // Charging active + } + if(deltaV < -0.03 || deltaV >= 4.15){ // Charging stops by grater than 4,15V + sensors.BatteryChargeStatus = 0; // Discharging + } + } + // Send to NMEA200 bus as instance 10 + if(!isnan(sensors.batteryVoltage)){ + SetN2kDCBatStatus(N2kMsg, 10, sensors.batteryVoltage, N2kDoubleNA, N2kDoubleNA, 0); + api->sendN2kMessage(N2kMsg); + } + #endif + #ifdef BOARD_OBP60S3 // Send to NMEA200 bus if(!isnan(sensors.batteryVoltage)){ SetN2kDCBatStatus(N2kMsg, 0, sensors.batteryVoltage, N2kDoubleNA, N2kDoubleNA, 1); api->sendN2kMessage(N2kMsg); } + #endif } // Send data from environment sensor all 2s diff --git a/lib/obp60task/PageVoltage.cpp b/lib/obp60task/PageVoltage.cpp index d05c903..c98bc96 100644 --- a/lib/obp60task/PageVoltage.cpp +++ b/lib/obp60task/PageVoltage.cpp @@ -205,6 +205,18 @@ public: getdisplay().setCursor(20, 100); getdisplay().print(name1); // Value name + #if defined BOARD_OBP40S3 && defined LIPO_ACCU_1200 && defined VOLTAGE_SENSOR + // Show charge status + getdisplay().setFont(&Ubuntu_Bold8pt7b); + getdisplay().setCursor(185, 100); + if(commonData->data.BatteryChargeStatus == true){ + getdisplay().print("Charge"); + } + else{ + getdisplay().print("Discharge"); + } + #endif + // Show unit getdisplay().setFont(&Ubuntu_Bold20pt7b); getdisplay().setCursor(270, 100); @@ -213,7 +225,12 @@ public: // Show battery type getdisplay().setFont(&Ubuntu_Bold8pt7b); getdisplay().setCursor(295, 100); + #ifdef BOARD_OBP60S3 getdisplay().print(batType); + #endif + #if defined BOARD_OBP40S3 && defined LIPO_ACCU_1200 && defined VOLTAGE_SENSOR + getdisplay().print("LiPo"); + #endif // Show average settings printAvg(average, 320, 84, true); diff --git a/lib/obp60task/Pagedata.h b/lib/obp60task/Pagedata.h index b385b5c..b979b57 100644 --- a/lib/obp60task/Pagedata.h +++ b/lib/obp60task/Pagedata.h @@ -31,6 +31,8 @@ typedef struct{ double batteryVoltage300 = 0; // Sliding average over 300 values double batteryCurrent300 = 0; double batteryPower300 = 0; + double batteryLevelLiPo = 0; // Battery level for OBP40 LiPo accu + int BatteryChargeStatus = 0; // LiPo charge status: 0 = discharge, 1 = loading activ double solarVoltage = 0; double solarCurrent = 0; double solarPower = 0; @@ -93,6 +95,7 @@ typedef struct{ uint16_t fgcolor; uint16_t bgcolor; bool keylock = false; + String powermode; } CommonData; //a base class that all pages must inherit from @@ -110,7 +113,7 @@ class Page{ commonData->keydata[2].label = "#LEFT"; commonData->keydata[3].label = "#RIGHT"; commonData->keydata[4].label = ""; - if (commonData->backlight.mode == KEY) { + if ((commonData->backlight.mode == KEY) && !(commonData->powermode == "Min Power")) { commonData->keydata[5].label = "ILUM"; } else { commonData->keydata[5].label = ""; diff --git a/lib/obp60task/config_obp40.json b/lib/obp60task/config_obp40.json index 3a4b6bf..888c746 100644 --- a/lib/obp60task/config_obp40.json +++ b/lib/obp60task/config_obp40.json @@ -609,10 +609,10 @@ "label": "Undervoltage", "type": "boolean", "default": "false", - "description": "Switch off device if voltage drops below 9V [on|off]", + "description": "Switch off device if LiPo voltage drops below 3.65V [on|off]", "category": "OBP40 Hardware", "capabilities": { - "obp40": "false" + "obp40":"true" } }, { diff --git a/lib/obp60task/obp60task.cpp b/lib/obp60task/obp60task.cpp index a55e62b..1b4b936 100644 --- a/lib/obp60task/obp60task.cpp +++ b/lib/obp60task/obp60task.cpp @@ -78,7 +78,7 @@ void OBP60Init(GwApi *api){ } #ifdef BOARD_OBP40S3 - //String sdcard = config->getConfigItem(config->useSDCard, true)->asString(); +// String sdcard = config->getConfigItem(config->useSDCard, true)->asString(); String sdcard = "on"; if (sdcard == "on") { SPIClass SD_SPI = SPIClass(HSPI); @@ -310,12 +310,39 @@ void registerAllPages(PageList &list){ // Undervoltage detection for shutdown display void underVoltageDetection(GwApi *api, CommonData &common){ // Read settings - float vslope = uint(api->getConfig()->getConfigItem(api->getConfig()->vSlope,true)->asFloat()); - float voffset = uint(api->getConfig()->getConfigItem(api->getConfig()->vOffset,true)->asFloat()); + double voffset = (api->getConfig()->getConfigItem(api->getConfig()->vOffset,true)->asString()).toFloat(); + double vslope = (api->getConfig()->getConfigItem(api->getConfig()->vSlope,true)->asString()).toFloat(); // Read supply voltage - float actVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.17) * 20; // V = 1/20 * Vin - actVoltage = actVoltage * vslope + voffset; - if(actVoltage < MIN_VOLTAGE){ + #if defined VOLTAGE_SENSOR && defined LIPO_ACCU_1200 + float actVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.53) * 2; // Vin = 1/2 for OBP40 + float minVoltage = 3.65; // Absolut minimum volatge for 3,7V LiPo accu + #else + float actVoltage = (float(analogRead(OBP_ANALOG0)) * 3.3 / 4096 + 0.17) * 20; // Vin = 1/20 for OBP60 + float minVoltage = MIN_VOLTAGE; + #endif + double calVoltage = actVoltage * vslope + voffset; // Calibration + if(calVoltage < minVoltage){ + #if defined VOLTAGE_SENSOR && defined LIPO_ACCU_1200 + // Switch off all power lines + setPortPin(OBP_BACKLIGHT_LED, false); // Backlight Off + setFlashLED(false); // Flash LED Off + buzzer(TONE4, 20); // Buzzer tone 4kHz 20ms + // Shutdown EInk display + getdisplay().setFullWindow(); // Set full Refresh + //getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update + getdisplay().fillScreen(common.bgcolor);// Clear screen + getdisplay().setTextColor(common.fgcolor); + getdisplay().setFont(&Ubuntu_Bold20pt7b); + getdisplay().setCursor(65, 150); + getdisplay().print("Undervoltage"); + getdisplay().setFont(&Ubuntu_Bold8pt7b); + getdisplay().setCursor(65, 175); + getdisplay().print("Charge battery and restart system"); + getdisplay().nextPage(); // Partial update + getdisplay().powerOff(); // Display power off + setPortPin(OBP_POWER_EPD, false); // Power off ePaper display + setPortPin(OBP_POWER_SD, false); // Power off SD card + #else // Switch off all power lines setPortPin(OBP_BACKLIGHT_LED, false); // Backlight Off setFlashLED(false); // Flash LED Off @@ -323,13 +350,17 @@ void underVoltageDetection(GwApi *api, CommonData &common){ setPortPin(OBP_POWER_50, false); // Power rail 5.0V Off // Shutdown EInk display getdisplay().setPartialWindow(0, 0, getdisplay().width(), getdisplay().height()); // Set partial update - getdisplay().fillScreen(common.bgcolor); // Clear screen + getdisplay().fillScreen(common.bgcolor);// Clear screen getdisplay().setTextColor(common.fgcolor); getdisplay().setFont(&Ubuntu_Bold20pt7b); getdisplay().setCursor(65, 150); getdisplay().print("Undervoltage"); + getdisplay().setFont(&Ubuntu_Bold8pt7b); + getdisplay().setCursor(65, 175); + getdisplay().print("To wake up repower system"); getdisplay().nextPage(); // Partial update getdisplay().powerOff(); // Display power off + #endif // Stop system while(true){ esp_deep_sleep_start(); // Deep Sleep without weakup. Weakup only after power cycle (restart). @@ -382,7 +413,8 @@ void OBP60Task(GwApi *api){ String fastrefresh = api->getConfig()->getConfigItem(api->getConfig()->fastRefresh,true)->asString(); uint fullrefreshtime = uint(api->getConfig()->getConfigItem(api->getConfig()->fullRefreshTime,true)->asInt()); #ifdef BOARD_OBP40S3 - bool syspage_enabled = config->getBool(config->systemPage); +// bool syspage_enabled = config->getBool(config->systemPage); + bool syspage_enabled = true; #endif #ifdef DISPLAY_GDEY042T81 @@ -523,6 +555,7 @@ void OBP60Task(GwApi *api){ commonData.backlight.mode = backlightMapping(config->getConfigItem(config->backlight,true)->asString()); commonData.backlight.color = colorMapping(config->getConfigItem(config->blColor,true)->asString()); commonData.backlight.brightness = 2.55 * uint(config->getConfigItem(config->blBrightness,true)->asInt()); + commonData.powermode = api->getConfig()->getConfigItem(api->getConfig()->powerMode,true)->asString(); bool uvoltage = api->getConfig()->getConfigItem(api->getConfig()->underVoltage,true)->asBoolean(); String cpuspeed = api->getConfig()->getConfigItem(api->getConfig()->cpuSpeed,true)->asString(); diff --git a/lib/obp60task/platformio.ini b/lib/obp60task/platformio.ini index da78f39..2259b61 100644 --- a/lib/obp60task/platformio.ini +++ b/lib/obp60task/platformio.ini @@ -2,7 +2,10 @@ #if you want a pio run to only build #your special environments you can set this here #by uncommenting the next line -default_envs = obp60_s3 + +default_envs = + obp60_s3 + obp40_s3 [env:obp60_s3] platform = espressif32@6.8.1 @@ -91,6 +94,8 @@ build_flags= -D DISABLE_DIAGNOSTIC_OUTPUT #Disable diagnostic output for GxEPD2 lib -D BOARD_OBP40S3 #Board OBP40 V1.0 with ESP32S3 SKU:DIE07300S (CrowPanel 4.2) -D DISPLAY_GDEY042T81 #new E-Ink display from Waveshare, R10 2.2 ohm + -D LIPO_ACCU_1200 #Hardware extension, LiPo accu 3,7V 1200mAh + -D VOLTAGE_SENSOR #Hardware extension, LiPo voltage sensor with two resistors ${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