Verified Commit 50580a98 authored by Peter Müller's avatar Peter Müller
Browse files

Updated control app

Control app now supports v1.x and v2.0.0
parent 574a381a
......@@ -2,6 +2,7 @@
- Added support for DS18B20, DS18S20, DS1820, DS1822 temperature sensors
- Added polling mode
- Changed some RadioHead messages
- Updated control app to support old and new version of the automatic watering system
## v1.0.3 - 2018-06-04
- Added fix for unplausible values read from the DHT sensor
......
File mode changed from 100755 to 100644
......@@ -3,15 +3,31 @@
*
* Frontend
*
* (c) 2018 Peter Müller <peter@crycode.de> (https://crycode.de)
* (c) 2018-2020 Peter Müller <peter@crycode.de> (https://crycode.de)
*/
'use strict';
/**
* Function to check if version A is greater or equal to version B.
*/
function checkVersionGe (vA, vB) {
const [, vAmajor, vAminor, vArev] = vA.match(/(\d+)\.(\d+)\.(\d+)$/);
const [, vBmajor, vBminor, vBrev] = vB.match(/(\d+)\.(\d+)\.(\d+)$/);
if (parseInt(vAmajor, 10) < parseInt(vBmajor, 10)) return false;
if (parseInt(vAminor, 10) < parseInt(vBminor, 10)) return false;
if (parseInt(vArev, 10) < parseInt(vBrev, 10)) return false;
return true;
}
class WateringClient {
constructor () {
document.getElementById('checkNowButton').onclick = this.apiCheckNow;
document.getElementById('pingButton').onclick = this.apiPing;
document.getElementById('pollButton').onclick = this.apiPoll;
document.getElementById('connectButton').onclick = this.apiConnect;
document.getElementById('disconnectButton').onclick = this.apiDisconnect;
document.getElementById('getSettingsButton').onclick = this.apiGetSettings;
document.getElementById('setSettingsButton').onclick = this.apiSetSettings;
document.getElementById('saveSettingsButton').onclick = this.apiSaveSettings;
......@@ -57,6 +73,25 @@ class WateringClient {
document.getElementById('settingsDialog').style.display = 'none';
}
if (this.softwareVersion != info.softwareVersion) {
document.getElementById('softwareVersion').innerHTML = info.softwareVersion;
this.softwareVersion = info.softwareVersion;
// show hint if software version of watering system is lower than the version of the controll app
if (checkVersionGe(this.softwareVersion, info.softwareVersionControl)) {
document.getElementById('versionOutdatedInfo').style.display = '';
} else {
document.getElementById('versionOutdatedInfo').style.display = 'block';
}
// push data can only be disabled in >= v2.0.0
if (checkVersionGe(this.softwareVersion, '2.0.0')) {
document.getElementById('pushDataEnabled').disabled = false;
} else {
document.getElementById('pushDataEnabled').disabled = true;
}
}
if (info.settings) {
document.getElementById('settings').style.display = '';
document.getElementById('setSettingsButton').style.display = '';
......@@ -69,8 +104,14 @@ class WateringClient {
document.getElementById('wateringTime' + i).value = info.settings.wateringTime[i];
}
document.getElementById('checkInterval').value = info.settings.checkInterval;
document.getElementById('dhtInterval').value = info.settings.dhtInterval;
document.getElementById('tempSensorInterval').value = info.settings.tempSensorInterval;
document.getElementById('sendAdcValuesThroughRH').checked = info.settings.sendAdcValuesThroughRH;
if (checkVersionGe(this.softwareVersion, '2.0.0')) {
document.getElementById('pushDataEnabled').checked = info.settings.pushDataEnabled;
} else {
document.getElementById('pushDataEnabled').checked = true;
}
}
} else {
document.getElementById('settings').style.display = 'none';
......@@ -92,11 +133,6 @@ class WateringClient {
document.getElementById('battery').innerHTML = info.status.batPercent + ' %';
document.getElementById('battery2').innerHTML = info.status.batVolt + ' V (' + info.status.batRaw + ')';
if (this.softwareVersion != info.softwareVersion) {
document.getElementById('softwareVersion').innerHTML = info.softwareVersion;
this.softwareVersion = info.softwareVersion;
}
if (this.logCount != info.log.length) {
document.getElementById('log').innerHTML = info.log.map((l) => { return l.time + ' ' + l.text }).reverse().join('\n');
this.logCount = info.log.length;
......@@ -152,6 +188,18 @@ class WateringClient {
});
}
/**
* Method to send the 'poll data' command to the watering system.
*/
apiPoll () {
fetch('/api/poll')
.then((res) => {
if (res.status != 200) {
alert('Error! ' + res.status + '\n' + res.body);
}
});
}
/**
* Method to send the 'get settings' command to the watering system.
*/
......@@ -189,8 +237,9 @@ class WateringClient {
document.getElementById('wateringTime3').value
],
checkInterval: document.getElementById('checkInterval').value,
dhtInterval: document.getElementById('dhtInterval').value,
sendAdcValuesThroughRH: document.getElementById('sendAdcValuesThroughRH').checked
tempSensorInterval: document.getElementById('tempSensorInterval').value,
sendAdcValuesThroughRH: document.getElementById('sendAdcValuesThroughRH').checked,
pushDataEnabled: document.getElementById('pushDataEnabled').checked
}),
headers: {
'content-type': 'application/json'
......@@ -289,6 +338,18 @@ class WateringClient {
}
});
}
/**
* Method to disconnect the backend from serial-radio gateway.
*/
apiDisconnect () {
fetch('/api/disconnect')
.then((res) => {
if (res.status != 200) {
alert('Error! ' + res.status + '\n' + res.body);
}
});
}
}
window.watering = new WateringClient();
......@@ -4,7 +4,7 @@
*
* Frontend
*
* (c) 2018 Peter Müller <peter@crycode.de> (https://crycode.de)
* (c) 2018-2020 Peter Müller <peter@crycode.de> (https://crycode.de)
-->
<html lang="en-US" >
<head>
......@@ -54,13 +54,18 @@
</div>
<div id="settingsDialog" style="display:none;">
<div>
<h1>
Automatic Watering System <span id="softwareVersion"></span>
</h1>
<div id="versionOutdatedInfo">
<strong>Warning:</strong> The software version of the automatic watering system is outdated! Some features may not be available.
</div>
<div>
<button id="checkNowButton">Check now</button>
<button id="pingButton">Send Ping</button>
<button id="getSettingsButton">Get settings from watering system</button>
<button id="pollButton">Poll data</button>
<button id="disconnectButton">Disconnect</button>
</div>
<div id="settings">
<div class="table">
......@@ -99,14 +104,18 @@
<div class="cell">seconds</div>
</div>
<div class="row">
<div class="cell">DHT interval</div>
<div class="cell"><input type="number" id="dhtInterval" min="1" max="65535" required /></div>
<div class="cell">Temperature sensor interval</div>
<div class="cell"><input type="number" id="tempSensorInterval" min="1" max="65535" required /></div>
<div class="cell">seconds</div>
</div>
<div class="row">
<div class="cell">Send adc values</div>
<div class="cell"><input type="checkbox" id="sendAdcValuesThroughRH" /></div>
</div>
<div class="row">
<div class="cell">Enable automatic data push via RadioHead</div>
<div class="cell"><input type="checkbox" id="pushDataEnabled" /></div>
</div>
</div>
<div>
<button id="setSettingsButton">Send settings to watering system</button>
......@@ -140,8 +149,11 @@
</div>
<div class="row">&nbsp;</div>
<div class="row">
<div class="cell">DHT</div>
<div class="cell">Temperature</div>
<div class="cell center" id="temperature"></div>
</div>
<div class="row">
<div class="cell">Humidity</div>
<div class="cell center" id="humidity"></div>
</div>
<div class="row">
......
......@@ -3,11 +3,16 @@
*
* Frontend styles
*
* (c) 2018 Peter Müller <peter@crycode.de> (https://crycode.de)
* (c) 2018-2020 Peter Müller <peter@crycode.de> (https://crycode.de)
*/
* {
font-family: Arial;
}
#fetchError {
background-color: #FDD;
border: 1px dotted #f00;
background: #fcc;
color: #A00;
font-weight: bold;
padding: 20px;
......@@ -66,3 +71,14 @@ input:focus, select:focus, button:focus {
color: #0A0;
font-weight: bold;
}
#versionOutdatedInfo {
display: none;
border: 1px dotted #f00;
background: #fcc;
}
h1 {
font-size: 1.2em;
font-weight: bold;
}
......@@ -13,15 +13,17 @@ const RadioHeadSerial=require('radiohead-serial').RadioHeadSerial;
const bodyParser = require('body-parser');
const express = require('express');
const semver = require('semver');
const http = require('http');
const path = require('path');
const RH_MSG_START = 0x00;
const RH_MSG_BATTERY = 0x02;
const RH_MSG_SENSOR_VALUES = 0x10;
const RH_MSG_DHTDATA = 0x20;
const RH_MSG_CHANNEL_ON = 0x21;
const RH_MSG_CHANNEL_OFF = 0x22;
const RH_MSG_TEMP_SENSOR_DATA = 0x20;
const RH_MSG_CHANNEL_ON = 0x21; // < v2.0.0 only
const RH_MSG_CHANNEL_OFF = 0x22; // < v2.0.0 only
const RH_MSG_CHANNEL_STATE = 0x25; // >= v2.0.0 only
const RH_MSG_SETTINGS = 0x50;
const RH_MSG_GET_SETTINGS = 0x51;
......@@ -29,10 +31,13 @@ const RH_MSG_SET_SETTINGS = 0x52;
const RH_MSG_SAVE_SETTINGS = 0x53;
const RH_MSG_CHECK_NOW = 0x60;
const RH_MSG_TURN_CHANNEL_ON = 0x61;
const RH_MSG_TURN_CHANNEL_OFF = 0x62;
const RH_MSG_TURN_CHANNEL_ON = 0x61; // < v2.0.0 only
const RH_MSG_TURN_CHANNEL_OFF = 0x62; // < v2.0.0 only
const RH_MSG_PAUSE = 0x63;
const RH_MSG_RESUME = 0x64;
const RH_MSG_TURN_CHANNEL_ON_OFF = 0x65; // >= v2.0.0 only
const RH_MSG_POLL_DATA = 0x66; // >= v2.0.0 only
const RH_MSG_PAUSE_ON_OFF = 0x67; // >= v2.0.0 only
const RH_MSG_GET_VERSION = 0xF0;
const RH_MSG_VERSION = 0xF1;
......@@ -57,13 +62,16 @@ class Watering {
on: [false, false, false, false]
};
this.softwareVersion = '';
this.softwareVersionControl = require('./package.json').version;
this.logData = [];
this.lastPingData = Buffer.alloc(4);
// bind own methods to 'this'
this.apiCheckNow = this.apiCheckNow.bind(this);
this.apiPoll = this.apiPoll.bind(this);
this.apiPing = this.apiPing.bind(this);
this.apiConnect = this.apiConnect.bind(this);
this.apiDisconnect = this.apiDisconnect.bind(this);
this.apiGetInfo = this.apiGetInfo.bind(this);
this.apiGetSettings = this.apiGetSettings.bind(this);
this.apiSetSettings = this.apiSetSettings.bind(this);
......@@ -89,6 +97,7 @@ class Watering {
// register API endpoints
this.app.get('/api/checkNow', this.apiCheckNow);
this.app.get('/api/poll', this.apiPoll);
this.app.get('/api/ping', this.apiPing);
this.app.get('/api/getInfo', this.apiGetInfo);
this.app.get('/api/getPorts', this.apiGetPorts);
......@@ -97,6 +106,7 @@ class Watering {
this.app.get('/api/pause', this.apiPause);
this.app.get('/api/resume', this.apiResume);
this.app.post('/api/connect', this.apiConnect);
this.app.get('/api/disconnect', this.apiDisconnect);
this.app.post('/api/onoff', this.apiOnoff);
this.app.post('/api/setSettings', this.apiSetSettings);
......@@ -116,7 +126,8 @@ class Watering {
log: this.logData,
settings: this.settings,
status: this.status,
softwareVersion: this.softwareVersion
softwareVersion: this.softwareVersion,
softwareVersionControl: this.softwareVersionControl
});
}
......@@ -189,6 +200,28 @@ class Watering {
this.versionInterval = setInterval(getVersion, 3000);
}
/**
* API endpoint for disconnecting from seral-radio gateway.
*/
apiDisconnect (req, res, next) {
if (!this.connected) {
res.status(400);
res.send('Not connected');
return;
}
this.rhs.close()
.then(() => {
this.rhs = null;
this.connected = false;
this.log('disconnected from the serial-radio gateway');
res.status(200);
res.send('Ok');
});
}
/**
* API endpoint for sending a 'check now' command to the watering system.
*/
......@@ -241,9 +274,13 @@ class Watering {
this.settings.wateringTime[chan] = parseInt(req.body.wateringTime[chan], 10);
}
this.settings.checkInterval = parseInt(req.body.checkInterval, 10);
this.settings.dhtInterval = parseInt(req.body.dhtInterval, 10);
this.settings.tempSensorInterval = parseInt(req.body.tempSensorInterval, 10);
this.settings.sendAdcValuesThroughRH = req.body.sendAdcValuesThroughRH;
if (semver.satisfies(this.softwareVersion, '>=2.0.0')) {
this.settings.pushDataEnabled = req.body.pushDataEnabled;
}
let buf = Buffer.alloc(22);
buf[0] = RH_MSG_SET_SETTINGS;
......@@ -258,9 +295,12 @@ class Watering {
if (this.settings.sendAdcValuesThroughRH) {
bools |= (1 << 7);
}
if (semver.satisfies(this.softwareVersion, '>=2.0.0') && this.settings.pushDataEnabled) {
bools |= (1 << 6);
}
buf[1] = bools;
buf.writeUInt16LE(this.settings.checkInterval, 18);
buf.writeUInt16LE(this.settings.dhtInterval, 20);
buf.writeUInt16LE(this.settings.tempSensorInterval, 20);
this.rhsSend(buf);
......@@ -282,9 +322,28 @@ class Watering {
* API endpoint for turning a channel on or off at the watering system.
*/
apiOnoff (req, res, next) {
let buf = Buffer.alloc(2);
buf[0] = req.body.on ? RH_MSG_TURN_CHANNEL_ON : RH_MSG_TURN_CHANNEL_OFF;
buf[1] = parseInt(req.body.channel, 10) || 0;
const chanToSet = parseInt(req.body.channel, 10) || 0;
let buf;
if (semver.satisfies(this.softwareVersion, '>=2.0.0')) {
// >= v2.0.0
buf = Buffer.alloc(5);
buf[0] = RH_MSG_TURN_CHANNEL_ON_OFF;
for (let chan = 0; chan < 4; chan++) {
if (chan === chanToSet) {
buf[chan + 1] = req.body.on ? 0x01 : 0x00;
} else {
// set channel state to 0xff to let the watering system ignore it
buf[chan + 1] = 0xff;
}
}
} else {
// < v2.0.0
buf = Buffer.alloc(2);
buf[0] = req.body.on ? RH_MSG_TURN_CHANNEL_ON : RH_MSG_TURN_CHANNEL_OFF;
buf[1] = chanToSet;
}
this.rhsSend(buf);
res.send('Ok');
......@@ -294,8 +353,17 @@ class Watering {
* API endpoint for sending a 'pause' command to the watering system.
*/
apiPause (req, res, next) {
let buf = Buffer.alloc(1);
buf[0] = RH_MSG_PAUSE;
let buf;
if (semver.satisfies(this.softwareVersion, '>=2.0.0')) {
// >= v2.0.0
buf = Buffer.alloc(2);
buf[0] = RH_MSG_PAUSE_ON_OFF;
buf[1] = 0x01;
} else {
// < v2.0.0
buf = Buffer.alloc(1);
buf[0] = RH_MSG_PAUSE;
}
this.rhsSend(buf);
res.send('Ok');
......@@ -305,8 +373,29 @@ class Watering {
* API endpoint for sending a 'resume' command to the watering system.
*/
apiResume (req, res, next) {
let buf = Buffer.alloc(1);
buf[0] = RH_MSG_RESUME;
let buf;
if (semver.satisfies(this.softwareVersion, '>=2.0.0')) {
// >= v2.0.0
buf = Buffer.alloc(2);
buf[0] = RH_MSG_PAUSE_ON_OFF;
buf[1] = 0x00;
} else {
// < v2.0.0
buf = Buffer.alloc(1);
buf[0] = RH_MSG_RESUME;
}
this.rhsSend(buf);
res.send('Ok');
}
/**
* API endpoint for sending a 'poll data' command to the watering system.
* Supported since v2.0.0
*/
apiPoll (req, res, next) {
const buf = Buffer.alloc(1);
buf[0] = RH_MSG_POLL_DATA;
this.rhsSend(buf);
res.send('Ok');
......@@ -353,7 +442,7 @@ class Watering {
switch (msg.data[0]) {
case RH_MSG_START:
this.log('System started');
this.log('system started');
break;
case RH_MSG_BATTERY:
......@@ -361,7 +450,7 @@ class Watering {
this.status.batRaw = msg.data.readUInt16LE(2);
this.status.batVolt = 5/1023*this.status.batRaw;
this.status.batVolt = Math.round(this.status.batVolt*100)/100;
this.log('Battery: ' + this.status.batPercent + '%, ' + this.status.batVolt + 'V (' + this.status.batRaw + ')');
this.log(`battery: ${this.status.batPercent} %, ${this.status.batVolt} V (${this.status.batRaw})`);
break;
case RH_MSG_SENSOR_VALUES:
......@@ -370,33 +459,52 @@ class Watering {
this.status.adcVolt[i] = 5/1023*this.status.adcRaw[i];
this.status.adcVolt[i] = Math.round(this.status.adcVolt[i]*100)/100;
}
this.log('Sensors: ' +
this.log('sensors: ' +
this.status.adcVolt[0] + 'V (' + this.status.adcRaw[0] + ') ' +
this.status.adcVolt[1] + 'V (' + this.status.adcRaw[1] + ') ' +
this.status.adcVolt[2] + 'V (' + this.status.adcRaw[2] + ') ' +
this.status.adcVolt[3] + 'V (' + this.status.adcRaw[3] + ')');
break;
case RH_MSG_DHTDATA:
this.status.temperature = msg.data.readFloatLE(1);
this.status.humidity = msg.data.readFloatLE(5);
this.status.temperature = Math.round(this.status.temperature*10)/10;
this.status.humidity = Math.round(this.status.humidity*10)/10;
this.log('DHT: ' + this.status.temperature + '°C ' + this.status.humidity + '%');
case RH_MSG_TEMP_SENSOR_DATA:
if (msg.data.length >= 5) {
this.status.temperature = msg.data.readFloatLE(1);
this.status.temperature = Math.round(this.status.temperature*10)/10;
this.log(`temperature: ${this.status.temperature} °C `);
} else {
this.status.temperature = '-';
}
if (msg.data.length >= 9) {
this.status.humidity = msg.data.readFloatLE(5);
this.status.humidity = Math.round(this.status.humidity*10)/10;
this.log(`humidity: ${this.status.humidity} %`);
} else {
this.status.humidity = '-';
}
break;
case RH_MSG_CHANNEL_ON:
case RH_MSG_CHANNEL_ON: // < v2.0.0
this.status.on[msg.data[1]] = true;
this.log('Channel ' + msg.data[1] + ' on');
this.log(`channel ${msg.data[1]} on`);
break;
case RH_MSG_CHANNEL_OFF:
case RH_MSG_CHANNEL_OFF: // < v2.0.0
this.status.on[msg.data[1]] = false;
this.log('Channel ' + msg.data[1] + ' off');
this.log(`channel ${msg.data[1]} off`);
break;
case RH_MSG_CHANNEL_STATE: // >= v2.0.0
for (let chan = 0; chan < 4; chan++) {
const newChanState = !!msg.data[chan+1];
if (newChanState !== this.status.on[chan]) {
this.status.on[chan] = newChanState;
this.log(`channel ${chan} ${newChanState ? 'on' : 'off'}`);
}
}
break;
case RH_MSG_SETTINGS:
this.log('Got settings');
this.log('got settings');
this.settings = {
time: (new Date()).getTime(),
channelEnabled: [],
......@@ -409,23 +517,27 @@ class Watering {
this.settings.wateringTime[chan] = msg.data.readUInt16LE(10+chan*2);
}
this.settings.checkInterval = msg.data.readUInt16LE(18);
this.settings.dhtInterval = msg.data.readUInt16LE(20);
this.settings.tempSensorInterval = msg.data.readUInt16LE(20);
this.settings.sendAdcValuesThroughRH = ((msg.data[1] & (1 << 7)) != 0);
if (semver.satisfies(this.softwareVersion, '>=2.0.0')) {
this.settings.pushDataEnabled = ((msg.data[1] & (1 << 6)) != 0);
}
break;
case RH_MSG_VERSION:
clearInterval(this.versionInterval);
this.softwareVersion = `v${msg.data[1]}.${msg.data[2]}.${msg.data[3]}`;
this.log('Got software version ' + this.softwareVersion);
this.log('got software version ' + this.softwareVersion);
break;
case RH_MSG_PONG:
if (this.lastPingData.equals(msg.data.slice(1))) {
// correct data
this.log('Got pong with correct data :-)');
this.log('got pong with correct data :-)');
} else {
// wrong data
this.log('Got pong with wrong data :-(');
this.log('got pong with wrong data :-(');
}
break;
}
......
{
"name": "auto-watering-control",
"version": "1.0.3",
"version": "2.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
......@@ -173,6 +173,14 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"requires": {
"file-uri-to-path": "1.0.0"
}
},
"bl": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-3.0.0.tgz",
......@@ -696,6 +704,11 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"nan": {
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
"integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg=="
},