Merge branch 'master' of https://github.com/wellenvogel/esp32-nmea2000 into feature/env2

This commit is contained in:
free-x 2021-11-21 22:07:57 +01:00
commit eb7239f27e
6 changed files with 517 additions and 239 deletions

View File

@ -10,10 +10,18 @@ Import("env")
GEN_DIR='generated' GEN_DIR='generated'
CFG_FILE='web/config.json' CFG_FILE='web/config.json'
XDR_FILE='web/xdrconfig.json' XDR_FILE='web/xdrconfig.json'
FILES=['web/index.html',CFG_FILE,XDR_FILE,'web/index.js','web/index.css']
CFG_INCLUDE='GwConfigDefinitions.h' CFG_INCLUDE='GwConfigDefinitions.h'
XDR_INCLUDE='GwXdrTypeMappings.h' XDR_INCLUDE='GwXdrTypeMappings.h'
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(): def basePath():
#see: https://stackoverflow.com/questions/16771894/python-nameerror-global-name-file-is-not-defined #see: https://stackoverflow.com/questions/16771894/python-nameerror-global-name-file-is-not-defined
@ -38,29 +46,45 @@ def isCurrent(infile,outfile):
print("%s is newer then %s, no need to recreate"%(outfile,infile)) print("%s is newer then %s, no need to recreate"%(outfile,infile))
return True return True
return False return False
def compressFile(inFile): def compressFile(inFile,outfile):
outfile=os.path.basename(inFile)+".gz"
inFile=os.path.join(basePath(),inFile)
outfile=os.path.join(outPath(),outfile)
if isCurrent(inFile,outfile): if isCurrent(inFile,outfile):
return return
with open(inFile, 'rb') as f_in: with open(inFile, 'rb') as f_in:
with gzip.open(outfile, 'wb') as f_out: with gzip.open(outfile, 'wb') as f_out:
shutil.copyfileobj(f_in, f_out) shutil.copyfileobj(f_in, f_out)
def generateFile(infile,outfile,callback,inMode='rb',outMode='w'):
def generateCfg():
outfile=os.path.join(outPath(),CFG_INCLUDE)
infile=os.path.join(basePath(),CFG_FILE)
if isCurrent(infile,outfile): if isCurrent(infile,outfile):
return return
print("creating %s"%CFG_INCLUDE) print("creating %s"%outfile)
oh=None oh=None
with open(CFG_FILE,'rb') as ch: with open(infile,inMode) as ch:
config=json.load(ch)
try:
with open(outfile,'w') as oh: with open(outfile,'w') as oh:
oh.write("//generated from %s\n"%CFG_FILE) 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
print("#generating %s"%fileName)
with open(fileName,"w") as oh:
oh.write(data)
def generateCfg(ch,oh,inFile=''):
config=json.load(ch)
oh.write("//generated from %s\n"%inFile)
oh.write('#include "GwConfigItem.h"\n') oh.write('#include "GwConfigItem.h"\n')
l=len(config) l=len(config)
oh.write('class GwConfigDefinitions{\n') oh.write('class GwConfigDefinitions{\n')
@ -83,28 +107,10 @@ def generateCfg():
oh.write(" new GwConfigItem(%s,\"%s\")"%(item.get('name'),item.get('default'))) oh.write(" new GwConfigItem(%s,\"%s\")"%(item.get('name'),item.get('default')))
oh.write('};\n') oh.write('};\n')
oh.write('};\n') oh.write('};\n')
oh.close()
except Exception as e:
if oh is not None:
try:
oh.close()
except:
pass
os.unlink(outfile)
raise
def generateXdrMappings():
outfile=os.path.join(outPath(),XDR_INCLUDE)
infile=os.path.join(basePath(),XDR_FILE)
if isCurrent(infile,outfile):
return
print("creating %s"%XDR_INCLUDE)
oh=None
with open(infile,"rb") as fp: def generateXdrMappings(fp,oh,inFile=''):
jdoc=json.load(fp) jdoc=json.load(fp)
try:
with open(outfile,"w") as oh:
oh.write("static GwXDRTypeMapping* typeMappings[]={\n") oh.write("static GwXDRTypeMapping* typeMappings[]={\n")
first=True first=True
for cat in jdoc: for cat in jdoc:
@ -118,7 +124,7 @@ def generateXdrMappings():
first=False first=False
else: else:
oh.write(",\n") oh.write(",\n")
oh.write(" new GwXDRTypeMapping(%d,%d,0) /*%s*/"%(cid,tc,cat)) oh.write(" new GwXDRTypeMapping(%d,0,%d) /*%s*/"%(cid,tc,cat))
fields=item.get('fields') fields=item.get('fields')
if fields is None: if fields is None:
continue continue
@ -138,26 +144,67 @@ def generateXdrMappings():
first=False first=False
else: else:
oh.write(",\n") oh.write(",\n")
oh.write(" new GwXDRTypeMapping(%d,%d,%d) /*%s:%s*/"%(cid,tc,id,cat,l)) oh.write(" new GwXDRTypeMapping(%d,%d,%d) /*%s:%s*/"%(cid,id,tc,cat,l))
oh.write("\n") oh.write("\n")
oh.write("};\n") oh.write("};\n")
except Exception as e:
if oh:
try:
oh.close()
except:
pass
os.unlink(outfile)
raise
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 prebuild(env):
print("#prebuild running")
if not checkDir(): if not checkDir():
sys.exit(1) sys.exit(1)
for f in FILES: embedded=getEmbeddedFiles(env)
print("compressing %s"%f) filedefs=[]
compressFile(f) for ef in embedded:
generateCfg() print("#checking embedded file %s"%ef)
generateXdrMappings() (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):
print("compressing %s"%inFile)
compressFile(inFile,ef)
else:
print("#WARNING: infile %s for %s not found"%(inFile,ef))
generateEmbedded(filedefs,os.path.join(outPath(),EMBEDDED_INCLUDE))
generateFile(os.path.join(basePath(),CFG_FILE),os.path.join(outPath(),CFG_INCLUDE),generateCfg)
generateFile(os.path.join(basePath(),XDR_FILE),os.path.join(outPath(),XDR_INCLUDE),generateXdrMappings)
version="dev"+datetime.now().strftime("%Y%m%d") version="dev"+datetime.now().strftime("%Y%m%d")
env.Append(CPPDEFINES=[('GWDEVVERSION',version)]) 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)
#script does not run on clean yet - maybe in the future
env.AddPostAction("clean",cleangenerated)

View File

@ -16,15 +16,12 @@ class EmbeddedFile {
embeddedFiles[name]=this; embeddedFiles[name]=this;
} }
} ; } ;
#define EMBED_GZ_FILE(fileName, fileExt, contentType) \ #define EMBED_GZ_FILE(fileName, binName, contentType) \
extern const uint8_t fileName##_##fileExt##_File[] asm("_binary_generated_" #fileName "_" #fileExt "_gz_start"); \ extern const uint8_t binName##_File[] asm("_binary_" #binName "_start"); \
extern const uint8_t fileName##_##fileExt##_FileLen[] asm("_binary_generated_" #fileName "_" #fileExt "_gz_size"); \ extern const uint8_t binName##_FileLen[] asm("_binary_" #binName "_size"); \
const EmbeddedFile fileName##_##fileExt##_Config(#fileName "." #fileExt,contentType,(const uint8_t*)fileName##_##fileExt##_File,(int)fileName##_##fileExt##_FileLen); const EmbeddedFile binName##_Config(fileName,contentType,(const uint8_t*)binName##_File,(int)binName##_FileLen);
EMBED_GZ_FILE(index,html,"text/html") #include "GwEmbeddedFiles.h"
EMBED_GZ_FILE(config,json,"application/json")
EMBED_GZ_FILE(index,js,"text/javascript")
EMBED_GZ_FILE(index,css,"text/css")
void sendEmbeddedFile(String name,String contentType,AsyncWebServerRequest *request){ void sendEmbeddedFile(String name,String contentType,AsyncWebServerRequest *request){
std::map<String,EmbeddedFile*>::iterator it=embeddedFiles.find(name); std::map<String,EmbeddedFile*>::iterator it=embeddedFiles.find(name);

View File

@ -23,6 +23,7 @@ board_build.embed_files =
generated/index.js.gz generated/index.js.gz
generated/index.css.gz generated/index.css.gz
generated/config.json.gz generated/config.json.gz
generated/xdrconfig.json.gz
board_build.partitions = partitions_custom.csv board_build.partitions = partitions_custom.csv
extra_scripts = extra_scripts =
pre:extra_script.py pre:extra_script.py

View File

@ -1,3 +1,6 @@
*{
box-sizing: border-box;
}
body{ body{
font-family: Arial, Helvetica, sans-serif; font-family: Arial, Helvetica, sans-serif;
} }
@ -89,6 +92,58 @@ body{
.hidden{ .hidden{
display: none !important; display: none !important;
} }
#xdrPage .content.hidden {
display: unset !important;
}
#xdrPage .category .title{
display: none;
}
#xdrPage span.label{
width: 4em;
}
#xdrPage .value{
width: 24em;
}
.xdrline {
padding-top: 0.2em;
padding-bottom: 0.2em;
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.xdrunused{
opacity: 0;
pointer-events: none;
}
span.xdrlabel {
width: 8em;
display: inline-block;
}
.xdrinput .xdrdir {
width: 12em;
}
.xdrinput .xdrdir:before {
content: 'Direction';
}
.xdrinput .xdrcat {
width: 12em;
}
.xdrinput .xdrsel {
width: 12em;
}
.xdrinput .xdrfield {
width: 12em;
}
.xdrinput .xdrimode {
width: 8em;
}
.xdrinput .xdrinstance {
width: 4em;
}
.xdrinput .xdrname {
width: 16em;
}
.msgDetails .value { .msgDetails .value {
width: 5em; width: 5em;
text-align: right; text-align: right;

View File

@ -17,6 +17,7 @@
<div id="tabs"> <div id="tabs">
<div class="tab active" data-page="statusPage">Status</div> <div class="tab active" data-page="statusPage">Status</div>
<div class="tab" data-page="configPage">Config</div> <div class="tab" data-page="configPage">Config</div>
<div class="tab" data-page="xdrPage">XDR</div>
<div class="tab" data-page="dashboardPage">Data</div> <div class="tab" data-page="dashboardPage">Data</div>
</div> </div>
<div id="statusPage" class="tabPage"> <div id="statusPage" class="tabPage">
@ -56,6 +57,16 @@
<button id="factoryReset">FactoryReset</button> <button id="factoryReset">FactoryReset</button>
</div> </div>
</div> </div>
<div class="configForm tabPage hidden" id="xdrPage" >
<div class="configFormRows">
</div>
<div class="buttons">
<button id="resetForm">ReloadConfig</button>
<button id="changeConfig">Save&Restart</button>
<button id="loadUnassigned">Show Unmapped</button>
</div>
</div>
<div class="tabPage hidden" id="dashboardPage"> <div class="tabPage hidden" id="dashboardPage">
</div> </div>

View File

@ -3,12 +3,14 @@ let lastUpdate = (new Date()).getTime();
let reloadConfig = false; let reloadConfig = false;
function addEl(type, clazz, parent, text) { function addEl(type, clazz, parent, text) {
let el = document.createElement(type); let el = document.createElement(type);
if (clazz) {
if (!(clazz instanceof Array)) { if (!(clazz instanceof Array)) {
clazz = clazz.split(/ */); clazz = clazz.split(/ */);
} }
clazz.forEach(function (ce) { clazz.forEach(function (ce) {
el.classList.add(ce); el.classList.add(ce);
}); });
}
if (text) el.textContent = text; if (text) el.textContent = text;
if (parent) parent.appendChild(el); if (parent) parent.appendChild(el);
return el; return el;
@ -230,15 +232,22 @@ function checkChange(el, row) {
} }
} }
} }
function createInput(configItem, frame) { let configDefinitions;
let xdrConfig;
function createInput(configItem, frame,clazz) {
let el; let el;
if (configItem.type === 'boolean' || configItem.type === 'list') { if (configItem.type === 'boolean' || configItem.type === 'list') {
el = document.createElement('select') el=addEl('select',clazz,frame);
el.setAttribute('name', configItem.name) el.setAttribute('name', configItem.name)
let slist = []; let slist = [];
if (configItem.list) { if (configItem.list) {
configItem.list.forEach(function (v) { configItem.list.forEach(function (v) {
if (v instanceof Object){
slist.push({l:v.l,v:v.v});
}
else{
slist.push({ l: v, v: v }); slist.push({ l: v, v: v });
}
}) })
} }
else { else {
@ -246,17 +255,188 @@ function createInput(configItem, frame) {
slist.push({ l: 'off', v: 'false' }) slist.push({ l: 'off', v: 'false' })
} }
slist.forEach(function (sitem) { slist.forEach(function (sitem) {
let sitemEl = document.createElement('option'); let sitemEl = addEl('option','',el,sitem.l);
sitemEl.setAttribute('value', sitem.v); sitemEl.setAttribute('value', sitem.v);
sitemEl.textContent = sitem.l;
el.appendChild(sitemEl);
}) })
frame.appendChild(el);
return el; return el;
} }
if (configItem.type === 'filter') { if (configItem.type === 'filter') {
el = document.createElement('div'); return createFilterInput(configItem,frame,clazz);
el.classList.add('filter'); }
if (configItem.type === 'xdr'){
return createXdrInput(configItem,frame,clazz);
}
el = addEl('input',clazz,frame);
el.setAttribute('name', configItem.name)
if (configItem.type === 'password') {
el.setAttribute('type', 'password');
let vis = addEl('span', 'icon-eye icon', frame);
vis.addEventListener('click', function (v) {
if (vis.classList.toggle('active')) {
el.setAttribute('type', 'text');
}
else {
el.setAttribute('type', 'password');
}
});
}
else if (configItem.type === 'number') {
el.setAttribute('type', 'number');
}
else {
el.setAttribute('type', 'text');
}
return el;
}
function updateSelectList(item,slist){
item.innerHTML='';
slist.forEach(function (sitem) {
let sitemEl = addEl('option','',item,sitem.l);
sitemEl.setAttribute('value', sitem.v);
})
}
function getXdrCategories(){
let rt=[];
for (let c in xdrConfig){
if (xdrConfig[c].enabled !== false){
rt.push({l:c,v:xdrConfig[c].id});
}
}
return rt;
}
function getXdrSelectors(category){
category=parseInt(category);
for (let c in xdrConfig){
let base=xdrConfig[c];
if (parseInt(base.id) == category){
return base.selector || [];
}
}
return [];
}
function getXdrFields(category){
category=parseInt(category);
for (let c in xdrConfig){
let base=xdrConfig[c];
if (parseInt(base.id) == category){
return base.fields || [];
}
}
return [];
}
function createXdrLine(parent,label){
let d=addEl('div','xdrline',parent);
addEl('span','xdrlabel',d,label);
return d;
}
function showHideXdr(el,show,useParent){
if (useParent) el=el.parentElement;
if (show) el.classList.remove('xdrunused');
else el.classList.add('xdrunused');
}
function createXdrInput(configItem,frame){
let el = addEl('div','xdrinput',frame);
let d=createXdrLine(el,'Direction');
let direction=createInput({
type:'list',
name: configItem.name+"_dir",
list: [
//GwXDRMappingDef::Direction
{l:'off',v:0},
{l:'bidir',v:1},
{l:'to2K',v:2},
{l:'from2K',v:3}
]
},d,'xdrdir');
d=createXdrLine(el,'Category');
let category=createInput({
type: 'list',
name: configItem.name+"_cat",
list:getXdrCategories()
},d,'xdrcat');
d=createXdrLine(el,'Source');
let selector=createInput({
type: 'list',
name: configItem.name+"_sel",
list:[]
},d,'xdrsel');
d=createXdrLine(el,'Field');
let field=createInput({
type:'list',
name: configItem.name+'_field',
list: []
},d,'xdrfield');
d=createXdrLine(el,'Instance');
let imode=createInput({
type:'list',
name: configItem.name+"_imode",
list:[
//GwXDRMappingDef::InstanceMode
{l:'single',v:0},
{l:'ignore',v:1},
{l:'auto',v:2}
]
},d,'xdrimode');
let instance=createInput({
type:'number',
name: configItem.name+"_instance",
},d,'xdrinstance');
d=createXdrLine(el,'Transducer');
let xdrName=createInput({
type:'text',
name: configItem.name+"_xdr"
},d,'xdrname');
let data = addEl('input',undefined,el);
data.setAttribute('type', 'hidden');
data.setAttribute('name', configItem.name);
let changeFunction = function () {
let parts=data.value.split(',');
direction.value=parts[1] || 0;
category.value=parts[0] || 0;
let selectors=getXdrSelectors(category.value);
updateSelectList(selector,selectors);
showHideXdr(selector,selectors.length>0);
let fields=getXdrFields(category.value);
updateSelectList(field,fields);
showHideXdr(field,fields.length>0);
selector.value=parts[2]||0;
field.value=parts[3]||0;
imode.value=parts[4]||0;
instance.value=parts[5]||0;
showHideXdr(instance,imode.value == 0);
xdrName.value=parts[6]||'';
}
let updateFunction = function () {
let txt=category.value+","+direction.value+","+
selector.value+","+field.value+","+imode.value;
let instanceValue=parseInt(instance.value||0);
if (isNaN(instanceValue)) instanceValue=0;
if (instanceValue<0) instanceValue=0;
if (instanceValue>255) instanceValue=255;
txt+=","+instanceValue;
let xdr=xdrName.value.replace(/[^a-zA-Z0-9]/g,'');
xdr=xdr.substr(0,12);
txt+=","+xdr;
data.value=txt;
let ev=new Event('change');
data.dispatchEvent(ev);
}
category.addEventListener('change',updateFunction);
direction.addEventListener('change',updateFunction);
selector.addEventListener('change',updateFunction);
field.addEventListener('change',updateFunction);
imode.addEventListener('change',updateFunction);
instance.addEventListener('change',updateFunction);
xdrName.addEventListener('change',updateFunction);
data.addEventListener('change',changeFunction);
return data;
}
function createFilterInput(configItem, frame) {
let el = addEl('div','filter',frame);
let ais = createInput({ let ais = createInput({
type: 'list', type: 'list',
name: configItem.name + "_ais", name: configItem.name + "_ais",
@ -271,9 +451,8 @@ function createInput(configItem, frame) {
type: 'text', type: 'text',
name: configItem.name + "_sentences", name: configItem.name + "_sentences",
}, el); }, el);
let data = document.createElement('input'); let data = addEl('input',undefined,el);
data.setAttribute('type', 'hidden'); data.setAttribute('type', 'hidden');
el.appendChild(data);
let changeFunction = function () { let changeFunction = function () {
let cv = data.value || ""; let cv = data.value || "";
let parts = cv.split(":"); let parts = cv.split(":");
@ -298,46 +477,19 @@ function createInput(configItem, frame) {
changeFunction(); changeFunction();
}); });
data.setAttribute('name', configItem.name); data.setAttribute('name', configItem.name);
frame.appendChild(el);
return data; return data;
} }
el = document.createElement('input');
el.setAttribute('name', configItem.name) function createConfigDefinitions(parent, capabilities, defs,includeXdr) {
frame.appendChild(el);
if (configItem.type === 'password') {
el.setAttribute('type', 'password');
let vis = addEl('span', 'icon-eye icon', frame);
vis.addEventListener('click', function (v) {
if (vis.classList.toggle('active')) {
el.setAttribute('type', 'text');
}
else {
el.setAttribute('type', 'password');
}
});
}
else if (configItem.type === 'number') {
el.setAttribute('type', 'number');
}
else {
el.setAttribute('type', 'text');
}
return el;
}
let configDefinitions;
function loadConfigDefinitions() {
getJson("api/capabilities")
.then(function (capabilities) {
getJson("config.json")
.then(function (defs) {
let category; let category;
let categoryEl; let categoryEl;
let frame = document.querySelector('.configFormRows'); let frame = parent.querySelector('.configFormRows');
if (!frame) throw Error("no config form"); if (!frame) throw Error("no config form");
frame.innerHTML = ''; frame.innerHTML = '';
configDefinitions = defs; configDefinitions = defs;
defs.forEach(function (item) { defs.forEach(function (item) {
if (!item.type) return; if (!item.type) return;
if ((item.category === 'xdr') !== includeXdr) return;
if (item.category != category || !categoryEl) { if (item.category != category || !categoryEl) {
let categoryFrame = addEl('div', 'category', frame); let categoryFrame = addEl('div', 'category', frame);
let categoryTitle = addEl('div', 'title', categoryFrame); let categoryTitle = addEl('div', 'title', categoryFrame);
@ -395,9 +547,24 @@ function loadConfigDefinitions() {
showOverlay(item.description); showOverlay(item.description);
}); });
}) })
}
function loadConfigDefinitions() {
getJson("api/capabilities")
.then(function (capabilities) {
getJson("config.json")
.then(function (defs) {
getJson("xdrconfig.json")
.then(function(xdr){
xdrConfig=xdr;
configDefinitions=defs;
let normalConfig=document.getElementById('configPage');
let xdrParent=document.getElementById('xdrPage');
if (normalConfig) createConfigDefinitions(normalConfig,capabilities,defs,false);
if (xdrParent) createConfigDefinitions(xdrParent,capabilities,defs,true);
resetForm(); resetForm();
}) })
}) })
})
.catch(function (err) { alert("unable to load config: " + err) }) .catch(function (err) { alert("unable to load config: " + err) })
} }
function converterInfo() { function converterInfo() {