signalk-dashboard
Version:
Server plugin & Grafana dashboard for SignalK data
320 lines (298 loc) • 11.1 kB
JavaScript
const Proc = require('child_process').exec;
const { Client } = require('ssh2');
const fs = require('fs');
const yaml = require('js-yaml');
const log = require('debug')('signalk-dashboard:grafana')
let err = console.error
let setStatus
let setError
let configFile
const checkchrom = "pgrep chromium | head -1"
const checkkiosk = "sudo systemctl status grafana-kiosk | grep 'Active:' | awk '{print $2}'"
const closebrowser = "kill {pid}"
const setbrightness = "echo {val} | sudo tee /sys/class/backlight/*/brightness"
const chromium = "chromium-browser {uri} {params}"
const tabbed = "chromium-browser [tabs] {params}"
const nexttab = "wtype -M ctrl -P Tab"
const kiosk = "sudo systemctl restart {service}"
let command
let exec
let boards = {
default: {}
}
let current = {
board: undefined,
brightness: undefined
}
let rl = {
host: '',
port: 22,
user: '',
key: null
}
function init (config, logger, set, err) {
// store content to file
setStatus = set
setError = err
if (config && config.launch)
{
boards.default = {
tab: 0,
id: config.dashboard.idle,
params: config.dashboard.params
}
if (config.boards && config.boards.length>0)
{
if (config.launch.command.includes('chromium'))
config.launch.command = "chromium-tabbed"
let i=1
config.boards.forEach(b => {
boards[b.state] = {
tab: i++,
id: b.uid,
params: b.params
}
})
}
if (config.launch.remote)
{
exec = config.launch.remote!=="" ? config.launch.remote+' "{cmd}"' : '{cmd}'
let login = config.launch.remote.split(" ")
if (login.length>0)
{
rl.host = login[login.length-1].split("@")[1]
rl.user = login[login.length-1].split("@")[0]
rl.key = fs.readFileSync(config.launch.keyFile)
}
}
else
exec = '{cmd}'
switch (config.launch.command)
{
case "chromium-browser":
let url = `'${config.dashboard.server}/d/${boards.default.id}?kiosk${(boards.default.params ? `&${boards.default.params}` : '')}'`
command = exec.replace("{cmd}", chromium.replace("{uri}", url).replace("{params}", config.launch.params) + ' &')
break;
case "chromium-tabbed":
let urls = []
Object.keys(boards).forEach(b => {
urls.push(`'${config.dashboard.server}/d/${boards[b].id}?kiosk${(b.params ? `&${boards[b].params}` : '')}'`)
})
command = exec.replace("{cmd}", tabbed.replace("[tabs]", urls.join(' ')).replace("{params}", config.launch.params) + ' &')
break;
case "grafana-kiosk":
configFile = config.launch.params
Object.keys(boards).forEach(b => {
boards[b].url = `${config.dashboard.server}/d/${boards[b].id}?kiosk${(boards[b].params ? `&${boards[b].params}` : '')}`
})
updateConfig(configFile, boards.default.url, () => { log(`Configured launch URL: ${boards.default.url}`) })
command = exec.replace("{cmd}", kiosk.replace("{service}", config.launch.command))
break;
default:
if (config.autostart)
err('invalid grafana configuration')
return false;
}
}
log(`Grafana autostart: ${!config || !config.launch ? "none" : command}`)
return command && command !== ""
}
function check(msg, callback) {
Proc(exec.replace("{cmd}", command.includes("chromium") ? checkchrom : checkkiosk), (error,stdout,stderr) => {
if (error) {
setError(`grafana error: ${error || stderr}`)
err( `exec error: ${error ? error : stderr}` );
return;
}
setStatus(`Started: ${msg.replace("{pid}", stdout.replace("\n", ""))}`)
callback(stdout.replace("\n", ""))
})
}
function launch (status, callback) {
if (status==='') {
let msg = command.includes("chromium") ? chromium.replace("uri","pid").replace("{params}",`${Object.keys(boards).length} tab(s)`) : kiosk
let timer = setInterval( () => {
check(msg, callback)
current.board = 0
clearInterval(timer);
}, 15000)
Proc("nohup " + command + " >/dev/null 2>&1 </dev/null", (error,stdout,stderr) => { return; })
} else if (!command.includes("chromium")) {
log(`Grafana kiosk service is ${status}`)
Proc(status==='active' ? command : command.replace("restart", "start"), (error,stdout,stderr) => {
if (error) {
setError(`grafana error: ${error || stderr}`)
err( `exec error: ${error ? error : stderr}` );
return;
}
check("Grafana kiosk-service {pid}", callback)
})
} else {
log(`Status check: ${status}`)
Proc(exec.replace("{cmd}", closebrowser.replace("{pid}", status)), (error,stdout,stderr) => {
if (error) {
setError(`grafana error: ${error || stderr}`)
err( `exec error: ${error ? error : stderr}` );
return;
}
launch(stdout, callback)
})
}
}
function next (state, callback) {
if (command.includes("chromium"))
nextTab (state, callback)
else
newBoard (state, callback)
}
function nextTab (state, callback) {
let next = 0
let length = Object.keys(boards).length
let steps = 0
if (!boards.hasOwnProperty(state) && current.board !== 0)
next = 0
else
next = boards[state].tab
if (current.board !== next)
steps = next > current.board ? next - current.board : length - current.board + next
if (steps !== 0)
{
let cmd = nexttab
for (i=1; i<steps; i++)
cmd += ` && sleep 1 && ${nexttab} `
log(`Switching tab from ${current.board} to ${next} ...`)
Proc(exec.replace("{cmd}", cmd), (error,stdout,stderr) => {
if (error) {
err( `exec error: ${error ? error : stderr}` );
return;
}
current.board = next
callback()
})
}
}
function newBoard (state, callback)
{
log(`Switching board to ${state} ...`)
updateConfig(configFile, boards[boards.hasOwnProperty(state) ? state : 'default'].url, () => {
Proc("sleep 2 && " +command, (error,stdout,stderr) => {
if (error) {
err( `exec error: ${error ? error : stderr}` );
return;
}
callback()
})
})
}
function updateConfig(configFile, url, callback)
{
if (exec.includes('ssh'))
modifyRemoteYaml(configFile, url, callback)
else {
modifyLocalYaml(configFile, url)
callback()
}
}
function modifyLocalYaml(file, url) {
try {
// Read the YAML file
const fileContents = fs.readFileSync(file, 'utf8');
const data = yaml.load(fileContents);
// Modify the URL parameter
if (data.hasOwnProperty('target') && data.target &&
data.target.hasOwnProperty('URL') && data.target.URL)
{
data.target.URL = url;
} else {
throw new Error('URL parameter not found in the YAML file.');
}
// Write the updated YAML back to the file
const newYaml = yaml.dump(data);
fs.writeFileSync(file, newYaml, 'utf8');
log('Board URL updated');
} catch (e) {
err('Error updating the board URL:', e);
}
}
function modifyRemoteYaml(file, url, callback) {
const conn = new Client();
try
{
conn.on('ready', () => {
// log('SSH Client :: ready');
conn.sftp((err, sftp) => {
if (err) throw err;
// Read the remote YAML file
sftp.readFile(file, 'utf8', (err, data) => {
try
{
if (err) throw err;
// Parse the YAML file
let yamlData = yaml.load(data);
// Modify the URL parameter
if (yamlData.target && yamlData.target.URL) {
yamlData.target.URL = url;
} else {
throw new Error('URL parameter not found in the YAML file.');
}
// Convert the modified data back to YAML
const newYaml = yaml.dump(yamlData);
// Write the modified YAML back to the remote file
sftp.writeFile(file, newYaml, 'utf8', (err) => {
if (err) throw err;
log('Board URL updated on remote');
conn.end();
});
// next action
callback()
} catch (e) {
conn.end();
log(`Error updating the board URL: ${e}`);
}
});
});
}).connect({
host: rl.host,
port: rl.port,
username: rl.user,
privateKey: rl.key
});
} catch (e) {
err('Error updating the board URL:', e);
}
}
function set (param, value, callback) {
switch (param) {
case 'brightness':
if (!value || value < 0 || value > 255) {
log(`Invalid brightness value: ${value}`)
callback(current.brightness)
} else if (value !== current.brightness) {
// log(`Setting brightness to ${value} ...`)
Proc(exec.replace("{cmd}", setbrightness.replace("{val}", value)), (error,stdout,stderr) => {
if (error) {
err( `exec error: ${error ? error : stderr}` );
return;
}
current.brightness = value
log(`Brightness set to ${value}`)
callback(value)
})
} else {
log(`Brightness already set to ${value}`)
callback(current.brightness)
}
break;
default:
log(`Screen configuration - invalid parameter: ${param}`)
callback(undefined)
break;
}
}
module.exports = {
init, // initialize environment
check, // check kiosk service is already runnning
launch, // launch kiosk service
next, // switch visible board to new state
set // set board param to value (eg. brightness)
}