intermediate: split UI, more icons
This commit is contained in:
parent
3c4920d104
commit
1deef5c7de
|
@ -7,7 +7,7 @@ import inspect
|
||||||
import json
|
import json
|
||||||
GEN_DIR='generated'
|
GEN_DIR='generated'
|
||||||
CFG_FILE='web/config.json'
|
CFG_FILE='web/config.json'
|
||||||
FILES=['web/index.html',CFG_FILE]
|
FILES=['web/index.html',CFG_FILE,'web/index.js','web/index.css']
|
||||||
CFG_INCLUDE='GwConfigDefinitions.h'
|
CFG_INCLUDE='GwConfigDefinitions.h'
|
||||||
|
|
||||||
def basePath():
|
def basePath():
|
||||||
|
|
|
@ -8,19 +8,23 @@ class EmbeddedFile {
|
||||||
public:
|
public:
|
||||||
const uint8_t *start;
|
const uint8_t *start;
|
||||||
int len;
|
int len;
|
||||||
EmbeddedFile(String name,const uint8_t *start,int len){
|
String contentType;
|
||||||
|
EmbeddedFile(String name,String contentType, const uint8_t *start,int len){
|
||||||
this->start=start;
|
this->start=start;
|
||||||
this->len=len;
|
this->len=len;
|
||||||
|
this->contentType=contentType;
|
||||||
embeddedFiles[name]=this;
|
embeddedFiles[name]=this;
|
||||||
}
|
}
|
||||||
} ;
|
} ;
|
||||||
#define EMBED_GZ_FILE(fileName, fileExt) \
|
#define EMBED_GZ_FILE(fileName, fileExt, contentType) \
|
||||||
extern const uint8_t fileName##_##fileExt##_File[] asm("_binary_generated_" #fileName "_" #fileExt "_gz_start"); \
|
extern const uint8_t fileName##_##fileExt##_File[] asm("_binary_generated_" #fileName "_" #fileExt "_gz_start"); \
|
||||||
extern const uint8_t fileName##_##fileExt##_FileLen[] asm("_binary_generated_" #fileName "_" #fileExt "_gz_size"); \
|
extern const uint8_t fileName##_##fileExt##_FileLen[] asm("_binary_generated_" #fileName "_" #fileExt "_gz_size"); \
|
||||||
const EmbeddedFile fileName##_##fileExt##_Config(#fileName "." #fileExt,(const uint8_t*)fileName##_##fileExt##_File,(int)fileName##_##fileExt##_FileLen);
|
const EmbeddedFile fileName##_##fileExt##_Config(#fileName "." #fileExt,contentType,(const uint8_t*)fileName##_##fileExt##_File,(int)fileName##_##fileExt##_FileLen);
|
||||||
|
|
||||||
EMBED_GZ_FILE(index,html)
|
EMBED_GZ_FILE(index,html,"text/html")
|
||||||
EMBED_GZ_FILE(config,json)
|
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);
|
||||||
|
@ -48,9 +52,13 @@ void GwWebServer::begin(){
|
||||||
server->on("/", HTTP_GET, [](AsyncWebServerRequest *request){
|
server->on("/", HTTP_GET, [](AsyncWebServerRequest *request){
|
||||||
sendEmbeddedFile("index.html","text/html",request);
|
sendEmbeddedFile("index.html","text/html",request);
|
||||||
});
|
});
|
||||||
server->on("/config.json", HTTP_GET, [](AsyncWebServerRequest *request){
|
for (auto it=embeddedFiles.begin();it != embeddedFiles.end();it++){
|
||||||
sendEmbeddedFile("config.json","application/json",request);
|
String uri=String("/")+it->first;
|
||||||
|
server->on(uri.c_str(), HTTP_GET, [it](AsyncWebServerRequest *request){
|
||||||
|
sendEmbeddedFile(it->first,it->second->contentType,request);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
server->begin();
|
server->begin();
|
||||||
LOG_DEBUG(GwLog::LOG,"HTTP server started");
|
LOG_DEBUG(GwLog::LOG,"HTTP server started");
|
||||||
MDNS.addService("_http","_tcp",80);
|
MDNS.addService("_http","_tcp",80);
|
||||||
|
|
|
@ -20,6 +20,8 @@ lib_deps =
|
||||||
fastled/FastLED @ ^3.4.0
|
fastled/FastLED @ ^3.4.0
|
||||||
board_build.embed_files =
|
board_build.embed_files =
|
||||||
generated/index.html.gz
|
generated/index.html.gz
|
||||||
|
generated/index.js.gz
|
||||||
|
generated/index.css.gz
|
||||||
generated/config.json.gz
|
generated/config.json.gz
|
||||||
extra_scripts = pre:extra_script.py
|
extra_scripts = pre:extra_script.py
|
||||||
build_flags = -Igenerated
|
build_flags = -Igenerated
|
||||||
|
|
|
@ -0,0 +1,190 @@
|
||||||
|
body{
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
.configForm {
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
.configForm .buttons {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
.configForm .content>div:nth-child(even) {
|
||||||
|
background-color: rgb(211 211 211 / 43%);
|
||||||
|
}
|
||||||
|
#statusPage .even {
|
||||||
|
background-color: rgb(211 211 211 / 43%);
|
||||||
|
}
|
||||||
|
.category .title .label {
|
||||||
|
opacity: 1;
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
.changed input{
|
||||||
|
color: green
|
||||||
|
}
|
||||||
|
.changed select{
|
||||||
|
color: green
|
||||||
|
}
|
||||||
|
span.label {
|
||||||
|
width: 10em;
|
||||||
|
display: inline-block;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.configForm .value {
|
||||||
|
width: 21em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
margin-bottom: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
span#connected {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: red;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
span#connected.ok{
|
||||||
|
background-color: #13ac13;
|
||||||
|
}
|
||||||
|
.row {
|
||||||
|
padding: 0.5em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
input,select {
|
||||||
|
border: 1px solid #808080a1;
|
||||||
|
font-size: 0.9em;
|
||||||
|
padding: 0.2em;
|
||||||
|
}
|
||||||
|
.filter {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.buttons {
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
button.infoButton {
|
||||||
|
margin-left: 1em;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
.category .title {
|
||||||
|
padding-left: 0.5em;
|
||||||
|
background-color: lightgray;
|
||||||
|
padding-top: 0.3em;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
border-bottom: 1px solid darkgray;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.hidden{
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.msgDetails .value {
|
||||||
|
width: 5em;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.msgDetails .label {
|
||||||
|
width: 5em;
|
||||||
|
}
|
||||||
|
.overlayContainer {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: #80808070;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#overlay {
|
||||||
|
margin: auto;
|
||||||
|
background-color: white;
|
||||||
|
padding: 0.5em;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
div#overlayContent {
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
div#overlayContent.text{
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
.overlayButtons {
|
||||||
|
border-top: 1px solid grey;
|
||||||
|
padding-top: 0.5em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
|
#tabs {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1px solid grey;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
#tabs .tab {
|
||||||
|
background-color: lightgray;
|
||||||
|
padding: 0.5em;
|
||||||
|
border-left: 1px;
|
||||||
|
border-right: 1px;
|
||||||
|
border-top: 1px;
|
||||||
|
border-bottom: 1px;
|
||||||
|
border-color: grey;
|
||||||
|
border-style: solid;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
#tabs .tab.active{
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.buttons button{
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
button#reset{
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
h1{
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.icon-eye{
|
||||||
|
background-image: url("data:image/svg+xml;utf-8,<svg width='24' height='24' xmlns='http://www.w3.org/2000/svg' xmlns:svg='http://www.w3.org/2000/svg'> <g class='layer'> <title>Layer 1</title> <path d='m0,0l24,0l0,24l-24,0l0,-24z' fill='none' id='svg_1'/> <path d='m12,6c3.79,0 7.17,2.13 8.82,5.5c-1.65,3.37 -5.03,5.5 -8.82,5.5s-7.17,-2.13 -8.82,-5.5c1.65,-3.37 5.03,-5.5 8.82,-5.5m0,-2c-5,0 -9.27,3.11 -11,7.5c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zm0,5c1.38,0 2.5,1.12 2.5,2.5s-1.12,2.5 -2.5,2.5s-2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5m0,-2c-2.48,0 -4.5,2.02 -4.5,4.5s2.02,4.5 4.5,4.5s4.5,-2.02 4.5,-4.5s-2.02,-4.5 -4.5,-4.5z' id='svg_2'/> </g> </svg>");
|
||||||
|
margin-left: 0.5em;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
.icon{
|
||||||
|
height: 1.5em;
|
||||||
|
width: 1.5em;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.icon-more{
|
||||||
|
background-image: url("data:image/svg+xml;utf-8,<svg width='24' height='24' xmlns='http://www.w3.org/2000/svg' xmlns:svg='http://www.w3.org/2000/svg'> <g class='layer'> <title>Layer 1</title> <path d='m24,24l-24,0l0,-24l24,0l0,24z' fill='none' id='svg_1' opacity='0.87'/> <path d='m16.59,8.59l-4.59,4.58l-4.59,-4.58l-1.41,1.41l6,6l6,-6l-1.41,-1.41z' id='svg_2'/> </g> </svg>");
|
||||||
|
}
|
||||||
|
.icon-less{
|
||||||
|
background-image: url("data:image/svg+xml;utf-8,<svg width='24' height='24' xmlns='http://www.w3.org/2000/svg' xmlns:svg='http://www.w3.org/2000/svg'> <g class='layer'> <title>Layer 1</title> <path d='m0,0l24,0l0,24l-24,0l0,-24z' fill='none' id='svg_1'/> <path d='m12,8l-6,6l1.41,1.41l4.59,-4.58l4.59,4.58l1.41,-1.41l-6,-6z' id='svg_2'/> </g> </svg>");
|
||||||
|
}
|
||||||
|
.icon-eye.active{
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dash {
|
||||||
|
width: 6em;
|
||||||
|
height: 4em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid grey;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
div#dashboardPage {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.dashTitle {
|
||||||
|
font-size: 0.8em;
|
||||||
|
background-color: lightgray;
|
||||||
|
}
|
||||||
|
.dashValue{
|
||||||
|
font-size: 1.6em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
653
web/index.html
653
web/index.html
|
@ -4,657 +4,8 @@
|
||||||
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
|
<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
|
||||||
<title>NMEA 2000 Gateway</title>
|
<title>NMEA 2000 Gateway</title>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript" src="index.js"></script>
|
||||||
let self=this;
|
<link rel="stylesheet" href="index.css">
|
||||||
let lastUpdate=(new Date()).getTime();
|
|
||||||
let reloadConfig=false;
|
|
||||||
function addEl(type,clazz,parent,text){
|
|
||||||
let el=document.createElement(type);
|
|
||||||
el.classList.add(clazz);
|
|
||||||
if (text) el.textContent=text;
|
|
||||||
if (parent)parent.appendChild(el);
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
function forEl(query,callback){
|
|
||||||
let all=document.querySelectorAll(query);
|
|
||||||
for (let i=0;i<all.length;i++){
|
|
||||||
callback(all[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function alertRestart(){
|
|
||||||
reloadConfig=true;
|
|
||||||
alert("Board reset triggered, reconnect WLAN if necessary");
|
|
||||||
}
|
|
||||||
function getJson(url){
|
|
||||||
return fetch(url)
|
|
||||||
.then(function(r){return r.json()});
|
|
||||||
}
|
|
||||||
function reset(){
|
|
||||||
fetch('/api/reset');
|
|
||||||
alertRestart();
|
|
||||||
}
|
|
||||||
function update(){
|
|
||||||
let now=(new Date()).getTime();
|
|
||||||
let ce=document.getElementById('connected');
|
|
||||||
if (ce){
|
|
||||||
if ((lastUpdate+3000) > now){
|
|
||||||
ce.classList.add('ok');
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
ce.classList.remove('ok');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
getJson('/api/status')
|
|
||||||
.then(function(jsonData){
|
|
||||||
for (let k in jsonData){
|
|
||||||
if (typeof(jsonData[k]) === 'object'){
|
|
||||||
for (let sk in jsonData[k]){
|
|
||||||
let key=k+"."+sk;
|
|
||||||
if (typeof(jsonData[k][sk]) === 'object'){
|
|
||||||
//msg details
|
|
||||||
updateMsgDetails(key,jsonData[k][sk]);
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
let el=document.getElementById(key);
|
|
||||||
if (el) el.textContent=jsonData[k][sk];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
let el=document.getElementById(k);
|
|
||||||
if (el) el.textContent=jsonData[k];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lastUpdate=(new Date()).getTime();
|
|
||||||
if (reloadConfig){
|
|
||||||
reloadConfig=false;
|
|
||||||
resetForm();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
function resetForm(ev){
|
|
||||||
getJson("/api/config")
|
|
||||||
.then(function(jsonData){
|
|
||||||
for (let k in jsonData){
|
|
||||||
let el=document.querySelector("[name='"+k+"']");
|
|
||||||
if (el){
|
|
||||||
let v=jsonData[k];
|
|
||||||
el.value=v;
|
|
||||||
el.setAttribute('data-loaded',v);
|
|
||||||
let changeEvent=new Event('change');
|
|
||||||
el.dispatchEvent(changeEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function checkMaxClients(v){
|
|
||||||
let parsed=parseInt(v);
|
|
||||||
if (isNaN(parsed)) return "not a valid number";
|
|
||||||
if (parsed < 0) return "must be >= 0";
|
|
||||||
if (parsed > 10) return "max is 10";
|
|
||||||
}
|
|
||||||
function checkSystemName(v){
|
|
||||||
//2...32 characters for ssid
|
|
||||||
let allowed=v.replace(/[^a-zA-Z0-9]*/g,'');
|
|
||||||
if (allowed != v) return "contains invalid characters, only a-z, A-Z, 0-9";
|
|
||||||
if (v.length < 2 || v.length > 32) return "invalid length (2...32)";
|
|
||||||
}
|
|
||||||
function checkApPass(v){
|
|
||||||
//min 8 characters
|
|
||||||
if (v.length < 8){
|
|
||||||
return "password must be at least 8 characters";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function changeConfig(){
|
|
||||||
let url="/api/setConfig?";
|
|
||||||
let values=document.querySelectorAll('.configForm select , .configForm input');
|
|
||||||
for (let i=0;i<values.length;i++){
|
|
||||||
let v=values[i];
|
|
||||||
let name=v.getAttribute('name');
|
|
||||||
if (! name) continue;
|
|
||||||
if (name.indexOf("_") >= 0) continue;
|
|
||||||
let check=v.getAttribute('data-check');
|
|
||||||
if (check){
|
|
||||||
if (typeof(self[check]) === 'function'){
|
|
||||||
let res=self[check](v.value);
|
|
||||||
if (res){
|
|
||||||
let value=v.value;
|
|
||||||
if (v.type === 'password') value="******";
|
|
||||||
alert("invalid config for "+v.getAttribute('name')+"("+value+"):\n"+res);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
url+=name+"="+encodeURIComponent(v.value)+"&";
|
|
||||||
}
|
|
||||||
getJson(url)
|
|
||||||
.then(function(status){
|
|
||||||
if(status.status == 'OK'){
|
|
||||||
alertRestart();
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
alert("unable to set config: "+status.status);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
function factoryReset(){
|
|
||||||
if (! confirm("Really delete all configuration?\n"+
|
|
||||||
"This will reset all your Wifi settings and disconnect you.")){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
getJson("/api/resetConfig")
|
|
||||||
.then(function(status){
|
|
||||||
alertRestart();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateMsgDetails(key, details) {
|
|
||||||
forEl('.msgDetails', function (frame) {
|
|
||||||
if (frame.getAttribute('id') !== key) return;
|
|
||||||
for (let k in details) {
|
|
||||||
let el = frame.querySelector("[data-id=\"" + k + "\"] ");
|
|
||||||
if (!el) {
|
|
||||||
el = addEl('div','row',frame);
|
|
||||||
let cv = addEl('span','label',el,k);
|
|
||||||
cv = addEl('span','value',el,details[k]);
|
|
||||||
cv.setAttribute('data-id', k);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
el.textContent = details[k];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function showOverlay(text,isHtml){
|
|
||||||
let el=document.getElementById('overlayContent');
|
|
||||||
if (isHtml){
|
|
||||||
el.innerHTML=text;
|
|
||||||
el.classList.remove("text");
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
el.textContent=text;
|
|
||||||
el.classList.add("text");
|
|
||||||
}
|
|
||||||
let container=document.getElementById('overlayContainer');
|
|
||||||
container.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
function hideOverlay(){
|
|
||||||
let container=document.getElementById('overlayContainer');
|
|
||||||
container.classList.add('hidden');
|
|
||||||
}
|
|
||||||
function checkChange(el,row) {
|
|
||||||
let loaded = el.getAttribute('data-loaded');
|
|
||||||
if (loaded !== undefined) {
|
|
||||||
if (loaded != el.value) {
|
|
||||||
row.classList.add('changed');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
row.classList.remove("changed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function createInput(configItem,frame){
|
|
||||||
let el;
|
|
||||||
if (configItem.type === 'boolean' || configItem.type === 'list'){
|
|
||||||
el=document.createElement('select')
|
|
||||||
el.setAttribute('name',configItem.name)
|
|
||||||
let slist=[];
|
|
||||||
if (configItem.list){
|
|
||||||
configItem.list.forEach(function(v){
|
|
||||||
slist.push({l:v,v:v});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
slist.push({l:'on',v:'true'})
|
|
||||||
slist.push({l:'off',v:'false'})
|
|
||||||
}
|
|
||||||
slist.forEach(function(sitem){
|
|
||||||
let sitemEl=document.createElement('option');
|
|
||||||
sitemEl.setAttribute('value',sitem.v);
|
|
||||||
sitemEl.textContent=sitem.l;
|
|
||||||
el.appendChild(sitemEl);
|
|
||||||
})
|
|
||||||
frame.appendChild(el);
|
|
||||||
return el;
|
|
||||||
}
|
|
||||||
if (configItem.type === 'filter'){
|
|
||||||
el=document.createElement('div');
|
|
||||||
el.classList.add('filter');
|
|
||||||
let ais=createInput({
|
|
||||||
type:'list',
|
|
||||||
name:configItem.name+"_ais",
|
|
||||||
list:['aison','aisoff']
|
|
||||||
},el);
|
|
||||||
let mode=createInput({
|
|
||||||
type:'list',
|
|
||||||
name:configItem.name+"_mode",
|
|
||||||
list: ['whitelist','blacklist']
|
|
||||||
},el);
|
|
||||||
let sentences=createInput({
|
|
||||||
type:'text',
|
|
||||||
name:configItem.name+"_sentences",
|
|
||||||
},el);
|
|
||||||
let data=document.createElement('input');
|
|
||||||
data.setAttribute('type','hidden');
|
|
||||||
el.appendChild(data);
|
|
||||||
let changeFunction=function(){
|
|
||||||
let cv=data.value||"";
|
|
||||||
let parts=cv.split(":");
|
|
||||||
ais.value=(parts[0]=='0')?"aisoff":"aison";
|
|
||||||
mode.value=(parts[1]=='0')?"whitelist":"blacklist";
|
|
||||||
sentences.value=parts[2]||"";
|
|
||||||
}
|
|
||||||
let updateFunction=function(){
|
|
||||||
let nv=(ais.value == 'aison')?"1":"0";
|
|
||||||
nv+=":";
|
|
||||||
nv+=(mode.value == 'blacklist')?"1":"0";
|
|
||||||
nv+=":";
|
|
||||||
nv+=sentences.value;
|
|
||||||
data.value=nv;
|
|
||||||
let chev=new Event('change');
|
|
||||||
data.dispatchEvent(chev);
|
|
||||||
}
|
|
||||||
mode.addEventListener('change',updateFunction);
|
|
||||||
ais.addEventListener("change",updateFunction);
|
|
||||||
sentences.addEventListener("change",updateFunction);
|
|
||||||
data.addEventListener('change',function(ev){
|
|
||||||
changeFunction();
|
|
||||||
});
|
|
||||||
data.setAttribute('name',configItem.name);
|
|
||||||
frame.appendChild(el);
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
el=document.createElement('input');
|
|
||||||
el.setAttribute('name',configItem.name)
|
|
||||||
frame.appendChild(el);
|
|
||||||
if (configItem.type === 'password'){
|
|
||||||
el.setAttribute('type','password');
|
|
||||||
let vis=addEl('span','icon-eye',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 categoryEl;
|
|
||||||
let frame = document.querySelector('.configFormRows');
|
|
||||||
if (!frame) throw Error("no config form");
|
|
||||||
frame.innerHTML = '';
|
|
||||||
configDefinitions = defs;
|
|
||||||
defs.forEach(function (item) {
|
|
||||||
if (!item.type) return;
|
|
||||||
if (item.category != category || ! categoryEl){
|
|
||||||
let categoryFrame=addEl('div','category',frame);
|
|
||||||
let categoryTitle=addEl('div','title',categoryFrame);
|
|
||||||
let categoryButton=addEl('button','categoryButton',categoryTitle,'v');
|
|
||||||
addEl('span','label',categoryTitle,item.category);
|
|
||||||
categoryEl=addEl('div','content',categoryFrame);
|
|
||||||
categoryEl.classList.add('hidden');
|
|
||||||
let currentEl=categoryEl;
|
|
||||||
categoryTitle.addEventListener('click',function(ev){
|
|
||||||
let rs=currentEl.classList.toggle('hidden');
|
|
||||||
if (rs) categoryButton.textContent="v";
|
|
||||||
else categoryButton.textContent="^";
|
|
||||||
})
|
|
||||||
category=item.category;
|
|
||||||
}
|
|
||||||
if (item.capabilities !== undefined){
|
|
||||||
for (let capability in item.capabilities){
|
|
||||||
let values=item.capabilities[capability];
|
|
||||||
if ( !capabilities[capability]) return;
|
|
||||||
let found =false;
|
|
||||||
values.forEach(function(v){
|
|
||||||
if (capabilities[capability] == v) found=true;
|
|
||||||
});
|
|
||||||
if (! found) return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let row = addEl('div','row',categoryEl);
|
|
||||||
let label = item.label || item.name;
|
|
||||||
addEl('span','label',row,label);
|
|
||||||
let valueFrame=addEl('div','value',row);
|
|
||||||
let valueEl = createInput(item,valueFrame);
|
|
||||||
if (!valueEl) return;
|
|
||||||
valueEl.setAttribute('data-default', item.default);
|
|
||||||
valueEl.addEventListener('change', function (ev) {
|
|
||||||
let el = ev.target;
|
|
||||||
checkChange(el,row);
|
|
||||||
})
|
|
||||||
if (item.check) valueEl.setAttribute('data-check', item.check);
|
|
||||||
let btContainer=addEl('div','buttonContainer',row);
|
|
||||||
let bt = addEl('button','defaultButton',btContainer,'X');
|
|
||||||
bt.setAttribute('data-default', item.default);
|
|
||||||
bt.addEventListener('click', function (ev) {
|
|
||||||
valueEl.value = valueEl.getAttribute('data-default');
|
|
||||||
let changeEvent=new Event('change');
|
|
||||||
valueEl.dispatchEvent(changeEvent);
|
|
||||||
})
|
|
||||||
bt = addEl('button','infoButton',btContainer,'?');
|
|
||||||
bt.addEventListener('click', function (ev) {
|
|
||||||
showOverlay(item.description);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
resetForm();
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch(function (err) { alert("unable to load config: " + err) })
|
|
||||||
}
|
|
||||||
function converterInfo(){
|
|
||||||
getJson("api/converterInfo").then(function(json){
|
|
||||||
let text="<h3>Converted entities</h3>";
|
|
||||||
text+="<p><b>NMEA0183 to NMEA2000:</b><br/>";
|
|
||||||
text+=" "+(json.nmea0183||"").replace(/,/g,", ");
|
|
||||||
text+="</p>";
|
|
||||||
text+="<p><b>NMEA2000 to NMEA0183:</b><br/>";
|
|
||||||
text+=" "+(json.nmea2000||"").replace(/,/g,", ");
|
|
||||||
text+="</p>";
|
|
||||||
showOverlay(text,true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function handleTab(el){
|
|
||||||
let activeName=el.getAttribute('data-page');
|
|
||||||
if (! activeName) return;
|
|
||||||
let activeTab=document.getElementById(activeName);
|
|
||||||
if (!activeTab) return;
|
|
||||||
let all=document.querySelectorAll('.tabPage');
|
|
||||||
for (let i=0;i<all.length;i++){
|
|
||||||
all[i].classList.add('hidden');
|
|
||||||
}
|
|
||||||
let tabs=document.querySelectorAll('.tab');
|
|
||||||
for (let i=0;i<all.length;i++){
|
|
||||||
tabs[i].classList.remove('active');
|
|
||||||
}
|
|
||||||
el.classList.add('active');
|
|
||||||
activeTab.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
function createDashboardItem(name,def,parent){
|
|
||||||
let frame=addEl('div','dash',parent);
|
|
||||||
let title=addEl('span','dashTitle',frame,name);
|
|
||||||
let value=addEl('span','dashValue',frame);
|
|
||||||
value.setAttribute('id','data_'+name);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
function createDashboard(){
|
|
||||||
let frame=document.getElementById('dashboardPage');
|
|
||||||
if (! frame) return;
|
|
||||||
getJson("api/boatData").then(function(json){
|
|
||||||
frame.innerHTML='';
|
|
||||||
for (let n in json){
|
|
||||||
createDashboardItem(n,json[n],frame);
|
|
||||||
}
|
|
||||||
updateDashboard(json);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let valueFormatters={
|
|
||||||
formatCourse: function(v){let x=parseFloat(v); return x.toFixed(0);},
|
|
||||||
formatKnots: function(v){let x=parseFloat(v); return x.toFixed(2);},
|
|
||||||
formatWind: function(v){let x=parseFloat(v); return x.toFixed(0);},
|
|
||||||
mtr2nm: function(v){let x=parseFloat(v); return x.toFixed(2);},
|
|
||||||
kelvinToC: function(v){let x=parseFloat(v); return x.toFixed(0);},
|
|
||||||
formatFixed0: function(v){let x=parseFloat(v); return x.toFixed(0);},
|
|
||||||
formatDepth: function(v){let x=parseFloat(v); return x.toFixed(1);},
|
|
||||||
}
|
|
||||||
function updateDashboard(data){
|
|
||||||
for (let n in data){
|
|
||||||
let de=document.getElementById('data_'+n);
|
|
||||||
if (de){
|
|
||||||
if (data[n].valid){
|
|
||||||
let formatter;
|
|
||||||
if (data[n].format && data[n].format != "NULL"){
|
|
||||||
let key=data[n].format.replace(/^\&/,'');
|
|
||||||
formatter=valueFormatters[key];
|
|
||||||
}
|
|
||||||
if (formatter){
|
|
||||||
de.textContent=formatter(data[n].value);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
let v = parseFloat(data[n].value);
|
|
||||||
if (!isNaN(v)) {
|
|
||||||
v = v.toFixed(3)
|
|
||||||
de.textContent = v;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
de.textContent = data[n].value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else de.textContent="---";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.setInterval(update,1000);
|
|
||||||
window.setInterval(function(){
|
|
||||||
let dp=document.getElementById('dashboardPage');
|
|
||||||
if (dp.classList.contains('hidden')) return;
|
|
||||||
getJson('api/boatData').then(function(data){
|
|
||||||
updateDashboard(data);
|
|
||||||
});
|
|
||||||
},1000);
|
|
||||||
window.addEventListener('load',function(){
|
|
||||||
let buttons=document.querySelectorAll('button');
|
|
||||||
for (let i=0;i<buttons.length;i++){
|
|
||||||
let be=buttons[i];
|
|
||||||
be.onclick=window[be.id]; //assume a function with the button id
|
|
||||||
}
|
|
||||||
forEl('.showMsgDetails',function(cd){
|
|
||||||
cd.addEventListener('change',function(ev){
|
|
||||||
let key=ev.target.getAttribute('data-key');
|
|
||||||
if (! key) return;
|
|
||||||
let el=document.getElementById(key);
|
|
||||||
if (!el) return;
|
|
||||||
if (ev.target.checked) el.classList.remove('hidden');
|
|
||||||
else(el.classList).add('hidden');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
let tabs=document.querySelectorAll('.tab');
|
|
||||||
for (let i=0;i<tabs.length;i++){
|
|
||||||
tabs[i].addEventListener('click',function(ev){
|
|
||||||
handleTab(ev.target);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
loadConfigDefinitions();
|
|
||||||
createDashboard();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<style type="text/css">
|
|
||||||
body{
|
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
}
|
|
||||||
.configForm {
|
|
||||||
padding-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
.configForm .buttons {
|
|
||||||
margin-top: 0.5em;
|
|
||||||
}
|
|
||||||
.configForm .content>div:nth-child(even) {
|
|
||||||
background-color: rgb(211 211 211 / 43%);
|
|
||||||
}
|
|
||||||
#statusPage .even {
|
|
||||||
background-color: rgb(211 211 211 / 43%);
|
|
||||||
}
|
|
||||||
.category .title .label {
|
|
||||||
opacity: 1;
|
|
||||||
margin-left: 1em;
|
|
||||||
}
|
|
||||||
.changed input{
|
|
||||||
color: green
|
|
||||||
}
|
|
||||||
.changed select{
|
|
||||||
color: green
|
|
||||||
}
|
|
||||||
span.label {
|
|
||||||
width: 10em;
|
|
||||||
display: inline-block;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
.configForm .value {
|
|
||||||
width: 21em;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
margin-bottom: 0.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
span#connected {
|
|
||||||
display: inline-block;
|
|
||||||
background-color: red;
|
|
||||||
width: 1em;
|
|
||||||
height: 1em;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
span#connected.ok{
|
|
||||||
background-color: #13ac13;
|
|
||||||
}
|
|
||||||
.row {
|
|
||||||
padding: 0.5em;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
input,select {
|
|
||||||
border: 1px solid #808080a1;
|
|
||||||
font-size: 0.9em;
|
|
||||||
padding: 0.2em;
|
|
||||||
}
|
|
||||||
.filter {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
.buttons {
|
|
||||||
padding-left: 1em;
|
|
||||||
}
|
|
||||||
button.infoButton {
|
|
||||||
margin-left: 1em;
|
|
||||||
vertical-align: bottom;
|
|
||||||
}
|
|
||||||
.category .title {
|
|
||||||
padding-left: 0.5em;
|
|
||||||
background-color: lightgray;
|
|
||||||
padding-top: 0.3em;
|
|
||||||
padding-bottom: 0.5em;
|
|
||||||
border-bottom: 1px solid darkgray;
|
|
||||||
}
|
|
||||||
.hidden{
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
.msgDetails .value {
|
|
||||||
width: 5em;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
.msgDetails .label {
|
|
||||||
width: 5em;
|
|
||||||
}
|
|
||||||
.overlayContainer {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
background-color: #80808070;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
div#overlay {
|
|
||||||
margin: auto;
|
|
||||||
background-color: white;
|
|
||||||
padding: 0.5em;
|
|
||||||
max-width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
div#overlayContent {
|
|
||||||
padding: 0.5em;
|
|
||||||
}
|
|
||||||
div#overlayContent.text{
|
|
||||||
white-space: pre-line;
|
|
||||||
}
|
|
||||||
.overlayButtons {
|
|
||||||
border-top: 1px solid grey;
|
|
||||||
padding-top: 0.5em;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: end;
|
|
||||||
}
|
|
||||||
#tabs {
|
|
||||||
display: flex;
|
|
||||||
border-bottom: 1px solid grey;
|
|
||||||
margin-bottom: 0.5em;
|
|
||||||
}
|
|
||||||
#tabs .tab {
|
|
||||||
background-color: lightgray;
|
|
||||||
padding: 0.5em;
|
|
||||||
border-left: 1px;
|
|
||||||
border-right: 1px;
|
|
||||||
border-top: 1px;
|
|
||||||
border-bottom: 1px;
|
|
||||||
border-color: grey;
|
|
||||||
border-style: solid;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
#tabs .tab.active{
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
.buttons button{
|
|
||||||
padding: 0.5em;
|
|
||||||
}
|
|
||||||
button#reset{
|
|
||||||
padding: 0.5em;
|
|
||||||
}
|
|
||||||
h1{
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
.icon-eye{
|
|
||||||
background-image: url("data:image/svg+xml;utf-8,<svg width='24' height='24' xmlns='http://www.w3.org/2000/svg' xmlns:svg='http://www.w3.org/2000/svg'> <g class='layer'> <title>Layer 1</title> <path d='m0,0l24,0l0,24l-24,0l0,-24z' fill='none' id='svg_1'/> <path d='m12,6c3.79,0 7.17,2.13 8.82,5.5c-1.65,3.37 -5.03,5.5 -8.82,5.5s-7.17,-2.13 -8.82,-5.5c1.65,-3.37 5.03,-5.5 8.82,-5.5m0,-2c-5,0 -9.27,3.11 -11,7.5c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zm0,5c1.38,0 2.5,1.12 2.5,2.5s-1.12,2.5 -2.5,2.5s-2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5m0,-2c-2.48,0 -4.5,2.02 -4.5,4.5s2.02,4.5 4.5,4.5s4.5,-2.02 4.5,-4.5s-2.02,-4.5 -4.5,-4.5z' id='svg_2'/> </g> </svg>");
|
|
||||||
height: 1.5em;
|
|
||||||
width: 1.5em;
|
|
||||||
margin-left: 0.5em;
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
.icon-eye.active{
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dash {
|
|
||||||
width: 6em;
|
|
||||||
height: 4em;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
border: 1px solid grey;
|
|
||||||
overflow: hidden;
|
|
||||||
margin: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
font-size: 1.2em;
|
|
||||||
}
|
|
||||||
div#dashboardPage {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.dashTitle {
|
|
||||||
font-size: 0.8em;
|
|
||||||
background-color: lightgray;
|
|
||||||
}
|
|
||||||
.dashValue{
|
|
||||||
font-size: 1.6em;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
|
|
|
@ -0,0 +1,480 @@
|
||||||
|
let self = this;
|
||||||
|
let lastUpdate = (new Date()).getTime();
|
||||||
|
let reloadConfig = false;
|
||||||
|
function addEl(type, clazz, parent, text) {
|
||||||
|
let el = document.createElement(type);
|
||||||
|
if ( ! (clazz instanceof Array)){
|
||||||
|
clazz=clazz.split(/ */);
|
||||||
|
}
|
||||||
|
clazz.forEach(function(ce){
|
||||||
|
el.classList.add(ce);
|
||||||
|
});
|
||||||
|
if (text) el.textContent = text;
|
||||||
|
if (parent) parent.appendChild(el);
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
function forEl(query, callback) {
|
||||||
|
let all = document.querySelectorAll(query);
|
||||||
|
for (let i = 0; i < all.length; i++) {
|
||||||
|
callback(all[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function alertRestart() {
|
||||||
|
reloadConfig = true;
|
||||||
|
alert("Board reset triggered, reconnect WLAN if necessary");
|
||||||
|
}
|
||||||
|
function getJson(url) {
|
||||||
|
return fetch(url)
|
||||||
|
.then(function (r) { return r.json() });
|
||||||
|
}
|
||||||
|
function reset() {
|
||||||
|
fetch('/api/reset');
|
||||||
|
alertRestart();
|
||||||
|
}
|
||||||
|
function update() {
|
||||||
|
let now = (new Date()).getTime();
|
||||||
|
let ce = document.getElementById('connected');
|
||||||
|
if (ce) {
|
||||||
|
if ((lastUpdate + 3000) > now) {
|
||||||
|
ce.classList.add('ok');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ce.classList.remove('ok');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getJson('/api/status')
|
||||||
|
.then(function (jsonData) {
|
||||||
|
for (let k in jsonData) {
|
||||||
|
if (typeof (jsonData[k]) === 'object') {
|
||||||
|
for (let sk in jsonData[k]) {
|
||||||
|
let key = k + "." + sk;
|
||||||
|
if (typeof (jsonData[k][sk]) === 'object') {
|
||||||
|
//msg details
|
||||||
|
updateMsgDetails(key, jsonData[k][sk]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let el = document.getElementById(key);
|
||||||
|
if (el) el.textContent = jsonData[k][sk];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let el = document.getElementById(k);
|
||||||
|
if (el) el.textContent = jsonData[k];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastUpdate = (new Date()).getTime();
|
||||||
|
if (reloadConfig) {
|
||||||
|
reloadConfig = false;
|
||||||
|
resetForm();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function resetForm(ev) {
|
||||||
|
getJson("/api/config")
|
||||||
|
.then(function (jsonData) {
|
||||||
|
for (let k in jsonData) {
|
||||||
|
let el = document.querySelector("[name='" + k + "']");
|
||||||
|
if (el) {
|
||||||
|
let v = jsonData[k];
|
||||||
|
el.value = v;
|
||||||
|
el.setAttribute('data-loaded', v);
|
||||||
|
let changeEvent = new Event('change');
|
||||||
|
el.dispatchEvent(changeEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function checkMaxClients(v) {
|
||||||
|
let parsed = parseInt(v);
|
||||||
|
if (isNaN(parsed)) return "not a valid number";
|
||||||
|
if (parsed < 0) return "must be >= 0";
|
||||||
|
if (parsed > 10) return "max is 10";
|
||||||
|
}
|
||||||
|
function checkSystemName(v) {
|
||||||
|
//2...32 characters for ssid
|
||||||
|
let allowed = v.replace(/[^a-zA-Z0-9]*/g, '');
|
||||||
|
if (allowed != v) return "contains invalid characters, only a-z, A-Z, 0-9";
|
||||||
|
if (v.length < 2 || v.length > 32) return "invalid length (2...32)";
|
||||||
|
}
|
||||||
|
function checkApPass(v) {
|
||||||
|
//min 8 characters
|
||||||
|
if (v.length < 8) {
|
||||||
|
return "password must be at least 8 characters";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function changeConfig() {
|
||||||
|
let url = "/api/setConfig?";
|
||||||
|
let values = document.querySelectorAll('.configForm select , .configForm input');
|
||||||
|
for (let i = 0; i < values.length; i++) {
|
||||||
|
let v = values[i];
|
||||||
|
let name = v.getAttribute('name');
|
||||||
|
if (!name) continue;
|
||||||
|
if (name.indexOf("_") >= 0) continue;
|
||||||
|
let check = v.getAttribute('data-check');
|
||||||
|
if (check) {
|
||||||
|
if (typeof (self[check]) === 'function') {
|
||||||
|
let res = self[check](v.value);
|
||||||
|
if (res) {
|
||||||
|
let value = v.value;
|
||||||
|
if (v.type === 'password') value = "******";
|
||||||
|
alert("invalid config for " + v.getAttribute('name') + "(" + value + "):\n" + res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
url += name + "=" + encodeURIComponent(v.value) + "&";
|
||||||
|
}
|
||||||
|
getJson(url)
|
||||||
|
.then(function (status) {
|
||||||
|
if (status.status == 'OK') {
|
||||||
|
alertRestart();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
alert("unable to set config: " + status.status);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function factoryReset() {
|
||||||
|
if (!confirm("Really delete all configuration?\n" +
|
||||||
|
"This will reset all your Wifi settings and disconnect you.")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getJson("/api/resetConfig")
|
||||||
|
.then(function (status) {
|
||||||
|
alertRestart();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMsgDetails(key, details) {
|
||||||
|
forEl('.msgDetails', function (frame) {
|
||||||
|
if (frame.getAttribute('id') !== key) return;
|
||||||
|
for (let k in details) {
|
||||||
|
let el = frame.querySelector("[data-id=\"" + k + "\"] ");
|
||||||
|
if (!el) {
|
||||||
|
el = addEl('div', 'row', frame);
|
||||||
|
let cv = addEl('span', 'label', el, k);
|
||||||
|
cv = addEl('span', 'value', el, details[k]);
|
||||||
|
cv.setAttribute('data-id', k);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
el.textContent = details[k];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function showOverlay(text, isHtml) {
|
||||||
|
let el = document.getElementById('overlayContent');
|
||||||
|
if (isHtml) {
|
||||||
|
el.innerHTML = text;
|
||||||
|
el.classList.remove("text");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
el.textContent = text;
|
||||||
|
el.classList.add("text");
|
||||||
|
}
|
||||||
|
let container = document.getElementById('overlayContainer');
|
||||||
|
container.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
function hideOverlay() {
|
||||||
|
let container = document.getElementById('overlayContainer');
|
||||||
|
container.classList.add('hidden');
|
||||||
|
}
|
||||||
|
function checkChange(el, row) {
|
||||||
|
let loaded = el.getAttribute('data-loaded');
|
||||||
|
if (loaded !== undefined) {
|
||||||
|
if (loaded != el.value) {
|
||||||
|
row.classList.add('changed');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
row.classList.remove("changed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function createInput(configItem, frame) {
|
||||||
|
let el;
|
||||||
|
if (configItem.type === 'boolean' || configItem.type === 'list') {
|
||||||
|
el = document.createElement('select')
|
||||||
|
el.setAttribute('name', configItem.name)
|
||||||
|
let slist = [];
|
||||||
|
if (configItem.list) {
|
||||||
|
configItem.list.forEach(function (v) {
|
||||||
|
slist.push({ l: v, v: v });
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
slist.push({ l: 'on', v: 'true' })
|
||||||
|
slist.push({ l: 'off', v: 'false' })
|
||||||
|
}
|
||||||
|
slist.forEach(function (sitem) {
|
||||||
|
let sitemEl = document.createElement('option');
|
||||||
|
sitemEl.setAttribute('value', sitem.v);
|
||||||
|
sitemEl.textContent = sitem.l;
|
||||||
|
el.appendChild(sitemEl);
|
||||||
|
})
|
||||||
|
frame.appendChild(el);
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
if (configItem.type === 'filter') {
|
||||||
|
el = document.createElement('div');
|
||||||
|
el.classList.add('filter');
|
||||||
|
let ais = createInput({
|
||||||
|
type: 'list',
|
||||||
|
name: configItem.name + "_ais",
|
||||||
|
list: ['aison', 'aisoff']
|
||||||
|
}, el);
|
||||||
|
let mode = createInput({
|
||||||
|
type: 'list',
|
||||||
|
name: configItem.name + "_mode",
|
||||||
|
list: ['whitelist', 'blacklist']
|
||||||
|
}, el);
|
||||||
|
let sentences = createInput({
|
||||||
|
type: 'text',
|
||||||
|
name: configItem.name + "_sentences",
|
||||||
|
}, el);
|
||||||
|
let data = document.createElement('input');
|
||||||
|
data.setAttribute('type', 'hidden');
|
||||||
|
el.appendChild(data);
|
||||||
|
let changeFunction = function () {
|
||||||
|
let cv = data.value || "";
|
||||||
|
let parts = cv.split(":");
|
||||||
|
ais.value = (parts[0] == '0') ? "aisoff" : "aison";
|
||||||
|
mode.value = (parts[1] == '0') ? "whitelist" : "blacklist";
|
||||||
|
sentences.value = parts[2] || "";
|
||||||
|
}
|
||||||
|
let updateFunction = function () {
|
||||||
|
let nv = (ais.value == 'aison') ? "1" : "0";
|
||||||
|
nv += ":";
|
||||||
|
nv += (mode.value == 'blacklist') ? "1" : "0";
|
||||||
|
nv += ":";
|
||||||
|
nv += sentences.value;
|
||||||
|
data.value = nv;
|
||||||
|
let chev = new Event('change');
|
||||||
|
data.dispatchEvent(chev);
|
||||||
|
}
|
||||||
|
mode.addEventListener('change', updateFunction);
|
||||||
|
ais.addEventListener("change", updateFunction);
|
||||||
|
sentences.addEventListener("change", updateFunction);
|
||||||
|
data.addEventListener('change', function (ev) {
|
||||||
|
changeFunction();
|
||||||
|
});
|
||||||
|
data.setAttribute('name', configItem.name);
|
||||||
|
frame.appendChild(el);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
el = document.createElement('input');
|
||||||
|
el.setAttribute('name', configItem.name)
|
||||||
|
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 categoryEl;
|
||||||
|
let frame = document.querySelector('.configFormRows');
|
||||||
|
if (!frame) throw Error("no config form");
|
||||||
|
frame.innerHTML = '';
|
||||||
|
configDefinitions = defs;
|
||||||
|
defs.forEach(function (item) {
|
||||||
|
if (!item.type) return;
|
||||||
|
if (item.category != category || !categoryEl) {
|
||||||
|
let categoryFrame = addEl('div', 'category', frame);
|
||||||
|
let categoryTitle = addEl('div', 'title', categoryFrame);
|
||||||
|
let categoryButton = addEl('span', 'icon icon-more', categoryTitle);
|
||||||
|
addEl('span', 'label', categoryTitle, item.category);
|
||||||
|
categoryEl = addEl('div', 'content', categoryFrame);
|
||||||
|
categoryEl.classList.add('hidden');
|
||||||
|
let currentEl = categoryEl;
|
||||||
|
categoryTitle.addEventListener('click', function (ev) {
|
||||||
|
let rs = currentEl.classList.toggle('hidden');
|
||||||
|
if (rs) {
|
||||||
|
categoryButton.classList.add('icon-more');
|
||||||
|
categoryButton.classList.remove('icon-less');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
categoryButton.classList.remove('icon-more');
|
||||||
|
categoryButton.classList.add('icon-less');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
category = item.category;
|
||||||
|
}
|
||||||
|
if (item.capabilities !== undefined) {
|
||||||
|
for (let capability in item.capabilities) {
|
||||||
|
let values = item.capabilities[capability];
|
||||||
|
if (!capabilities[capability]) return;
|
||||||
|
let found = false;
|
||||||
|
values.forEach(function (v) {
|
||||||
|
if (capabilities[capability] == v) found = true;
|
||||||
|
});
|
||||||
|
if (!found) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let row = addEl('div', 'row', categoryEl);
|
||||||
|
let label = item.label || item.name;
|
||||||
|
addEl('span', 'label', row, label);
|
||||||
|
let valueFrame = addEl('div', 'value', row);
|
||||||
|
let valueEl = createInput(item, valueFrame);
|
||||||
|
if (!valueEl) return;
|
||||||
|
valueEl.setAttribute('data-default', item.default);
|
||||||
|
valueEl.addEventListener('change', function (ev) {
|
||||||
|
let el = ev.target;
|
||||||
|
checkChange(el, row);
|
||||||
|
})
|
||||||
|
if (item.check) valueEl.setAttribute('data-check', item.check);
|
||||||
|
let btContainer = addEl('div', 'buttonContainer', row);
|
||||||
|
let bt = addEl('button', 'defaultButton', btContainer, 'X');
|
||||||
|
bt.setAttribute('data-default', item.default);
|
||||||
|
bt.addEventListener('click', function (ev) {
|
||||||
|
valueEl.value = valueEl.getAttribute('data-default');
|
||||||
|
let changeEvent = new Event('change');
|
||||||
|
valueEl.dispatchEvent(changeEvent);
|
||||||
|
})
|
||||||
|
bt = addEl('button', 'infoButton', btContainer, '?');
|
||||||
|
bt.addEventListener('click', function (ev) {
|
||||||
|
showOverlay(item.description);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
resetForm();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(function (err) { alert("unable to load config: " + err) })
|
||||||
|
}
|
||||||
|
function converterInfo() {
|
||||||
|
getJson("api/converterInfo").then(function (json) {
|
||||||
|
let text = "<h3>Converted entities</h3>";
|
||||||
|
text += "<p><b>NMEA0183 to NMEA2000:</b><br/>";
|
||||||
|
text += " " + (json.nmea0183 || "").replace(/,/g, ", ");
|
||||||
|
text += "</p>";
|
||||||
|
text += "<p><b>NMEA2000 to NMEA0183:</b><br/>";
|
||||||
|
text += " " + (json.nmea2000 || "").replace(/,/g, ", ");
|
||||||
|
text += "</p>";
|
||||||
|
showOverlay(text, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function handleTab(el) {
|
||||||
|
let activeName = el.getAttribute('data-page');
|
||||||
|
if (!activeName) return;
|
||||||
|
let activeTab = document.getElementById(activeName);
|
||||||
|
if (!activeTab) return;
|
||||||
|
let all = document.querySelectorAll('.tabPage');
|
||||||
|
for (let i = 0; i < all.length; i++) {
|
||||||
|
all[i].classList.add('hidden');
|
||||||
|
}
|
||||||
|
let tabs = document.querySelectorAll('.tab');
|
||||||
|
for (let i = 0; i < all.length; i++) {
|
||||||
|
tabs[i].classList.remove('active');
|
||||||
|
}
|
||||||
|
el.classList.add('active');
|
||||||
|
activeTab.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
function createDashboardItem(name, def, parent) {
|
||||||
|
let frame = addEl('div', 'dash', parent);
|
||||||
|
let title = addEl('span', 'dashTitle', frame, name);
|
||||||
|
let value = addEl('span', 'dashValue', frame);
|
||||||
|
value.setAttribute('id', 'data_' + name);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
function createDashboard() {
|
||||||
|
let frame = document.getElementById('dashboardPage');
|
||||||
|
if (!frame) return;
|
||||||
|
getJson("api/boatData").then(function (json) {
|
||||||
|
frame.innerHTML = '';
|
||||||
|
for (let n in json) {
|
||||||
|
createDashboardItem(n, json[n], frame);
|
||||||
|
}
|
||||||
|
updateDashboard(json);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let valueFormatters = {
|
||||||
|
formatCourse: function (v) { let x = parseFloat(v); return x.toFixed(0); },
|
||||||
|
formatKnots: function (v) { let x = parseFloat(v); return x.toFixed(2); },
|
||||||
|
formatWind: function (v) { let x = parseFloat(v); return x.toFixed(0); },
|
||||||
|
mtr2nm: function (v) { let x = parseFloat(v); return x.toFixed(2); },
|
||||||
|
kelvinToC: function (v) { let x = parseFloat(v); return x.toFixed(0); },
|
||||||
|
formatFixed0: function (v) { let x = parseFloat(v); return x.toFixed(0); },
|
||||||
|
formatDepth: function (v) { let x = parseFloat(v); return x.toFixed(1); },
|
||||||
|
}
|
||||||
|
function updateDashboard(data) {
|
||||||
|
for (let n in data) {
|
||||||
|
let de = document.getElementById('data_' + n);
|
||||||
|
if (de) {
|
||||||
|
if (data[n].valid) {
|
||||||
|
let formatter;
|
||||||
|
if (data[n].format && data[n].format != "NULL") {
|
||||||
|
let key = data[n].format.replace(/^\&/, '');
|
||||||
|
formatter = valueFormatters[key];
|
||||||
|
}
|
||||||
|
if (formatter) {
|
||||||
|
de.textContent = formatter(data[n].value);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let v = parseFloat(data[n].value);
|
||||||
|
if (!isNaN(v)) {
|
||||||
|
v = v.toFixed(3)
|
||||||
|
de.textContent = v;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
de.textContent = data[n].value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else de.textContent = "---";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.setInterval(update, 1000);
|
||||||
|
window.setInterval(function () {
|
||||||
|
let dp = document.getElementById('dashboardPage');
|
||||||
|
if (dp.classList.contains('hidden')) return;
|
||||||
|
getJson('api/boatData').then(function (data) {
|
||||||
|
updateDashboard(data);
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
window.addEventListener('load', function () {
|
||||||
|
let buttons = document.querySelectorAll('button');
|
||||||
|
for (let i = 0; i < buttons.length; i++) {
|
||||||
|
let be = buttons[i];
|
||||||
|
be.onclick = window[be.id]; //assume a function with the button id
|
||||||
|
}
|
||||||
|
forEl('.showMsgDetails', function (cd) {
|
||||||
|
cd.addEventListener('change', function (ev) {
|
||||||
|
let key = ev.target.getAttribute('data-key');
|
||||||
|
if (!key) return;
|
||||||
|
let el = document.getElementById(key);
|
||||||
|
if (!el) return;
|
||||||
|
if (ev.target.checked) el.classList.remove('hidden');
|
||||||
|
else (el.classList).add('hidden');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
let tabs = document.querySelectorAll('.tab');
|
||||||
|
for (let i = 0; i < tabs.length; i++) {
|
||||||
|
tabs[i].addEventListener('click', function (ev) {
|
||||||
|
handleTab(ev.target);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
loadConfigDefinitions();
|
||||||
|
createDashboard();
|
||||||
|
});
|
Loading…
Reference in New Issue