Sync tool status across all clients

master
Tanner Collin 6 years ago
parent a1c80c70cb
commit 0c59652222
  1. 49
      webclient/src/App.js
  2. 2
      webclient/src/Category.js
  3. 40
      webclient/src/Tool.js
  4. 246
      webserver/package-lock.json
  5. 3
      webserver/package.json
  6. 89
      webserver/server.js

@ -4,29 +4,62 @@ import Category from './Category';
import Tool from './Tool';
import { Container, Dimmer, Dropdown, Header, Icon, Item, Loader, Menu, Segment, Input } from 'semantic-ui-react';
import { Link, Route } from 'react-router-dom';
import io from 'socket.io-client';
// Move to env var
const SERVER_URL = "http://localhost:8080";
class App extends Component {
constructor() {
super();
this.socket = io(SERVER_URL);
this.state = {
user: null,
toolData: null,
toolStatus: null,
connected: false,
};
}
componentDidMount() {
fetch('http://localhost:8080/api/tooldata')
fetch(SERVER_URL + '/api/tooldata')
.then(response => response.json())
.then(data => this.setState({ toolData: data }));
fetch('http://localhost:8080/api/user')
fetch(SERVER_URL + '/api/user')
.then(response => response.json())
.then(data => this.setState({ user: data }));
this.socket.on('toolStatus', toolStatus =>
this.setState({ toolStatus: toolStatus })
);
this.socket.on('connect', toolStatus =>
this.setState({ connected: true })
);
this.socket.on('disconnect', toolStatus =>
this.setState({ toolStatus: null, connected: false })
);
}
componentWillUnmount() {
this.socket.removeAllListeners();
}
requestInterlock = change => {
this.socket.emit('requestInterlock', {
username: this.state.user.username,
change: change,
});
}
render() {
const toolData = this.state.toolData;
const toolStatus = this.state.toolStatus;
const user = this.state.user;
const connected = this.state.connected;
return (
<div>
@ -37,6 +70,9 @@ class App extends Component {
<Menu.Item as={Link} to='/'>
<Icon name='home' size='big' />
</Menu.Item>
<Menu.Item>
<Icon name='circle' color={connected ? 'green' : 'red'} />
</Menu.Item>
<Menu.Menu position='right'>
<Menu.Item position='right'>
<Input transparent inverted placeholder='Search...' icon='search' />
@ -49,7 +85,7 @@ class App extends Component {
{toolData && user ?
<div>
<Route exact path='/' render={props =>
<Categories {...props} data={toolData} user={user} />
<Categories {...props} data={toolData} />
} />
<Route exact path='/:category' render={props =>
@ -57,7 +93,12 @@ class App extends Component {
} />
<Route exact path='/:category/:id' render={props =>
<Tool {...props} data={toolData} user={user} />
<Tool {...props}
data={toolData}
user={user}
toolStatus={toolStatus}
requestInterlock={this.requestInterlock}
/>
} />
</div>
:

@ -8,7 +8,7 @@ class Category extends Component {
const user = this.props.user;
const match = this.props.match;
const category = data.categories.find((x) =>
const category = data.categories.find(x =>
x.slug === match.params.category
);

@ -3,19 +3,41 @@ import { Breadcrumb, Button, Container, Dropdown, Header, Icon, Image, Item, Lab
import { Link } from 'react-router-dom';
class Tool extends Component {
decodeStatus = status => {
if (status === null) {
return { msg: 'Unknown! Connection error?', canArm: false, canDisarm: false, };
} else if (!status.armed && !status.on) {
return { msg: 'Off', canArm: true, canDisarm: false, };
} else if (status.armed && !status.on) {
return { msg: 'Armed', canArm: false, canDisarm: true, };
} else if (status.armed && status.on) {
return { msg: 'On', canArm: false, canDisarm: true, };
} else if (!status.armed && status.on) {
return { msg: 'Error: Impossible state!', canArm: false, canDisarm: false, };
}
}
render() {
const data = this.props.data;
const user = this.props.user;
const match = this.props.match;
const toolStatus = this.props.toolStatus || [];
const requestInterlock = this.props.requestInterlock;
const category = data.categories.find((x) =>
const category = data.categories.find(x =>
x.slug === match.params.category
);
const tool = category.tools.find((x) =>
const tool = category.tools.find(x =>
x.id.toString() === match.params.id
);
const status = toolStatus.find(x =>
x.id.toString() === match.params.id
) || null;
const decodedStatus = this.decodeStatus(status);
console.log(decodedStatus);
const approved = user.authorizedTools.includes(tool.id);
return (
@ -32,12 +54,18 @@ class Tool extends Component {
<Segment>
<Image src={tool.photo} size='medium' centered rounded />
<Segment textAlign='center' basic>
<p>Status: Off</p>
<p> Status: {decodedStatus.msg}</p>
<div>
<Button color='green' disabled={!approved}>
<Icon name='lightning' /> Arm
<Button color='green'
disabled={!approved || !decodedStatus.canArm}
onClick={() => requestInterlock({toolId: tool.id, action: 'arm',})}
>
<Icon name='lightning' /> Arm
</Button>
<Button color='red' disabled={!approved}>
<Button color='red'
disabled={!approved || !decodedStatus.canDisarm}
onClick={() => requestInterlock({toolId: tool.id, action: 'disarm',})}
>
<Icon name='stop' /> Disarm
</Button>
</div>

@ -13,11 +13,54 @@
"negotiator": "0.6.1"
}
},
"after": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
"integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8="
},
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"arraybuffer.slice": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
"integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog=="
},
"async-limiter": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
"integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg=="
},
"backo2": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
"integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc="
},
"base64-arraybuffer": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
"integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg="
},
"base64id": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz",
"integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY="
},
"better-assert": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
"integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=",
"requires": {
"callsite": "1.0.0"
}
},
"blob": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz",
"integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE="
},
"body-parser": {
"version": "1.18.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz",
@ -40,6 +83,26 @@
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
"integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
},
"callsite": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
"integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA="
},
"component-bind": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
"integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E="
},
"component-emitter": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
"integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
},
"component-inherit": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
"integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM="
},
"content-disposition": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
@ -88,6 +151,61 @@
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
},
"engine.io": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.1.4.tgz",
"integrity": "sha1-PQIRtwpVLOhB/8fahiezAamkFi4=",
"requires": {
"accepts": "1.3.3",
"base64id": "1.0.0",
"cookie": "0.3.1",
"debug": "2.6.9",
"engine.io-parser": "2.1.2",
"uws": "0.14.5",
"ws": "3.3.3"
},
"dependencies": {
"accepts": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.3.tgz",
"integrity": "sha1-w8p0NJOGSMPg2cHjKN1otiLChMo=",
"requires": {
"mime-types": "2.1.17",
"negotiator": "0.6.1"
}
}
}
},
"engine.io-client": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.1.4.tgz",
"integrity": "sha1-T88TcLRxY70s6b4nM5ckMDUNTqE=",
"requires": {
"component-emitter": "1.2.1",
"component-inherit": "0.0.3",
"debug": "2.6.9",
"engine.io-parser": "2.1.2",
"has-cors": "1.1.0",
"indexof": "0.0.1",
"parseqs": "0.0.5",
"parseuri": "0.0.5",
"ws": "3.3.3",
"xmlhttprequest-ssl": "1.5.5",
"yeast": "0.1.2"
}
},
"engine.io-parser": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.2.tgz",
"integrity": "sha512-dInLFzr80RijZ1rGpx1+56/uFoH7/7InhH3kZt+Ms6hT8tNx3NGW/WNSA/f8As1WkOfkuyb3tnRyuXGxusclMw==",
"requires": {
"after": "0.8.2",
"arraybuffer.slice": "0.0.7",
"base64-arraybuffer": "0.1.5",
"blob": "0.0.4",
"has-binary2": "1.0.2"
}
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@ -159,6 +277,19 @@
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
},
"has-binary2": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.2.tgz",
"integrity": "sha1-6D26SfC5vk0CbSc2U1DZ8D9Uvpg=",
"requires": {
"isarray": "2.0.1"
}
},
"has-cors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz",
"integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk="
},
"http-errors": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz",
@ -187,6 +318,11 @@
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz",
"integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ=="
},
"indexof": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
"integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10="
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
@ -197,6 +333,11 @@
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.5.2.tgz",
"integrity": "sha1-1LUFvemUaYfM8PxY2QEP+WB+P6A="
},
"isarray": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
"integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
},
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@ -240,6 +381,11 @@
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
},
"object-component": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/object-component/-/object-component-0.0.3.tgz",
"integrity": "sha1-8MaapQ78lbhmwYb0AKM3acsvEpE="
},
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
@ -248,6 +394,22 @@
"ee-first": "1.1.1"
}
},
"parseqs": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
"integrity": "sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0=",
"requires": {
"better-assert": "1.0.2"
}
},
"parseuri": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/parseuri/-/parseuri-0.0.5.tgz",
"integrity": "sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo=",
"requires": {
"better-assert": "1.0.2"
}
},
"parseurl": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
@ -329,11 +491,64 @@
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
"integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
},
"socket.io": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.0.4.tgz",
"integrity": "sha1-waRZDO/4fs8TxyZS8Eb3FrKeYBQ=",
"requires": {
"debug": "2.6.9",
"engine.io": "3.1.4",
"socket.io-adapter": "1.1.1",
"socket.io-client": "2.0.4",
"socket.io-parser": "3.1.2"
}
},
"socket.io-adapter": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz",
"integrity": "sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs="
},
"socket.io-client": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.0.4.tgz",
"integrity": "sha1-CRilUkBtxeVAs4Dc2Xr8SmQzL44=",
"requires": {
"backo2": "1.0.2",
"base64-arraybuffer": "0.1.5",
"component-bind": "1.0.0",
"component-emitter": "1.2.1",
"debug": "2.6.9",
"engine.io-client": "3.1.4",
"has-cors": "1.1.0",
"indexof": "0.0.1",
"object-component": "0.0.3",
"parseqs": "0.0.5",
"parseuri": "0.0.5",
"socket.io-parser": "3.1.2",
"to-array": "0.1.4"
}
},
"socket.io-parser": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.1.2.tgz",
"integrity": "sha1-28IoIVH8T6675Aru3Ady66YZ9/I=",
"requires": {
"component-emitter": "1.2.1",
"debug": "2.6.9",
"has-binary2": "1.0.2",
"isarray": "2.0.1"
}
},
"statuses": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz",
"integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4="
},
"to-array": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz",
"integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA="
},
"type-is": {
"version": "1.6.15",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz",
@ -343,6 +558,11 @@
"mime-types": "2.1.17"
}
},
"ultron": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz",
"integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og=="
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@ -353,10 +573,36 @@
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"uws": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/uws/-/uws-0.14.5.tgz",
"integrity": "sha1-Z6rzPEaypYel9mZtAPdpEyjxSdw=",
"optional": true
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
"ws": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
"integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==",
"requires": {
"async-limiter": "1.0.0",
"safe-buffer": "5.1.1",
"ultron": "1.1.1"
}
},
"xmlhttprequest-ssl": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz",
"integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4="
},
"yeast": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
"integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
}
}
}

@ -10,6 +10,7 @@
"author": "",
"license": "MIT",
"dependencies": {
"express": "^4.16.2"
"express": "^4.16.2",
"socket.io": "^2.0.4"
}
}

@ -1,6 +1,7 @@
const express = require('express');
const app = express();
// Hardcoded data - can only be changed by admin
const toolData = {
categories: [
{
@ -50,30 +51,60 @@ const toolData = {
],
};
const user = {
username: "protospace",
name: "Protospace User",
authorizedTools: [1, 2],
}
// Hardcoded data - can only be changed by admin
const users = [
{
username: "protospace",
name: "Protospace User",
authorizedTools: [1, 2],
},
];
// Hardcoded data - can only be changed by admin
const lockoutData = {
lockouts: [
{
id: 0,
mac: 'ABCDEF000000',
},
{
id: 1,
mac: '2C3AE843A15F',
relayOn: false,
ledOn: true,
date: '2018-02-01',
},
{
id: 2,
mac: 'ABCDEF000002',
},
{
id: 3,
mac: 'ABCDEF000003',
},
],
};
// Derived data - changes through use of system
let toolStatus = lockoutData.lockouts.map(x => (
{
id: x.id,
on: false,
armed: false,
user: null,
}
));
console.log(toolStatus);
const server = app.listen(8080, function () {
console.log('Example app listening on port 8080!');
});
const io = require('socket.io')(server);
// Express http server stuff:
// TODO : remove on prod
app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
next();
});
@ -90,7 +121,7 @@ app.get('/api/user', function (req, res) {
console.log('Request for user data');
res.setHeader('Content-Type', 'application/json');
res.send(user);
res.send(users[0]);
});
app.get('/api/lockout/:mac', function (req, res) {
@ -106,3 +137,39 @@ app.get('/api/lockout/:mac', function (req, res) {
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify(data));
});
// Socket.io websocket stuff:
// TODO : remove on prod
io.origins('*:*');
io.on('connection', socket => {
socket.emit('toolStatus', toolStatus);
socket.on('requestInterlock', data => {
console.log('Interlock change requested: ' + data.toString());
const user = users.find(x => x.username === data.username);
const toolId = data.change.toolId;
const action = data.change.action;
// TODO ; Make this part prettier
if (user) {
if (user.authorizedTools.includes(data.change.toolId)) {
const toolIndex = toolStatus.findIndex(x => x.id === toolId);
let tool = toolStatus[toolIndex];
if (action === 'arm' && !tool.armed && !tool.on) {
tool.armed = true;
} else if (action === 'disarm' && tool.armed) {
tool.armed = false;
tool.on = false;
}
toolStatus[toolIndex] = tool;
console.log(toolStatus);
io.sockets.emit('toolStatus', toolStatus);
}
}
});
});

Loading…
Cancel
Save