Merge branch 'wellenvogel:master' into master
This commit is contained in:
commit
96dfd9e773
|
@ -10,6 +10,8 @@ jobs:
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
env:
|
||||||
|
PIP_BREAK_SYSTEM_PACKAGES: 1
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -18,6 +18,9 @@ jobs:
|
||||||
# The type of runner that the job will run on
|
# The type of runner that the job will run on
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
PIP_BREAK_SYSTEM_PACKAGES: 1
|
||||||
|
|
||||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||||
steps:
|
steps:
|
||||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -18,6 +18,8 @@ OWN_FILE="extra_script.py"
|
||||||
GEN_DIR='lib/generated'
|
GEN_DIR='lib/generated'
|
||||||
CFG_FILE='web/config.json'
|
CFG_FILE='web/config.json'
|
||||||
XDR_FILE='web/xdrconfig.json'
|
XDR_FILE='web/xdrconfig.json'
|
||||||
|
INDEXJS="index.js"
|
||||||
|
INDEXCSS="index.css"
|
||||||
CFG_INCLUDE='GwConfigDefinitions.h'
|
CFG_INCLUDE='GwConfigDefinitions.h'
|
||||||
CFG_INCLUDE_IMPL='GwConfigDefImpl.h'
|
CFG_INCLUDE_IMPL='GwConfigDefImpl.h'
|
||||||
XDR_INCLUDE='GwXdrTypeMappings.h'
|
XDR_INCLUDE='GwXdrTypeMappings.h'
|
||||||
|
@ -66,6 +68,7 @@ def isCurrent(infile,outfile):
|
||||||
def compressFile(inFile,outfile):
|
def compressFile(inFile,outfile):
|
||||||
if isCurrent(inFile,outfile):
|
if isCurrent(inFile,outfile):
|
||||||
return
|
return
|
||||||
|
print("compressing %s"%inFile)
|
||||||
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)
|
||||||
|
@ -372,6 +375,16 @@ def getLibs():
|
||||||
rt.append(e)
|
rt.append(e)
|
||||||
return rt
|
return rt
|
||||||
|
|
||||||
|
def joinFiles(target,pattern,dirlist):
|
||||||
|
with gzip.open(target,"wb") as oh:
|
||||||
|
for dir in dirlist:
|
||||||
|
fn=os.path.join(dir,pattern)
|
||||||
|
if os.path.exists(fn):
|
||||||
|
print("adding %s to %s"%(fn,target))
|
||||||
|
with open(fn,"rb") as rh:
|
||||||
|
shutil.copyfileobj(rh,oh)
|
||||||
|
|
||||||
|
|
||||||
OWNLIBS=getLibs()+["FS","WiFi"]
|
OWNLIBS=getLibs()+["FS","WiFi"]
|
||||||
GLOBAL_INCLUDES=[]
|
GLOBAL_INCLUDES=[]
|
||||||
|
|
||||||
|
@ -440,6 +453,8 @@ def prebuild(env):
|
||||||
compressFile(mergedConfig,mergedConfig+".gz")
|
compressFile(mergedConfig,mergedConfig+".gz")
|
||||||
generateCfg(mergedConfig,os.path.join(outPath(),CFG_INCLUDE),False)
|
generateCfg(mergedConfig,os.path.join(outPath(),CFG_INCLUDE),False)
|
||||||
generateCfg(mergedConfig,os.path.join(outPath(),CFG_INCLUDE_IMPL),True)
|
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)
|
embedded=getEmbeddedFiles(env)
|
||||||
filedefs=[]
|
filedefs=[]
|
||||||
for ef in embedded:
|
for ef in embedded:
|
||||||
|
@ -453,7 +468,6 @@ def prebuild(env):
|
||||||
filedefs.append((pureName,usname,ct))
|
filedefs.append((pureName,usname,ct))
|
||||||
inFile=os.path.join(basePath(),"web",pureName)
|
inFile=os.path.join(basePath(),"web",pureName)
|
||||||
if os.path.exists(inFile):
|
if os.path.exists(inFile):
|
||||||
print("compressing %s"%inFile)
|
|
||||||
compressFile(inFile,ef)
|
compressFile(inFile,ef)
|
||||||
else:
|
else:
|
||||||
print("#WARNING: infile %s for %s not found"%(inFile,ef))
|
print("#WARNING: infile %s for %s not found"%(inFile,ef))
|
||||||
|
|
|
@ -47,7 +47,6 @@ GwBoatItemBase::GwBoatItemBase(String name, String format, GwBoatItemBase::TOTyp
|
||||||
this->format = format;
|
this->format = format;
|
||||||
this->type = 0;
|
this->type = 0;
|
||||||
this->lastUpdateSource = -1;
|
this->lastUpdateSource = -1;
|
||||||
this->toType=TOType::user;
|
|
||||||
}
|
}
|
||||||
void GwBoatItemBase::setInvalidTime(unsigned long it, bool force){
|
void GwBoatItemBase::setInvalidTime(unsigned long it, bool force){
|
||||||
if (toType != TOType::user || force ){
|
if (toType != TOType::user || force ){
|
||||||
|
@ -375,7 +374,7 @@ GwBoatItem<T> *GwBoatData::getOrCreate(T initial, GwBoatItemNameProvider *provid
|
||||||
provider->getBoatItemFormat(),
|
provider->getBoatItemFormat(),
|
||||||
provider->getInvalidTime(),
|
provider->getInvalidTime(),
|
||||||
&values);
|
&values);
|
||||||
rt->update(initial);
|
rt->update(initial,-1);
|
||||||
LOG_DEBUG(GwLog::LOG, "creating boatItem %s, type %d",
|
LOG_DEBUG(GwLog::LOG, "creating boatItem %s, type %d",
|
||||||
name.c_str(), rt->getCurrentType());
|
name.c_str(), rt->getCurrentType());
|
||||||
return rt;
|
return rt;
|
||||||
|
|
|
@ -105,8 +105,8 @@ template<class T> class GwBoatItem : public GwBoatItemBase{
|
||||||
GwBoatItem(String name,String formatInfo,unsigned long invalidTime=INVALID_TIME,GwBoatItemMap *map=NULL);
|
GwBoatItem(String name,String formatInfo,unsigned long invalidTime=INVALID_TIME,GwBoatItemMap *map=NULL);
|
||||||
GwBoatItem(String name,String formatInfo,TOType toType,GwBoatItemMap *map=NULL);
|
GwBoatItem(String name,String formatInfo,TOType toType,GwBoatItemMap *map=NULL);
|
||||||
virtual ~GwBoatItem(){}
|
virtual ~GwBoatItem(){}
|
||||||
bool update(T nv, int source=-1);
|
bool update(T nv, int source);
|
||||||
bool updateMax(T nv,int sourceId=-1);
|
bool updateMax(T nv,int sourceId);
|
||||||
T getData(){
|
T getData(){
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
@ -185,7 +185,6 @@ public:
|
||||||
#define GWSPECBOATDATA(clazz,name,toType,fmt) \
|
#define GWSPECBOATDATA(clazz,name,toType,fmt) \
|
||||||
clazz *name=new clazz(#name,GwBoatItemBase::fmt,toType,&values) ;
|
clazz *name=new clazz(#name,GwBoatItemBase::fmt,toType,&values) ;
|
||||||
class GwBoatData{
|
class GwBoatData{
|
||||||
static const unsigned long DEF_TIME=4000;
|
|
||||||
private:
|
private:
|
||||||
GwLog *logger;
|
GwLog *logger;
|
||||||
GwBoatItemBase::GwBoatItemMap values;
|
GwBoatItemBase::GwBoatItemMap values;
|
||||||
|
|
|
@ -16,16 +16,63 @@
|
||||||
#define _GWCONVERTERCONFIG_H
|
#define _GWCONVERTERCONFIG_H
|
||||||
|
|
||||||
#include "GWConfig.h"
|
#include "GWConfig.h"
|
||||||
|
#include "N2kTypes.h"
|
||||||
|
#include <map>
|
||||||
|
|
||||||
|
//list of configs for the PGN 130306 wind references
|
||||||
|
static std::map<tN2kWindReference,String> windConfigs={
|
||||||
|
{N2kWind_True_water,GwConfigDefinitions::windmtra},
|
||||||
|
{N2kWind_Apparent,GwConfigDefinitions::windmawa},
|
||||||
|
{N2kWind_True_boat,GwConfigDefinitions::windmgna},
|
||||||
|
{N2kWind_Magnetic,GwConfigDefinitions::windmmgd},
|
||||||
|
{N2kWind_True_North,GwConfigDefinitions::windmtng},
|
||||||
|
};
|
||||||
|
|
||||||
class GwConverterConfig{
|
class GwConverterConfig{
|
||||||
public:
|
public:
|
||||||
|
class WindMapping{
|
||||||
|
public:
|
||||||
|
using Wind0183Type=enum{
|
||||||
|
AWA_AWS,
|
||||||
|
TWA_TWS,
|
||||||
|
TWD_TWS,
|
||||||
|
GWA_GWS,
|
||||||
|
GWD_GWS
|
||||||
|
};
|
||||||
|
tN2kWindReference n2kType;
|
||||||
|
Wind0183Type nmea0183Type;
|
||||||
|
bool valid=false;
|
||||||
|
WindMapping(){}
|
||||||
|
WindMapping(const tN2kWindReference &n2k,const Wind0183Type &n183):
|
||||||
|
n2kType(n2k),nmea0183Type(n183),valid(true){}
|
||||||
|
WindMapping(const tN2kWindReference &n2k,const String &n183):
|
||||||
|
n2kType(n2k){
|
||||||
|
if (n183 == "twa_tws"){
|
||||||
|
nmea0183Type=TWA_TWS;
|
||||||
|
valid=true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (n183 == "awa_aws"){
|
||||||
|
nmea0183Type=AWA_AWS;
|
||||||
|
valid=true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (n183 == "twd_tws"){
|
||||||
|
nmea0183Type=TWD_TWS;
|
||||||
|
valid=true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
int minXdrInterval=100;
|
int minXdrInterval=100;
|
||||||
int starboardRudderInstance=0;
|
int starboardRudderInstance=0;
|
||||||
int portRudderInstance=-1; //ignore
|
int portRudderInstance=-1; //ignore
|
||||||
int min2KInterval=50;
|
int min2KInterval=50;
|
||||||
int rmcInterval=1000;
|
int rmcInterval=1000;
|
||||||
int rmcCheckTime=4000;
|
int rmcCheckTime=4000;
|
||||||
void init(GwConfigHandler *config){
|
int winst312=256;
|
||||||
|
std::vector<WindMapping> windMappings;
|
||||||
|
void init(GwConfigHandler *config, GwLog*logger){
|
||||||
minXdrInterval=config->getInt(GwConfigDefinitions::minXdrInterval,100);
|
minXdrInterval=config->getInt(GwConfigDefinitions::minXdrInterval,100);
|
||||||
starboardRudderInstance=config->getInt(GwConfigDefinitions::stbRudderI,0);
|
starboardRudderInstance=config->getInt(GwConfigDefinitions::stbRudderI,0);
|
||||||
portRudderInstance=config->getInt(GwConfigDefinitions::portRudderI,-1);
|
portRudderInstance=config->getInt(GwConfigDefinitions::portRudderI,-1);
|
||||||
|
@ -36,6 +83,30 @@ class GwConverterConfig{
|
||||||
rmcInterval=config->getInt(GwConfigDefinitions::sendRMCi,1000);
|
rmcInterval=config->getInt(GwConfigDefinitions::sendRMCi,1000);
|
||||||
if (rmcInterval < 0) rmcInterval=0;
|
if (rmcInterval < 0) rmcInterval=0;
|
||||||
if (rmcInterval > 0 && rmcInterval <100) rmcInterval=100;
|
if (rmcInterval > 0 && rmcInterval <100) rmcInterval=100;
|
||||||
|
winst312=config->getInt(GwConfigDefinitions::winst312,256);
|
||||||
|
for (auto && it:windConfigs){
|
||||||
|
String cfg=config->getString(it.second);
|
||||||
|
WindMapping mapping(it.first,cfg);
|
||||||
|
if (mapping.valid){
|
||||||
|
LOG_DEBUG(GwLog::ERROR,"add wind mapping n2k=%d,nmea0183=%01d(%s)",
|
||||||
|
(int)(mapping.n2kType),(int)(mapping.nmea0183Type),cfg.c_str());
|
||||||
|
windMappings.push_back(mapping);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const WindMapping findWindMapping(const tN2kWindReference &n2k) const{
|
||||||
|
for (const auto & it:windMappings){
|
||||||
|
if (it.n2kType == n2k) return it;
|
||||||
|
}
|
||||||
|
return WindMapping();
|
||||||
|
}
|
||||||
|
const WindMapping findWindMapping(const WindMapping::Wind0183Type &n183) const{
|
||||||
|
for (const auto & it:windMappings){
|
||||||
|
if (it.nmea0183Type == n183) return it;
|
||||||
|
}
|
||||||
|
return WindMapping();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
#endif
|
#endif
|
|
@ -0,0 +1,3 @@
|
||||||
|
.examplecss{
|
||||||
|
background-color: coral;
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
(function(){
|
||||||
|
const api=window.esp32nmea2k;
|
||||||
|
if (! api) return;
|
||||||
|
//we only do something if a special capability is set
|
||||||
|
//on our case this is "testboard"
|
||||||
|
//so we only start any action when we receive the init event
|
||||||
|
//and we successfully checked that our requested capability is there
|
||||||
|
let isActive=false;
|
||||||
|
const tabName="example";
|
||||||
|
const configName="exampleBDSel";
|
||||||
|
const infoUrl='https://github.com/wellenvogel/esp32-nmea2000/tree/master/lib/exampletask';
|
||||||
|
let boatItemName;
|
||||||
|
let boatItemElement;
|
||||||
|
api.registerListener((id,data)=>{
|
||||||
|
if (id === api.EVENTS.init){
|
||||||
|
//data is capabilities
|
||||||
|
//check if our requested capability is there (see GwExampleTask.h)
|
||||||
|
if (data.testboard) isActive=true;
|
||||||
|
if (isActive){
|
||||||
|
//add a simple additional tab page
|
||||||
|
//you will have to build the content of the page dynamically
|
||||||
|
//using normal dom manipulation methods
|
||||||
|
//you can use the helper addEl to create elements
|
||||||
|
let page=api.addTabPage(tabName,"Example");
|
||||||
|
api.addEl('div','hdg',page,"this is a test tab");
|
||||||
|
api.addEl('button','',page,'Info').addEventListener('click',function(ev){
|
||||||
|
window.open(infoUrl,'info');
|
||||||
|
})
|
||||||
|
//add a tab for an external URL
|
||||||
|
api.addTabPage('exhelp','Info',infoUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isActive){
|
||||||
|
//console.log("exampletask listener",id,data);
|
||||||
|
if (id === api.EVENTS.tab){
|
||||||
|
if (data === tabName){
|
||||||
|
//maybe we need some activity when our page is being activated
|
||||||
|
console.log("example tab activated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (id == api.EVENTS.config){
|
||||||
|
//we have a configuration that
|
||||||
|
//gives us the name of a boat data item we would like to
|
||||||
|
//handle special
|
||||||
|
//in our case we just use an own formatter and add some
|
||||||
|
//css to the display field
|
||||||
|
//as this item can change we need to keep track of the
|
||||||
|
//last item we handled
|
||||||
|
let nextboatItemName=data[configName];
|
||||||
|
console.log("value of "+configName,nextboatItemName);
|
||||||
|
if (nextboatItemName){
|
||||||
|
//register a user formatter that will be called whenever
|
||||||
|
//there is a new valid value
|
||||||
|
//we simply add an "X:" in front
|
||||||
|
api.addUserFormatter(nextboatItemName,"m(x)",function(v,valid){
|
||||||
|
if (!valid) return;
|
||||||
|
return "X:"+v;
|
||||||
|
})
|
||||||
|
//after this call the item will be recreated
|
||||||
|
}
|
||||||
|
if (boatItemName !== undefined && boatItemName != nextboatItemName){
|
||||||
|
//if the boat item that we handle has changed, remove
|
||||||
|
//the previous user formatter (this will recreate the item)
|
||||||
|
api.removeUserFormatter(boatItemName);
|
||||||
|
}
|
||||||
|
boatItemName=nextboatItemName;
|
||||||
|
boatItemElement=undefined;
|
||||||
|
}
|
||||||
|
if (id == api.EVENTS.dataItemCreated){
|
||||||
|
//this event is called whenever a data item has
|
||||||
|
//been created (or recreated)
|
||||||
|
//if this is the item we handle, we just add a css class
|
||||||
|
//we could also completely rebuild the dom below the element
|
||||||
|
//and use our formatter to directly write/draw the data
|
||||||
|
//avoid direct manipulation of the element (i.e. changing the classlist)
|
||||||
|
//as this element remains there all the time
|
||||||
|
if (boatItemName && boatItemName == data.name){
|
||||||
|
boatItemElement=data.element;
|
||||||
|
//use the helper forEl to find elements within the dashboard item
|
||||||
|
//the value element has the class "dashValue"
|
||||||
|
api.forEl(".dashValue",function(el){
|
||||||
|
el.classList.add("examplecss");
|
||||||
|
},boatItemElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})();
|
|
@ -103,7 +103,7 @@ private:
|
||||||
if (v != NMEA0183UInt32NA){
|
if (v != NMEA0183UInt32NA){
|
||||||
return target->update(v,sourceId);
|
return target->update(v,sourceId);
|
||||||
}
|
}
|
||||||
return v;
|
return false;
|
||||||
}
|
}
|
||||||
uint32_t getUint32(GwBoatItem<uint32_t> *src){
|
uint32_t getUint32(GwBoatItem<uint32_t> *src){
|
||||||
return src->getDataWithDefault(N2kUInt32NA);
|
return src->getDataWithDefault(N2kUInt32NA);
|
||||||
|
@ -399,28 +399,29 @@ private:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
tN2kMsg n2kMsg;
|
tN2kMsg n2kMsg;
|
||||||
tN2kWindReference n2kRef;
|
|
||||||
bool shouldSend=false;
|
bool shouldSend=false;
|
||||||
WindAngle=formatDegToRad(WindAngle);
|
WindAngle=formatDegToRad(WindAngle);
|
||||||
|
GwConverterConfig::WindMapping mapping;
|
||||||
switch(Reference){
|
switch(Reference){
|
||||||
case NMEA0183Wind_Apparent:
|
case NMEA0183Wind_Apparent:
|
||||||
n2kRef=N2kWind_Apparent;
|
|
||||||
shouldSend=updateDouble(boatData->AWA,WindAngle,msg.sourceId) &&
|
shouldSend=updateDouble(boatData->AWA,WindAngle,msg.sourceId) &&
|
||||||
updateDouble(boatData->AWS,WindSpeed,msg.sourceId);
|
updateDouble(boatData->AWS,WindSpeed,msg.sourceId);
|
||||||
if (WindSpeed != NMEA0183DoubleNA) boatData->MaxAws->updateMax(WindSpeed);
|
if (WindSpeed != NMEA0183DoubleNA) boatData->MaxAws->updateMax(WindSpeed,msg.sourceId);
|
||||||
|
mapping=config.findWindMapping(GwConverterConfig::WindMapping::AWA_AWS);
|
||||||
break;
|
break;
|
||||||
case NMEA0183Wind_True:
|
case NMEA0183Wind_True:
|
||||||
n2kRef=N2kWind_True_water;
|
|
||||||
shouldSend=updateDouble(boatData->TWA,WindAngle,msg.sourceId) &&
|
shouldSend=updateDouble(boatData->TWA,WindAngle,msg.sourceId) &&
|
||||||
updateDouble(boatData->TWS,WindSpeed,msg.sourceId);
|
updateDouble(boatData->TWS,WindSpeed,msg.sourceId);
|
||||||
if (WindSpeed != NMEA0183DoubleNA) boatData->MaxTws->updateMax(WindSpeed);
|
if (WindSpeed != NMEA0183DoubleNA) boatData->MaxTws->updateMax(WindSpeed,msg.sourceId);
|
||||||
|
mapping=config.findWindMapping(GwConverterConfig::WindMapping::TWA_TWS);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
LOG_DEBUG(GwLog::DEBUG,"unknown wind reference %d in %s",(int)Reference,msg.line);
|
LOG_DEBUG(GwLog::DEBUG,"unknown wind reference %d in %s",(int)Reference,msg.line);
|
||||||
}
|
}
|
||||||
if (shouldSend){
|
//TODO: try to compute TWD and get mapping for this one
|
||||||
SetN2kWindSpeed(n2kMsg,1,WindSpeed,WindAngle,n2kRef);
|
if (shouldSend && mapping.valid){
|
||||||
send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((int)n2kRef));
|
SetN2kWindSpeed(n2kMsg,1,WindSpeed,WindAngle,mapping.n2kType);
|
||||||
|
send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((int)mapping.n2kType));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void convertVWR(const SNMEA0183Msg &msg)
|
void convertVWR(const SNMEA0183Msg &msg)
|
||||||
|
@ -457,11 +458,14 @@ private:
|
||||||
bool shouldSend = false;
|
bool shouldSend = false;
|
||||||
shouldSend = updateDouble(boatData->AWA, WindAngle, msg.sourceId) &&
|
shouldSend = updateDouble(boatData->AWA, WindAngle, msg.sourceId) &&
|
||||||
updateDouble(boatData->AWS, WindSpeed, msg.sourceId);
|
updateDouble(boatData->AWS, WindSpeed, msg.sourceId);
|
||||||
if (WindSpeed != NMEA0183DoubleNA) boatData->MaxAws->updateMax(WindSpeed);
|
if (WindSpeed != NMEA0183DoubleNA) boatData->MaxAws->updateMax(WindSpeed,msg.sourceId);
|
||||||
if (shouldSend)
|
if (shouldSend)
|
||||||
{
|
{
|
||||||
SetN2kWindSpeed(n2kMsg, 1, WindSpeed, WindAngle, N2kWind_Apparent);
|
const GwConverterConfig::WindMapping mapping=config.findWindMapping(GwConverterConfig::WindMapping::AWA_AWS);
|
||||||
send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((int)N2kWind_Apparent));
|
if (mapping.valid){
|
||||||
|
SetN2kWindSpeed(n2kMsg, 1, WindSpeed, WindAngle, mapping.n2kType);
|
||||||
|
send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((int)mapping.n2kType));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -499,13 +503,21 @@ private:
|
||||||
if (WindDirection != NMEA0183DoubleNA){
|
if (WindDirection != NMEA0183DoubleNA){
|
||||||
shouldSend = updateDouble(boatData->TWD, WindDirection, msg.sourceId) &&
|
shouldSend = updateDouble(boatData->TWD, WindDirection, msg.sourceId) &&
|
||||||
updateDouble(boatData->TWS, WindSpeed, msg.sourceId);
|
updateDouble(boatData->TWS, WindSpeed, msg.sourceId);
|
||||||
if (WindSpeed != NMEA0183DoubleNA) boatData->MaxTws->updateMax(WindSpeed);
|
if (WindSpeed != NMEA0183DoubleNA) boatData->MaxTws->updateMax(WindSpeed,msg.sourceId);
|
||||||
if(shouldSend && boatData->HDT->isValid()) {
|
if(shouldSend && boatData->HDT->isValid()) {
|
||||||
double twa = WindDirection-boatData->HDT->getData();
|
double twa = WindDirection-boatData->HDT->getData();
|
||||||
if(twa<0) { twa+=2*M_PI; }
|
if(twa<0) { twa+=2*M_PI; }
|
||||||
updateDouble(boatData->TWA, twa, msg.sourceId);
|
updateDouble(boatData->TWA, twa, msg.sourceId);
|
||||||
SetN2kWindSpeed(n2kMsg, 1, WindSpeed, twa, N2kWind_True_water);
|
const GwConverterConfig::WindMapping mapping=config.findWindMapping(GwConverterConfig::WindMapping::TWA_TWS);
|
||||||
send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((int)N2kWind_True_water));
|
if (mapping.valid){
|
||||||
|
SetN2kWindSpeed(n2kMsg, 1, WindSpeed, twa, mapping.n2kType);
|
||||||
|
send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((int)mapping.n2kType));
|
||||||
|
}
|
||||||
|
const GwConverterConfig::WindMapping mapping2=config.findWindMapping(GwConverterConfig::WindMapping::TWD_TWS);
|
||||||
|
if (mapping2.valid){
|
||||||
|
SetN2kWindSpeed(n2kMsg, 1, WindSpeed, WindDirection, mapping2.n2kType);
|
||||||
|
send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((int)mapping2.n2kType));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -590,10 +602,10 @@ private:
|
||||||
}
|
}
|
||||||
//offset == 0? SK does not allow this
|
//offset == 0? SK does not allow this
|
||||||
if (Offset != NMEA0183DoubleNA && Offset>=0 ){
|
if (Offset != NMEA0183DoubleNA && Offset>=0 ){
|
||||||
if (! boatData->DBS->update(DepthBelowTransducer+Offset)) return;
|
if (! boatData->DBS->update(DepthBelowTransducer+Offset,msg.sourceId)) return;
|
||||||
}
|
}
|
||||||
if (Offset == NMEA0183DoubleNA) Offset=N2kDoubleNA;
|
if (Offset == NMEA0183DoubleNA) Offset=N2kDoubleNA;
|
||||||
if (! boatData->DBT->update(DepthBelowTransducer)) return;
|
if (! boatData->DBT->update(DepthBelowTransducer,msg.sourceId)) return;
|
||||||
tN2kMsg n2kMsg;
|
tN2kMsg n2kMsg;
|
||||||
SetN2kWaterDepth(n2kMsg,1,DepthBelowTransducer,Offset);
|
SetN2kWaterDepth(n2kMsg,1,DepthBelowTransducer,Offset);
|
||||||
send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((Offset != N2kDoubleNA)?1:0));
|
send(n2kMsg,msg.sourceId,String(n2kMsg.PGN)+String((Offset != N2kDoubleNA)?1:0));
|
||||||
|
|
|
@ -469,39 +469,66 @@ private:
|
||||||
unsigned char SID;
|
unsigned char SID;
|
||||||
tN2kWindReference WindReference;
|
tN2kWindReference WindReference;
|
||||||
double WindAngle=N2kDoubleNA, WindSpeed=N2kDoubleNA;
|
double WindAngle=N2kDoubleNA, WindSpeed=N2kDoubleNA;
|
||||||
|
tNMEA0183WindReference NMEA0183Reference;
|
||||||
if (ParseN2kWindSpeed(N2kMsg, SID, WindSpeed, WindAngle, WindReference)) {
|
if (ParseN2kWindSpeed(N2kMsg, SID, WindSpeed, WindAngle, WindReference)) {
|
||||||
tNMEA0183Msg NMEA0183Msg;
|
tNMEA0183Msg NMEA0183Msg;
|
||||||
tNMEA0183WindReference NMEA0183Reference;
|
GwConverterConfig::WindMapping mapping=config.findWindMapping(WindReference);
|
||||||
bool shouldSend = false;
|
bool shouldSend = false;
|
||||||
|
|
||||||
// MWV sentence contains apparent/true ANGLE and SPEED
|
// MWV sentence contains apparent/true ANGLE and SPEED
|
||||||
// https://gpsd.gitlab.io/gpsd/NMEA.html#_mwv_wind_speed_and_angle
|
// https://gpsd.gitlab.io/gpsd/NMEA.html#_mwv_wind_speed_and_angle
|
||||||
// https://docs.vaisala.com/r/M211109EN-L/en-US/GUID-7402DEF8-5E82-446F-B63E-998F49F3D743/GUID-C77934C7-2A72-466E-BC52-CE6B8CC7ACB6
|
// https://docs.vaisala.com/r/M211109EN-L/en-US/GUID-7402DEF8-5E82-446F-B63E-998F49F3D743/GUID-C77934C7-2A72-466E-BC52-CE6B8CC7ACB6
|
||||||
|
if (mapping.valid)
|
||||||
if (WindReference == N2kWind_Apparent) {
|
{
|
||||||
|
if (mapping.nmea0183Type == GwConverterConfig::WindMapping::AWA_AWS)
|
||||||
|
{
|
||||||
NMEA0183Reference = NMEA0183Wind_Apparent;
|
NMEA0183Reference = NMEA0183Wind_Apparent;
|
||||||
updateDouble(boatData->AWA, WindAngle);
|
updateDouble(boatData->AWA, WindAngle);
|
||||||
updateDouble(boatData->AWS, WindSpeed);
|
updateDouble(boatData->AWS, WindSpeed);
|
||||||
setMax(boatData->MaxAws, boatData->AWS);
|
setMax(boatData->MaxAws, boatData->AWS);
|
||||||
shouldSend = true;
|
shouldSend = true;
|
||||||
}
|
}
|
||||||
if (WindReference == N2kWind_True_water) {
|
if (mapping.nmea0183Type == GwConverterConfig::WindMapping::TWA_TWS)
|
||||||
|
{
|
||||||
NMEA0183Reference = NMEA0183Wind_True;
|
NMEA0183Reference = NMEA0183Wind_True;
|
||||||
updateDouble(boatData->TWA, WindAngle);
|
updateDouble(boatData->TWA, WindAngle);
|
||||||
updateDouble(boatData->TWS, WindSpeed);
|
updateDouble(boatData->TWS, WindSpeed);
|
||||||
setMax(boatData->MaxTws, boatData->TWS);
|
setMax(boatData->MaxTws, boatData->TWS);
|
||||||
shouldSend = true;
|
shouldSend = true;
|
||||||
if (boatData->HDT->isValid()) {
|
if (boatData->HDT->isValid())
|
||||||
|
{
|
||||||
double twd = WindAngle + boatData->HDT->getData();
|
double twd = WindAngle + boatData->HDT->getData();
|
||||||
if (twd>2*M_PI) { twd-=2*M_PI; }
|
if (twd > 2 * M_PI)
|
||||||
|
{
|
||||||
|
twd -= 2 * M_PI;
|
||||||
|
}
|
||||||
updateDouble(boatData->TWD, twd);
|
updateDouble(boatData->TWD, twd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (mapping.nmea0183Type == GwConverterConfig::WindMapping::TWD_TWS)
|
||||||
|
{
|
||||||
|
NMEA0183Reference = NMEA0183Wind_True;
|
||||||
|
updateDouble(boatData->TWD, WindAngle);
|
||||||
|
updateDouble(boatData->TWS, WindSpeed);
|
||||||
|
setMax(boatData->MaxTws, boatData->TWS);
|
||||||
|
if (boatData->HDT->isValid())
|
||||||
|
{
|
||||||
|
shouldSend = true;
|
||||||
|
double twa = WindAngle - boatData->HDT->getData();
|
||||||
|
if (twa > 2 * M_PI)
|
||||||
|
{
|
||||||
|
twa -= 2 * M_PI;
|
||||||
|
}
|
||||||
|
updateDouble(boatData->TWA, twa);
|
||||||
|
WindAngle=twa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (shouldSend && NMEA0183SetMWV(NMEA0183Msg, formatCourse(WindAngle), NMEA0183Reference, WindSpeed, talkerId)) {
|
if (shouldSend && NMEA0183SetMWV(NMEA0183Msg, formatCourse(WindAngle), NMEA0183Reference, WindSpeed, talkerId))
|
||||||
|
{
|
||||||
SendMessage(NMEA0183Msg);
|
SendMessage(NMEA0183Msg);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* if (WindReference == N2kWind_Apparent && boatData->SOG->isValid())
|
/* if (WindReference == N2kWind_Apparent && boatData->SOG->isValid())
|
||||||
{ // Lets calculate and send TWS/TWA if SOG is available
|
{ // Lets calculate and send TWS/TWA if SOG is available
|
||||||
|
@ -1305,6 +1332,20 @@ private:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
int i=0;
|
int i=0;
|
||||||
|
if (TempSource == N2kts_SeaTemperature) {
|
||||||
|
updateDouble(boatData->WTemp, Temperature);
|
||||||
|
tNMEA0183Msg NMEA0183Msg;
|
||||||
|
|
||||||
|
if (!NMEA0183Msg.Init("MTW", talkerId))
|
||||||
|
return;
|
||||||
|
if (!NMEA0183Msg.AddDoubleField(KelvinToC(Temperature)))
|
||||||
|
return;
|
||||||
|
if (!NMEA0183Msg.AddStrField("C"))
|
||||||
|
return;
|
||||||
|
|
||||||
|
SendMessage(NMEA0183Msg);
|
||||||
|
}
|
||||||
|
|
||||||
GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRTEMP,TempSource,0,0);
|
GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRTEMP,TempSource,0,0);
|
||||||
if (updateDouble(&mapping,Temperature)){
|
if (updateDouble(&mapping,Temperature)){
|
||||||
LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str());
|
LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str());
|
||||||
|
@ -1337,6 +1378,21 @@ private:
|
||||||
LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN);
|
LOG_DEBUG(GwLog::DEBUG,"unable to parse PGN %d",msg.PGN);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (TemperatureSource == N2kts_SeaTemperature &&
|
||||||
|
(config.winst312 == TemperatureInstance || config.winst312 == 256)) {
|
||||||
|
updateDouble(boatData->WTemp, Temperature);
|
||||||
|
tNMEA0183Msg NMEA0183Msg;
|
||||||
|
|
||||||
|
if (!NMEA0183Msg.Init("MTW", talkerId))
|
||||||
|
return;
|
||||||
|
if (!NMEA0183Msg.AddDoubleField(KelvinToC(Temperature)))
|
||||||
|
return;
|
||||||
|
if (!NMEA0183Msg.AddStrField("C"))
|
||||||
|
return;
|
||||||
|
|
||||||
|
SendMessage(NMEA0183Msg);
|
||||||
|
}
|
||||||
|
|
||||||
GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRTEMP,(int)TemperatureSource,0,TemperatureInstance);
|
GwXDRFoundMapping mapping=xdrMappings->getMapping(XDRTEMP,(int)TemperatureSource,0,TemperatureInstance);
|
||||||
if (updateDouble(&mapping,Temperature)){
|
if (updateDouble(&mapping,Temperature)){
|
||||||
LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str());
|
LOG_DEBUG(GwLog::DEBUG+1,"found temperature mapping %s",mapping.definition->toString().c_str());
|
||||||
|
|
|
@ -850,7 +850,7 @@ void setup() {
|
||||||
xdrMappings.begin();
|
xdrMappings.begin();
|
||||||
logger.flush();
|
logger.flush();
|
||||||
GwConverterConfig converterConfig;
|
GwConverterConfig converterConfig;
|
||||||
converterConfig.init(&config);
|
converterConfig.init(&config,&logger);
|
||||||
nmea0183Converter= N2kDataToNMEA0183::create(&logger, &boatData,
|
nmea0183Converter= N2kDataToNMEA0183::create(&logger, &boatData,
|
||||||
[](const tNMEA0183Msg &msg, int sourceId){
|
[](const tNMEA0183Msg &msg, int sourceId){
|
||||||
SendNMEA0183Message(msg,sourceId,false);
|
SendNMEA0183Message(msg,sourceId,false);
|
||||||
|
|
|
@ -56,7 +56,16 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
if not self.do_proxy():
|
if not self.do_proxy():
|
||||||
super().do_POST()
|
super().do_POST()
|
||||||
|
def guess_type(self,path):
|
||||||
|
if path.endswith('.gz'):
|
||||||
|
return super().guess_type(path[0:-3])
|
||||||
|
return super().guess_type(path)
|
||||||
|
def end_headers(self):
|
||||||
|
if hasattr(self,"isgz") and self.isgz:
|
||||||
|
self.send_header("Content-Encoding","gzip")
|
||||||
|
super().end_headers()
|
||||||
def translate_path(self, path):
|
def translate_path(self, path):
|
||||||
|
self.isgz=False
|
||||||
"""Translate a /-separated PATH to the local filename syntax.
|
"""Translate a /-separated PATH to the local filename syntax.
|
||||||
|
|
||||||
Components that mean special things to the local file system
|
Components that mean special things to the local file system
|
||||||
|
@ -90,6 +99,9 @@ class RequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||||
rpath += '/'
|
rpath += '/'
|
||||||
if os.path.exists(rpath):
|
if os.path.exists(rpath):
|
||||||
return rpath
|
return rpath
|
||||||
|
if os.path.exists(rpath+".gz"):
|
||||||
|
self.isgz=True
|
||||||
|
return rpath+".gz"
|
||||||
if isSecond:
|
if isSecond:
|
||||||
return rpath
|
return rpath
|
||||||
isSecond=True
|
isSecond=True
|
||||||
|
|
109
web/config.json
109
web/config.json
|
@ -228,28 +228,6 @@
|
||||||
"check": "checkMinMax",
|
"check": "checkMinMax",
|
||||||
"category": "converter"
|
"category": "converter"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "stbRudderI",
|
|
||||||
"label":"stb rudder instance",
|
|
||||||
"type": "number",
|
|
||||||
"default": "0",
|
|
||||||
"check": "checkMinMax",
|
|
||||||
"min": 0,
|
|
||||||
"max": 253,
|
|
||||||
"description": "the n2k instance to be used as starboard(main) rudder 0...253",
|
|
||||||
"category": "converter"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "portRudderI",
|
|
||||||
"label":"port rudder instance",
|
|
||||||
"type": "number",
|
|
||||||
"default": "-1",
|
|
||||||
"check": "checkMinMax",
|
|
||||||
"min": -1,
|
|
||||||
"max": 253,
|
|
||||||
"description": "the n2k instance to be used as port rudder 0...253, -1 to disable",
|
|
||||||
"category": "converter"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "timeouts",
|
"name": "timeouts",
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
@ -290,6 +268,93 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "stbRudderI",
|
||||||
|
"label":"stb rudder instance",
|
||||||
|
"type": "number",
|
||||||
|
"default": "0",
|
||||||
|
"check": "checkMinMax",
|
||||||
|
"min": 0,
|
||||||
|
"max": 253,
|
||||||
|
"description": "the n2k instance to be used as starboard(main) rudder 0...253",
|
||||||
|
"category": "converter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "portRudderI",
|
||||||
|
"label":"port rudder instance",
|
||||||
|
"type": "number",
|
||||||
|
"default": "-1",
|
||||||
|
"check": "checkMinMax",
|
||||||
|
"min": -1,
|
||||||
|
"max": 253,
|
||||||
|
"description": "the n2k instance to be used as port rudder 0...253, -1 to disable",
|
||||||
|
"category": "converter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "windmappings",
|
||||||
|
"type": "array",
|
||||||
|
"replace":[
|
||||||
|
{
|
||||||
|
"n": "tng",
|
||||||
|
"l": "true north ground",
|
||||||
|
"t": "True_North=0",
|
||||||
|
"d": "twa_tws"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "mgd",
|
||||||
|
"l": "magnetic ground dir",
|
||||||
|
"t": "Magnetic=1",
|
||||||
|
"d":""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "awa",
|
||||||
|
"l": "apparent angle",
|
||||||
|
"t": "Apparent=2",
|
||||||
|
"d":"awa_aws"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "gna",
|
||||||
|
"l": "ground angle",
|
||||||
|
"t": "True_boat=3",
|
||||||
|
"d": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"n": "tra",
|
||||||
|
"l": "true angle",
|
||||||
|
"t": "True_water=4",
|
||||||
|
"d":""
|
||||||
|
}
|
||||||
|
|
||||||
|
],
|
||||||
|
"children":[
|
||||||
|
{
|
||||||
|
"name":"windm$n",
|
||||||
|
"type":"list",
|
||||||
|
"description": "mapping of the PGN 130306 wind reference $t",
|
||||||
|
"label":"wind $l",
|
||||||
|
"list":[
|
||||||
|
{"l": "-unset-","v":""},
|
||||||
|
{"l": "TWA/TWS","v":"twa_tws"},
|
||||||
|
{"l": "AWA/AWS", "v":"awa_aws"},
|
||||||
|
{"l": "TWD/TWS","v":"twd_tws"}
|
||||||
|
],
|
||||||
|
"category":"converter",
|
||||||
|
"default":"$d"
|
||||||
|
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "winst312",
|
||||||
|
"label": "130312 WTemp iid",
|
||||||
|
"type": "number",
|
||||||
|
"check": "checkMinMax",
|
||||||
|
"min": -1,
|
||||||
|
"max": 256,
|
||||||
|
"description": "the temp instance of PGN 130312 used for water temperature (0...255), use -1 for none, 256 for any",
|
||||||
|
"default": "256",
|
||||||
|
"category":"converter"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "usbActisense",
|
"name": "usbActisense",
|
||||||
"label": "USB mode",
|
"label": "USB mode",
|
||||||
|
|
|
@ -22,7 +22,7 @@ body {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabPage{
|
#tabPages{
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,6 +120,9 @@ body {
|
||||||
.hidden{
|
.hidden{
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
.dash.invalid{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
#xdrPage .row>.label{
|
#xdrPage .row>.label{
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -220,6 +223,7 @@ body {
|
||||||
}
|
}
|
||||||
#tabs {
|
#tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
border-bottom: 1px solid grey;
|
border-bottom: 1px solid grey;
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
|
@ -341,3 +345,6 @@ body {
|
||||||
.error{
|
.error{
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
input.error{
|
||||||
|
background-color: rgba(255, 0, 0, 0.329);
|
||||||
|
}
|
|
@ -23,6 +23,7 @@
|
||||||
<div class="tab" data-page="updatePage">Update</div>
|
<div class="tab" data-page="updatePage">Update</div>
|
||||||
<div class="tab" data-url="https://github.com/wellenvogel/esp32-nmea2000" data-window="help" id="helpButton">Help</div>
|
<div class="tab" data-url="https://github.com/wellenvogel/esp32-nmea2000" data-window="help" id="helpButton">Help</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="tabPages">
|
||||||
<div id="statusPage" class="tabPage">
|
<div id="statusPage" class="tabPage">
|
||||||
<div id="statusPageContent">
|
<div id="statusPageContent">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -123,6 +124,7 @@
|
||||||
<button id="uploadBin">Upload</button>
|
<button id="uploadBin">Upload</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="overlayContainer hidden" id="overlayContainer">
|
<div class="overlayContainer hidden" id="overlayContainer">
|
||||||
|
|
335
web/index.js
335
web/index.js
|
@ -1,10 +1,14 @@
|
||||||
let self = this;
|
(function () {
|
||||||
let lastUpdate = (new Date()).getTime();
|
let lastUpdate = (new Date()).getTime();
|
||||||
let reloadConfig = false;
|
let reloadConfig = false;
|
||||||
let needAdminPass = true;
|
let needAdminPass = true;
|
||||||
let lastSalt = "";
|
let lastSalt = "";
|
||||||
let channelList = {};
|
let channelList = {};
|
||||||
let minUser = 200;
|
let minUser = 200;
|
||||||
|
let listeners = [];
|
||||||
|
let buttonHandlers={};
|
||||||
|
let checkers={};
|
||||||
|
let userFormatters={};
|
||||||
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) {
|
||||||
|
@ -16,7 +20,12 @@ function addEl(type, clazz, parent, text) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (text) el.textContent = text;
|
if (text) el.textContent = text;
|
||||||
|
if (parent) {
|
||||||
|
if (typeof(parent) != 'object'){
|
||||||
|
parent=document.querySelector(parent);
|
||||||
|
}
|
||||||
if (parent) parent.appendChild(el);
|
if (parent) parent.appendChild(el);
|
||||||
|
}
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
function forEl(query, callback, base) {
|
function forEl(query, callback, base) {
|
||||||
|
@ -46,7 +55,7 @@ function getText(url){
|
||||||
return fetch(url)
|
return fetch(url)
|
||||||
.then(function (r) { return r.text() });
|
.then(function (r) { return r.text() });
|
||||||
}
|
}
|
||||||
function reset() {
|
buttonHandlers.reset=function() {
|
||||||
ensurePass()
|
ensurePass()
|
||||||
.then(function (hash) {
|
.then(function (hash) {
|
||||||
fetch('/api/reset?_hash=' + encodeURIComponent(hash));
|
fetch('/api/reset?_hash=' + encodeURIComponent(hash));
|
||||||
|
@ -119,6 +128,7 @@ function update() {
|
||||||
function resetForm(ev) {
|
function resetForm(ev) {
|
||||||
getJson("/api/config")
|
getJson("/api/config")
|
||||||
.then(function (jsonData) {
|
.then(function (jsonData) {
|
||||||
|
callListeners(api.EVENTS.config,jsonData);
|
||||||
for (let k in jsonData) {
|
for (let k in jsonData) {
|
||||||
if (k == "useAdminPass") {
|
if (k == "useAdminPass") {
|
||||||
needAdminPass = jsonData[k] != 'false';
|
needAdminPass = jsonData[k] != 'false';
|
||||||
|
@ -159,7 +169,8 @@ function resetForm(ev) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function checkMinMax(v,allValues,def){
|
buttonHandlers.resetForm=resetForm;
|
||||||
|
checkers.checkMinMax=function(v, allValues, def) {
|
||||||
let parsed = parseFloat(v);
|
let parsed = parseFloat(v);
|
||||||
if (isNaN(parsed)) return "must be a number";
|
if (isNaN(parsed)) return "must be a number";
|
||||||
if (def.min !== undefined) {
|
if (def.min !== undefined) {
|
||||||
|
@ -170,7 +181,7 @@ function checkMinMax(v,allValues,def){
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkSystemName(v) {
|
checkers.checkSystemName=function(v) {
|
||||||
//2...32 characters for ssid
|
//2...32 characters for ssid
|
||||||
let allowed = v.replace(/[^a-zA-Z0-9]*/g, '');
|
let allowed = v.replace(/[^a-zA-Z0-9]*/g, '');
|
||||||
if (allowed != v) return "contains invalid characters, only a-z, A-Z, 0-9";
|
if (allowed != v) return "contains invalid characters, only a-z, A-Z, 0-9";
|
||||||
|
@ -182,11 +193,11 @@ function checkApPass(v) {
|
||||||
return "password must be at least 8 characters";
|
return "password must be at least 8 characters";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function checkAdminPass(v){
|
checkers.checkAdminPass=function(v) {
|
||||||
return checkApPass(v);
|
return checkApPass(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkApIp(v,allValues){
|
checkers.checkApIp=function(v, allValues) {
|
||||||
if (!v) return "cannot be empty";
|
if (!v) return "cannot be empty";
|
||||||
let err1 = "must be in the form 192.168.x.x";
|
let err1 = "must be in the form 192.168.x.x";
|
||||||
if (!v.match(/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/)) return err1;
|
if (!v.match(/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/)) return err1;
|
||||||
|
@ -197,11 +208,11 @@ function checkApIp(v,allValues){
|
||||||
if (iv < 0 || iv > 255) return err1;
|
if (iv < 0 || iv > 255) return err1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function checkNetMask(v,allValues){
|
checkers.checkNetMask=function(v, allValues) {
|
||||||
return checkApIp(v,allValues);
|
return checkers.checkApIp(v, allValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkIpAddress(v,allValues,def){
|
checkers.checkIpAddress=function(v, allValues, def) {
|
||||||
if (allValues.tclEnabled != "true") return;
|
if (allValues.tclEnabled != "true") return;
|
||||||
if (!v) return "cannot be empty";
|
if (!v) return "cannot be empty";
|
||||||
if (!v.match(/[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*/)
|
if (!v.match(/[0-9]*\.[0-9]*\.[0-9]*\.[0-9]*/)
|
||||||
|
@ -209,7 +220,7 @@ function checkIpAddress(v,allValues,def){
|
||||||
return "must be either in the form 192.168.1.1 or xxx.local";
|
return "must be either in the form 192.168.1.1 or xxx.local";
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkXDR(v,allValues){
|
checkers.checkXDR=function(v, allValues) {
|
||||||
if (!v) return;
|
if (!v) return;
|
||||||
let parts = v.split(',');
|
let parts = v.split(',');
|
||||||
if (parseInt(parts[1]) == 0) return;
|
if (parseInt(parts[1]) == 0) return;
|
||||||
|
@ -238,6 +249,7 @@ function checkXDR(v,allValues){
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let loggedChecks={};
|
||||||
function getAllConfigs(omitPass) {
|
function getAllConfigs(omitPass) {
|
||||||
let values = document.querySelectorAll('.configForm select , .configForm input');
|
let values = document.querySelectorAll('.configForm select , .configForm input');
|
||||||
let allValues = {};
|
let allValues = {};
|
||||||
|
@ -253,21 +265,32 @@ function getAllConfigs(omitPass) {
|
||||||
}
|
}
|
||||||
let check = v.getAttribute('data-check');
|
let check = v.getAttribute('data-check');
|
||||||
if (check) {
|
if (check) {
|
||||||
if (typeof (self[check]) === 'function') {
|
let checkFunction=checkers[check];
|
||||||
let res = self[check](v.value, allValues, getConfigDefition(name));
|
if (typeof (checkFunction) === 'function') {
|
||||||
|
if (! loggedChecks[check]){
|
||||||
|
loggedChecks[check]=true;
|
||||||
|
//console.log("check:"+check);
|
||||||
|
}
|
||||||
|
let res = checkFunction(v.value, allValues, getConfigDefition(name));
|
||||||
if (res) {
|
if (res) {
|
||||||
let value = v.value;
|
let value = v.value;
|
||||||
if (v.type === 'password') value = "******";
|
if (v.type === 'password') value = "******";
|
||||||
alert("invalid config for " + v.getAttribute('name') + "(" + value + "):\n" + res);
|
let label = v.getAttribute('data-label');
|
||||||
|
if (!label) label = v.getAttribute('name');
|
||||||
|
v.classList.add("error");
|
||||||
|
alert("invalid config for " + label + "(" + value + "):\n" + res);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else{
|
||||||
|
console.log("check not found:",check);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
allValues[name] = v.value;
|
allValues[name] = v.value;
|
||||||
}
|
}
|
||||||
return allValues;
|
return allValues;
|
||||||
}
|
}
|
||||||
function changeConfig() {
|
buttonHandlers.changeConfig=function() {
|
||||||
ensurePass()
|
ensurePass()
|
||||||
.then(function (pass) {
|
.then(function (pass) {
|
||||||
let newAdminPass;
|
let newAdminPass;
|
||||||
|
@ -306,7 +329,7 @@ function changeConfig() {
|
||||||
})
|
})
|
||||||
.catch(function (e) { alert(e); })
|
.catch(function (e) { alert(e); })
|
||||||
}
|
}
|
||||||
function factoryReset() {
|
buttonHandlers.factoryReset=function() {
|
||||||
ensurePass()
|
ensurePass()
|
||||||
.then(function (hash) {
|
.then(function (hash) {
|
||||||
if (!confirm("Really delete all configuration?\n" +
|
if (!confirm("Really delete all configuration?\n" +
|
||||||
|
@ -408,6 +431,7 @@ function hideOverlay() {
|
||||||
el.textContent = '';
|
el.textContent = '';
|
||||||
}
|
}
|
||||||
function checkChange(el, row, name) {
|
function checkChange(el, row, name) {
|
||||||
|
el.classList.remove("error");
|
||||||
let loaded = el.getAttribute('data-loaded');
|
let loaded = el.getAttribute('data-loaded');
|
||||||
if (loaded !== undefined) {
|
if (loaded !== undefined) {
|
||||||
if (loaded != el.value) {
|
if (loaded != el.value) {
|
||||||
|
@ -505,11 +529,13 @@ function createCalSetInput(configItem,frame,clazz){
|
||||||
window.clearInterval(caliv);
|
window.clearInterval(caliv);
|
||||||
}
|
}
|
||||||
}, 200);
|
}, 200);
|
||||||
showOverlay(cs,false,[{label:'Set',click:()=>{
|
showOverlay(cs, false, [{
|
||||||
|
label: 'Set', click: () => {
|
||||||
el.value = vel.textContent;
|
el.value = vel.textContent;
|
||||||
let cev = new Event('change');
|
let cev = new Event('change');
|
||||||
el.dispatchEvent(cev);
|
el.dispatchEvent(cev);
|
||||||
}}]);
|
}
|
||||||
|
}]);
|
||||||
})
|
})
|
||||||
el.setAttribute('name', configItem.name)
|
el.setAttribute('name', configItem.name)
|
||||||
return el;
|
return el;
|
||||||
|
@ -554,11 +580,13 @@ function createCalValInput(configItem,frame,clazz){
|
||||||
window.clearInterval(caliv);
|
window.clearInterval(caliv);
|
||||||
}
|
}
|
||||||
}, 200);
|
}, 200);
|
||||||
showOverlay(cs,false,[{label:'Set',click:()=>{
|
showOverlay(cs, false, [{
|
||||||
|
label: 'Set', click: () => {
|
||||||
el.value = vinp.value;
|
el.value = vinp.value;
|
||||||
let cev = new Event('change');
|
let cev = new Event('change');
|
||||||
el.dispatchEvent(cev);
|
el.dispatchEvent(cev);
|
||||||
}}]);
|
}
|
||||||
|
}]);
|
||||||
})
|
})
|
||||||
el.setAttribute('name', configItem.name)
|
el.setAttribute('name', configItem.name)
|
||||||
return el;
|
return el;
|
||||||
|
@ -995,7 +1023,7 @@ function unassignedAdd(ev) {
|
||||||
hideOverlay();
|
hideOverlay();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function loadUnassigned(){
|
buttonHandlers.loadUnassigned=function() {
|
||||||
getText("/api/xdrUnmapped")
|
getText("/api/xdrUnmapped")
|
||||||
.then(function (txt) {
|
.then(function (txt) {
|
||||||
let ot = "";
|
let ot = "";
|
||||||
|
@ -1043,17 +1071,17 @@ function downloadData(data,name){
|
||||||
target.setAttribute('download', name);
|
target.setAttribute('download', name);
|
||||||
target.click();
|
target.click();
|
||||||
}
|
}
|
||||||
function exportConfig(){
|
buttonHandlers.exportConfig=function() {
|
||||||
let data = getAllConfigs(true);
|
let data = getAllConfigs(true);
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
downloadData(data, formatDateForFilename(true) + ".json");
|
downloadData(data, formatDateForFilename(true) + ".json");
|
||||||
}
|
}
|
||||||
function exportXdr(){
|
buttonHandlers.exportXdr=function() {
|
||||||
let data = {};
|
let data = {};
|
||||||
forEl('.xdrvalue', function (el) {
|
forEl('.xdrvalue', function (el) {
|
||||||
let name = el.getAttribute('name');
|
let name = el.getAttribute('name');
|
||||||
let value = el.value;
|
let value = el.value;
|
||||||
let err=checkXDR(value,data);
|
let err = checkers.checkXDR(value, data);
|
||||||
if (err) {
|
if (err) {
|
||||||
alert("error in " + name + ": " + value + "\n" + err);
|
alert("error in " + name + ": " + value + "\n" + err);
|
||||||
return;
|
return;
|
||||||
|
@ -1114,10 +1142,10 @@ function importJson(opt_keyPattern){
|
||||||
});
|
});
|
||||||
ip.click();
|
ip.click();
|
||||||
}
|
}
|
||||||
function importXdr(){
|
buttonHandlers.importXdr=function() {
|
||||||
importJson(new RegExp(/^XDR[0-9][0-9]*/));
|
importJson(new RegExp(/^XDR[0-9][0-9]*/));
|
||||||
}
|
}
|
||||||
function importConfig(){
|
buttonHandlers.importConfig=function() {
|
||||||
importJson();
|
importJson();
|
||||||
}
|
}
|
||||||
function toggleClass(el, id, classList) {
|
function toggleClass(el, id, classList) {
|
||||||
|
@ -1140,7 +1168,7 @@ function createConfigDefinitions(parent, capabilities, defs,includeXdr) {
|
||||||
configDefinitions = defs;
|
configDefinitions = defs;
|
||||||
let currentCategoryPopulated = true;
|
let currentCategoryPopulated = true;
|
||||||
defs.forEach(function (item) {
|
defs.forEach(function (item) {
|
||||||
if (!item.type) return;
|
if (!item.type || item.category === undefined) return;
|
||||||
if (item.category.match(/^xdr/)) {
|
if (item.category.match(/^xdr/)) {
|
||||||
if (!includeXdr) return;
|
if (!includeXdr) return;
|
||||||
}
|
}
|
||||||
|
@ -1228,6 +1256,7 @@ function createConfigDefinitions(parent, capabilities, defs,includeXdr) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (item.check) valueEl.setAttribute('data-check', item.check);
|
if (item.check) valueEl.setAttribute('data-check', item.check);
|
||||||
|
valueEl.setAttribute('data-label', label);
|
||||||
let btContainer = addEl('div', 'buttonContainer', row);
|
let btContainer = addEl('div', 'buttonContainer', row);
|
||||||
if (!readOnly) {
|
if (!readOnly) {
|
||||||
let bt = addEl('button', 'defaultButton', btContainer, 'X');
|
let bt = addEl('button', 'defaultButton', btContainer, 'X');
|
||||||
|
@ -1265,6 +1294,12 @@ function loadConfigDefinitions() {
|
||||||
let el = document.getElementById('helpButton');
|
let el = document.getElementById('helpButton');
|
||||||
if (el) el.setAttribute('data-url', capabilities.HELP_URL);
|
if (el) el.setAttribute('data-url', capabilities.HELP_URL);
|
||||||
}
|
}
|
||||||
|
try{
|
||||||
|
Object.freeze(capabilities);
|
||||||
|
callListeners(api.EVENTS.init,capabilities);
|
||||||
|
}catch (e){
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
getJson("config.json")
|
getJson("config.json")
|
||||||
.then(function (defs) {
|
.then(function (defs) {
|
||||||
getJson("xdrconfig.json")
|
getJson("xdrconfig.json")
|
||||||
|
@ -1299,7 +1334,7 @@ function verifyPass(pass){
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function adminPassCancel(){
|
buttonHandlers.adminPassCancel=function() {
|
||||||
forEl('#adminPassOverlay', function (el) { el.classList.add('hidden') });
|
forEl('#adminPassOverlay', function (el) { el.classList.add('hidden') });
|
||||||
forEl('#adminPassInput', function (el) { el.value = '' });
|
forEl('#adminPassInput', function (el) { el.value = '' });
|
||||||
}
|
}
|
||||||
|
@ -1316,7 +1351,7 @@ function saveAdminPass(pass,forceIfSet){
|
||||||
} catch (e) { }
|
} catch (e) { }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function forgetPass(){
|
buttonHandlers.forgetPass=function() {
|
||||||
localStorage.removeItem('adminPass');
|
localStorage.removeItem('adminPass');
|
||||||
forEl('#adminPassInput', function (el) {
|
forEl('#adminPassInput', function (el) {
|
||||||
el.value = '';
|
el.value = '';
|
||||||
|
@ -1369,7 +1404,7 @@ function ensurePass(){
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function converterInfo() {
|
buttonHandlers.converterInfo=function() {
|
||||||
getJson("api/converterInfo").then(function (json) {
|
getJson("api/converterInfo").then(function (json) {
|
||||||
let text = "<h3>Converted entities</h3>";
|
let text = "<h3>Converted entities</h3>";
|
||||||
text += "<p><b>NMEA0183 to NMEA2000:</b><br/>";
|
text += "<p><b>NMEA0183 to NMEA2000:</b><br/>";
|
||||||
|
@ -1390,16 +1425,15 @@ function handleTab(el) {
|
||||||
}
|
}
|
||||||
let activeTab = document.getElementById(activeName);
|
let activeTab = document.getElementById(activeName);
|
||||||
if (!activeTab) return;
|
if (!activeTab) return;
|
||||||
let all = document.querySelectorAll('.tabPage');
|
forEl('.tabPage',function(pel){
|
||||||
for (let i = 0; i < all.length; i++) {
|
pel.classList.add('hidden');
|
||||||
all[i].classList.add('hidden');
|
});
|
||||||
}
|
forEl('.tab',function(tel){
|
||||||
let tabs = document.querySelectorAll('.tab');
|
tel.classList.remove('active');
|
||||||
for (let i = 0; i < all.length; i++) {
|
});
|
||||||
tabs[i].classList.remove('active');
|
|
||||||
}
|
|
||||||
el.classList.add('active');
|
el.classList.add('active');
|
||||||
activeTab.classList.remove('hidden');
|
activeTab.classList.remove('hidden');
|
||||||
|
callListeners(api.EVENTS.tab,activeName);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -1574,6 +1608,10 @@ let valueFormatters = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
Object.freeze(valueFormatters);
|
||||||
|
for (let k in valueFormatters){
|
||||||
|
Object.freeze(valueFormatters[k]);
|
||||||
}
|
}
|
||||||
function resizeFont(el, reset, maxIt) {
|
function resizeFont(el, reset, maxIt) {
|
||||||
if (maxIt === undefined) maxIt = 10;
|
if (maxIt === undefined) maxIt = 10;
|
||||||
|
@ -1584,23 +1622,62 @@ function resizeFont(el,reset,maxIt){
|
||||||
el.style.fontSize = next + "px";
|
el.style.fontSize = next + "px";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function createDashboardItem(name, def, parent) {
|
function getUnit(def,useUser){
|
||||||
if (! def.name) return;
|
let fmt = useUser?(userFormatters[def.name] || valueFormatters[def.format]):valueFormatters[def.format] ;
|
||||||
let frame = addEl('div', 'dash', parent);
|
|
||||||
let title = addEl('span', 'dashTitle', frame, name);
|
|
||||||
let value = addEl('span', 'dashValue', frame);
|
|
||||||
value.setAttribute('id', 'data_' + name);
|
|
||||||
let fmt=valueFormatters[def.format];
|
|
||||||
if (def.format) value.classList.add(def.format);
|
|
||||||
let footer = addEl('div','footer',frame);
|
|
||||||
let src= addEl('span','source',footer);
|
|
||||||
src.setAttribute('id','source_'+name);
|
|
||||||
let u = fmt ? fmt.u : ' ';
|
let u = fmt ? fmt.u : ' ';
|
||||||
if (!fmt && def.format && def.format.match(/formatXdr/)) {
|
if (!fmt && def.format && def.format.match(/formatXdr/)) {
|
||||||
u = def.format.replace(/formatXdr:[^:]*:/, '');
|
u = def.format.replace(/formatXdr:[^:]*:/, '');
|
||||||
}
|
}
|
||||||
|
return u;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* create a dashboard item if it does not exist
|
||||||
|
* @param {*} def
|
||||||
|
* @param {*} show
|
||||||
|
* @param {*} parent
|
||||||
|
* @returns the value div of the dashboard item
|
||||||
|
*/
|
||||||
|
function createOrHideDashboardItem(def,show, parent) {
|
||||||
|
if (!def.name) return;
|
||||||
|
let frame=document.getElementById('frame_'+def.name);
|
||||||
|
let build=false;
|
||||||
|
if (frame){
|
||||||
|
if (frame.classList.contains('invalid') && show){
|
||||||
|
build=true;
|
||||||
|
frame.classList.remove('invalid');
|
||||||
|
frame.innerHTML='';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
if (! parent) return;
|
||||||
|
frame = addEl('div', 'dash', parent);
|
||||||
|
frame.setAttribute('id','frame_'+def.name);
|
||||||
|
build=true;
|
||||||
|
}
|
||||||
|
if (! show){
|
||||||
|
if (!frame.classList.contains('invalid')){
|
||||||
|
frame.classList.add('invalid');
|
||||||
|
frame.innerHTML='';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (build) {
|
||||||
|
let title = addEl('span', 'dashTitle', frame, def.name);
|
||||||
|
let value = addEl('span', 'dashValue', frame);
|
||||||
|
value.setAttribute('id', 'data_' + def.name);
|
||||||
|
if (def.format) value.classList.add(def.format);
|
||||||
|
let footer = addEl('div', 'footer', frame);
|
||||||
|
let src = addEl('span', 'source', footer);
|
||||||
|
src.setAttribute('id', 'source_' + def.name);
|
||||||
|
let u = getUnit(def, true)
|
||||||
addEl('span', 'unit', footer, u);
|
addEl('span', 'unit', footer, u);
|
||||||
return value;
|
callListeners(api.EVENTS.dataItemCreated,{name:def.name,element:frame});
|
||||||
|
}
|
||||||
|
let de = document.getElementById('data_' + def.name);
|
||||||
|
return de;
|
||||||
|
}
|
||||||
|
function hideDashboardItem(name){
|
||||||
|
createOrHideDashboardItem({name:name},false);
|
||||||
}
|
}
|
||||||
function parseBoatDataLine(line) {
|
function parseBoatDataLine(line) {
|
||||||
let rt = {};
|
let rt = {};
|
||||||
|
@ -1631,6 +1708,7 @@ function sourceName(v){
|
||||||
}
|
}
|
||||||
let lastSelectList = [];
|
let lastSelectList = [];
|
||||||
function updateDashboard(data) {
|
function updateDashboard(data) {
|
||||||
|
callListeners(api.EVENTS.boatData,data);
|
||||||
let frame = document.getElementById('dashboardPage');
|
let frame = document.getElementById('dashboardPage');
|
||||||
let showInvalid = true;
|
let showInvalid = true;
|
||||||
forEl('select[name=showInvalidData]', function (el) {
|
forEl('select[name=showInvalidData]', function (el) {
|
||||||
|
@ -1641,25 +1719,18 @@ function updateDashboard(data) {
|
||||||
let current = parseBoatDataLine(data[n]);
|
let current = parseBoatDataLine(data[n]);
|
||||||
if (!current.name) continue;
|
if (!current.name) continue;
|
||||||
names[current.name] = true;
|
names[current.name] = true;
|
||||||
let de = document.getElementById('data_' + current.name);
|
let show = current.valid||showInvalid;
|
||||||
let isValid=current.valid;
|
let de = createOrHideDashboardItem(current, show, frame);
|
||||||
if (! de && frame && (isValid || showInvalid)){
|
let newContent = '---';
|
||||||
de=createDashboardItem(current.name,current,frame);
|
|
||||||
}
|
|
||||||
if (de && (!isValid && !showInvalid)){
|
|
||||||
de.parentElement.remove();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (de) {
|
|
||||||
let newContent='----';
|
|
||||||
if (current.valid) {
|
if (current.valid) {
|
||||||
let formatter;
|
let formatter;
|
||||||
if (current.format && current.format != "NULL") {
|
if (current.format && current.format != "NULL") {
|
||||||
let key = current.format.replace(/^\&/, '');
|
let key = current.format.replace(/^\&/, '');
|
||||||
formatter = valueFormatters[key];
|
formatter = userFormatters[current.name] || valueFormatters[key];
|
||||||
}
|
}
|
||||||
if (formatter) {
|
if (formatter && formatter.f) {
|
||||||
newContent = formatter.f(current.value);
|
newContent = formatter.f(current.value,true);
|
||||||
|
if (newContent === undefined) newContent = "";
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
let v = parseFloat(current.value);
|
let v = parseFloat(current.value);
|
||||||
|
@ -1672,7 +1743,16 @@ function updateDashboard(data) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else newContent = "---";
|
else {
|
||||||
|
let uf=userFormatters[current.name];
|
||||||
|
if (uf && uf.f){
|
||||||
|
//call the user formatter
|
||||||
|
//so that it can detect the invalid state
|
||||||
|
newContent=uf.f(undefined,false);
|
||||||
|
}
|
||||||
|
if (newContent === undefined)newContent = "---";
|
||||||
|
}
|
||||||
|
if (de) {
|
||||||
if (newContent !== de.textContent) {
|
if (newContent !== de.textContent) {
|
||||||
de.textContent = newContent;
|
de.textContent = newContent;
|
||||||
resizeFont(de, true);
|
resizeFont(de, true);
|
||||||
|
@ -1683,11 +1763,14 @@ function updateDashboard(data) {
|
||||||
src.textContent = sourceName(current.source);
|
src.textContent = sourceName(current.source);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log("update");
|
//console.log("update");
|
||||||
forEl('.dashValue',function(el){
|
//remove all items that are not send any more
|
||||||
|
//this can only happen if the device restarted
|
||||||
|
//otherwise data items will not go away - they will become invalid
|
||||||
|
forEl('.dash', function (el) {
|
||||||
let id = el.getAttribute('id');
|
let id = el.getAttribute('id');
|
||||||
if (id) {
|
if (id) {
|
||||||
if (! names[id.replace(/^data_/,'')]){
|
if (!names[id.replace(/^frame_/, '')]) {
|
||||||
el.parentElement.remove();
|
el.parentElement.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1714,7 +1797,7 @@ function updateDashboard(data) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function uploadBin(ev){
|
buttonHandlers.uploadBin=function(ev) {
|
||||||
let el = document.getElementById("uploadFile");
|
let el = document.getElementById("uploadFile");
|
||||||
let progressEl = document.getElementById("uploadDone");
|
let progressEl = document.getElementById("uploadDone");
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
@ -1861,6 +1944,111 @@ function checkImageFile(file){
|
||||||
reader.readAsArrayBuffer(slice);
|
reader.readAsArrayBuffer(slice);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
function addTabPage(name,label,url){
|
||||||
|
if (label === undefined) label=name;
|
||||||
|
let tab=addEl('div','tab','#tabs',label);
|
||||||
|
tab.addEventListener('click',function(ev){
|
||||||
|
handleTab(ev.target);
|
||||||
|
})
|
||||||
|
if (url !== undefined){
|
||||||
|
tab.setAttribute('data-url',url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tab.setAttribute('data-page',name);
|
||||||
|
let page=addEl('div','tabPage hidden','#tabPages');
|
||||||
|
page.setAttribute('id',name);
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
function addUserFormatter(name,unit,formatter){
|
||||||
|
if (unit !== undefined && formatter !== undefined){
|
||||||
|
userFormatters[name]={
|
||||||
|
u:unit,
|
||||||
|
f:formatter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
delete userFormatters[name];
|
||||||
|
}
|
||||||
|
hideDashboardItem(name); //will recreate it on next data receive
|
||||||
|
}
|
||||||
|
const api= {
|
||||||
|
registerListener: function (callback) {
|
||||||
|
listeners.push(callback);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* helper for creating dom elements
|
||||||
|
* parameters:
|
||||||
|
* type: the element type (e.g. div)
|
||||||
|
* class: a list of classes separated by space
|
||||||
|
* parent (opt): a parent element (either a dom element vor a query selector)
|
||||||
|
* text (opt): the text to be set as textContent
|
||||||
|
* returns: the newly created element
|
||||||
|
*/
|
||||||
|
addEl: addEl,
|
||||||
|
/**
|
||||||
|
* iterator helper for a query selector
|
||||||
|
* parameters:
|
||||||
|
* query: the query selector
|
||||||
|
* callback: the callback function (will be called with the element as param)
|
||||||
|
* base (opt): a dome element to be used as the root (defaults to document)
|
||||||
|
*/
|
||||||
|
forEl: forEl,
|
||||||
|
/**
|
||||||
|
* find the closest parent that has a particular class
|
||||||
|
* parameters:
|
||||||
|
* element: the element to start with
|
||||||
|
* class: the class to be searched for
|
||||||
|
* returns: the element or undefined/null
|
||||||
|
*/
|
||||||
|
closestParent: closestParent,
|
||||||
|
/**
|
||||||
|
* add a new tab
|
||||||
|
* parameters:
|
||||||
|
* name - the name of the page
|
||||||
|
* label (opt): the label for the new page
|
||||||
|
* returns: the newly created element
|
||||||
|
*/
|
||||||
|
addTabPage: addTabPage,
|
||||||
|
/**
|
||||||
|
* add a user defined formatter for a boat data item
|
||||||
|
* parameters:
|
||||||
|
* name : the boat data item name
|
||||||
|
* unit: the unit to be displayed
|
||||||
|
* formatter: the formatter function (must return a string)
|
||||||
|
*/
|
||||||
|
addUserFormatter: addUserFormatter,
|
||||||
|
removeUserFormatter: function(name){
|
||||||
|
addUserFormatter(name);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* a dict of formatters
|
||||||
|
* each one has 2 members:
|
||||||
|
* u: the unit
|
||||||
|
* f: the formatter function
|
||||||
|
*/
|
||||||
|
formatters: valueFormatters,
|
||||||
|
/**
|
||||||
|
* parse a line of boat data
|
||||||
|
* the line has name,format,valid,update,source,value
|
||||||
|
*/
|
||||||
|
parseBoatDataLine: parseBoatDataLine,
|
||||||
|
EVENTS: {
|
||||||
|
init: 0, //called when capabilities are loaded, data is capabilities
|
||||||
|
tab: 1, //tab page activated data is the id of the tab page
|
||||||
|
config: 2, //data is the config object
|
||||||
|
boatData: 3, //data is the list of boat Data items
|
||||||
|
dataItemCreated: 4, //data is an object with
|
||||||
|
// name: the item name, element: the frame item of the boat data display
|
||||||
|
}
|
||||||
|
};
|
||||||
|
function callListeners(event,data){
|
||||||
|
listeners.forEach((listener)=>{
|
||||||
|
if (typeof(listener) === 'function'){
|
||||||
|
listener(event,data);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
window.esp32nmea2k = api;
|
||||||
window.setInterval(update, 1000);
|
window.setInterval(update, 1000);
|
||||||
window.setInterval(function () {
|
window.setInterval(function () {
|
||||||
let dp = document.getElementById('dashboardPage');
|
let dp = document.getElementById('dashboardPage');
|
||||||
|
@ -1873,7 +2061,14 @@ window.addEventListener('load', function () {
|
||||||
let buttons = document.querySelectorAll('button');
|
let buttons = document.querySelectorAll('button');
|
||||||
for (let i = 0; i < buttons.length; i++) {
|
for (let i = 0; i < buttons.length; i++) {
|
||||||
let be = buttons[i];
|
let be = buttons[i];
|
||||||
be.onclick = window[be.id]; //assume a function with the button id
|
let buttonFunction=buttonHandlers[be.id];
|
||||||
|
if (typeof(buttonFunction) === 'function'){
|
||||||
|
be.onclick = buttonFunction; //assume a function with the button id
|
||||||
|
//console.log("button: "+be.id);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
console.log("no handler for button "+be.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
forEl('.showMsgDetails', function (cd) {
|
forEl('.showMsgDetails', function (cd) {
|
||||||
cd.addEventListener('change', function (ev) {
|
cd.addEventListener('change', function (ev) {
|
||||||
|
@ -1931,3 +2126,5 @@ window.addEventListener('load', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
}());
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue