Skip to content

Commit

Permalink
Test mode, startup refactor, minor test page fixes.
Browse files Browse the repository at this point in the history
- Use `yarn run test-server` to get a test server with faked-out
  serial ports that randomly change state over time. Simulated
  button-mashing.
- Reorganizes startup; index.js is a stub that calls server.js.
- Adds CORS headers to allow access from anywhere.
- Fixes some CSS on the demo page.
- Speeds up polling on the demo page to 300ms.
  • Loading branch information
mildmojo committed Sep 20, 2017
1 parent 7596156 commit d98b9ce
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 110 deletions.
97 changes: 2 additions & 95 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,97 +1,4 @@
'use strict';
const fs = require('fs');
const express = require('express');
// const SerialPort = require('serialport/test');
const SerialPort = require('serialport');
// const MockBinding = SerialPort.Binding; // DEBUG
const chalk = require('chalk');
const cjson = require('cjson');
const xml2js = require('xml2js');
const ButtonStatus = require('./lib/buttonStatus');
const app = express();
// config.serialPorts.forEach(p => MockBinding.createPort(p)); // DEBUG

let buttonStatuses = [];
let ports = [];

if (!fs.existsSync('./config.json')) {
console.warn(chalk.red.bold('Could not find config.json. Please copy' +
' config.json.example to config.json and edit it.'));
process.exit(1);
}

const config = cjson.load('./config.json');

if (!config.serialPorts || !config.serialPorts.length) {
console.warn('No serial ports configured! Please specify serial port(s) in config.json.');
process.exit(1);
}

init().catch(console.log);

async function init() {
let mappings = [];
let portNames = [];

for (let i = 0; i < config.serialPorts.length; i++) {
let portName = config.serialPorts[i];
let xml = await readFile(config.mappingFiles[i]).catch(console.error);
let parsedXML = await parseXML(xml).catch(console.error);
mappings.push(parsedXML.ArrayOfInt.int);
portNames.push(portName);
}

portNames.forEach((portName, portIdx) => {
let port = new SerialPort(portName, { baudrate: 9600 });
port.on('open', () => {
port.on('data', chunk => {
buttonStatuses[portIdx].update(chunk);
});

port.on('error', err => console.warn(`Error: ${err}`));

// port.binding.emitData(Buffer.from([0,0,0,0,0,0xFE,0xFE,1])); // DEBUG
});


buttonStatuses.push(new ButtonStatus(mappings[portIdx]));
ports.push(port);
});
}

function readFile(file) {
return new Promise((resolve, reject) => {
fs.readFile(file, (err, data) => {
if (err) return reject(err);
resolve(data);
});
});
}

function parseXML(xml) {
return new Promise((resolve, reject) => {
let xmlParser = new xml2js.Parser();
xmlParser.parseString(xml, (err, data) => {
if (err) return reject(err);
resolve(data);
});
});
}


app.use('/', express.static('./public'));

app.get('/buttons', (_req, res) => {
let status = { devices: [] };
buttonStatuses.forEach((buttonStatus, idx) => {
status.devices[idx] = buttonStatus.toJSON();
status.devices[idx].name = config.serialPorts[idx];
});
res.send(JSON.stringify(status, null, ' '));
});


let port = process.env.PORT || config.port || 3000;
console.log(chalk.yellow.bold(`Starting server on port ${port}...`));
console.log(chalk.blue.bold(`Visit http://localhost:${port}/ to see a demo status page.`));
app.listen(port);
const server = require('./lib/server.js');
server.start().catch(console.error);
139 changes: 139 additions & 0 deletions lib/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
'use strict';
const fs = require('fs');
const express = require('express');
// const SerialPort = require('serialport/test');
const SerialPort = require('serialport');
const chalk = require('chalk');
const cjson = require('cjson');
const xml2js = require('xml2js');
const ButtonStatus = require('./buttonStatus');
const CONFIG_FILE = process.env.CONFIG_FILE || 'config.json';

module.exports = {start};

async function start() {
// Read config
// Verify config
// Read mappings
// Create button status instances
// Initialize serial ports
// Bind serial ports to button status instances
// Bind web app to button status instances

const config = readConfig(CONFIG_FILE);
if (!verifyConfig(config)) process.exit(1);
const mappings = await readMappings(config.mappingFiles);
const buttonStatuses = createButtonStatuses(config.serialPorts.length, mappings);
const ports = openSerialPorts(config.serialPorts);

for (let i = 0; i < ports.length; i++) {
ports[i].on('data', chunk => buttonStatuses[i].update(chunk));
}

startApp(config, buttonStatuses);
}

function readConfig(file) {
if (!fs.existsSync('./config.json')) {
console.warn(chalk.red.bold(`Could not find ${file}. Please copy` +
` ${file}.example to ${file}) and edit it.`));
throw new Error(`File not found: ${file}`)
}

return cjson.load(file);
}

function verifyConfig(config) {
if (!config.serialPorts || !config.serialPorts.length) {
console.warn(`No serial ports configured! Please specify serial port(s) in ${CONFIG_FILE}.`);
return false;
}
return true;
}

async function readMappings(mappingFiles) {
const mappings = [];
for (let file of mappingFiles) {
const xml = await readFile(file).catch(console.error);
const parsedXML = await parseXML(xml).catch(console.error);
mappings.push(parsedXML.ArrayOfInt.int);
}
return mappings;
}

function createButtonStatuses(count, mappings) {
return [...Array(count)].map((_v, idx) => new ButtonStatus(mappings[idx]));
}

function openSerialPorts(portNames) {
return portNames.map(portName => {
let serial = null;

if (process.env.TESTMODE) {
const SerialPortTest = require('serialport/test');
const MockBinding = SerialPortTest.Binding; // DEBUG
MockBinding.createPort(portName);
serial = new SerialPortTest(portName, { baudrate: 9600 });
// Randomly update button states.
setInterval(() => {
let randomVals = [...Array(7)].map(() => (Math.ceil(Math.random() * 0xFF)) & 0xFE);
randomVals[randomVals.length - 1] += 1;
serial.binding.emitData(Buffer.from(randomVals));
}, 50);
} else {
serial = new SerialPort(portName, { baudrate: 9600 });
}

serial.on('error', err => console.warn(`Error with port ${portName}: ${err}`));

return serial;
});
}

function readFile(file) {
return new Promise((resolve, reject) => {
fs.readFile(file, (err, data) => {
if (err) return reject(err);
resolve(data);
});
});
}

function parseXML(xml) {
return new Promise((resolve, reject) => {
let xmlParser = new xml2js.Parser();
xmlParser.parseString(xml, (err, data) => {
if (err) return reject(err);
resolve(data);
});
});
}

function startApp(config, buttonStatuses) {
const app = express();
app.disable('x-powered-by');

// Set proper CORS headers to allow browsers to use this resource from anywhere.
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");
next();
});

app.use('/', express.static('./public'));

app.get('/buttons', (_req, res) => {
let status = { testMode: !!process.env.TESTMODE, devices: [] };
buttonStatuses.forEach((buttonStatus, idx) => {
status.devices[idx] = buttonStatus.toJSON();
status.devices[idx].name = config.serialPorts[idx];
});
res.send(JSON.stringify(status, null, ' '));
});


let port = process.env.PORT || config.port || 3000;
console.log(chalk.yellow.bold(`Starting server on port ${port}...`));
console.log(chalk.blue.bold(`Visit http://localhost:${port}/ to see a demo status page.`));
app.listen(port);
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"scripts": {
"lint": "$(npm bin)/eslint *.js",
"server": "node index.js",
"test-server": "TESTMODE=true node index.js",
"test": "$(npm bin)/mocha test/**/*.js"
},
"author": "mildmojo",
Expand Down
65 changes: 50 additions & 15 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
}

ready(function() {
var body = document.querySelector('body');
var container = document.querySelector('.container');

var loop = setInterval(function() {
Expand All @@ -21,10 +22,13 @@
return res.json();
})
.then(refreshButtons);
}, 500);
}, 300);

function refreshButtons(json) {
container.innerHTML = '';
if (json.testMode && !body.classList.contains('test-mode')) {
body.classList.add('test-mode');
}
for (var i = 0; i < json.devices.length; i++) {
var device = json.devices[i];
var buttons = device.buttons;
Expand All @@ -45,65 +49,94 @@
}
}
});

function parseHTML(str) {
var tmp = document.implementation.createHTMLDocument();
tmp.body.innerHTML = str;
return tmp.body.children;
}
</script>

<style type="text/css">
@import url('https://fonts.googleapis.com/css?family=Francois+One');

/* Reset */
html, body, h1, h2, h3, h4, h5, h6, p, ol, ul, li, dl, dt, dd, blockquote, address {
margin: 0;
padding: 0;
}

body {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex-wrap: nowrap;
width: 100vw;
height: 100vh;
font-family: "Tahoma";
padding-top: 20px;
font-family: "Francois One",Tahoma,Verdana,Sans;
background-color: #F0FEF0;
}

.test-mode-note {
display: none;
}

.live-mode-note {
display: block;
}

body.test-mode {
background-color: #FFFFF0;
}

body.test-mode .test-mode-note {
display: block;
}

body.test-mode .live-mode-note {
display: none;
}

body h2 {
clear: both;
}

.container {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
margin: 0;
padding: 10px;
height: auto;
width: 50%;
min-width: 300px;
min-height: 600px;
max-width: 600px;
min-width: 500px;
max-width: 800px;
border: 1px solid black;
margin-top: 10px;
}

.device {
display: flex;
flex-direction: column;
flex-wrap: wrap;
width: 100%;
max-height: 300px;
}

.device h2 {
display: block;
}

.device > div {
width: 20%;
}

.number-column {
display: block;
float: left;
min-width: 3rem;
}

.center {
text-align: center;
}

.full-width {
width: 100%;
}

.no-decorate {
text-decoration: none;
}
Expand All @@ -112,6 +145,8 @@
<body>
<div class="center full-width">
<h2><a class="no-decorate" href="http://buttonsare.cool">1000 Button Project Test Page</a></h2>
<div class='test-mode-note'>(Test Mode)</div>
<div class='live-mode-note'>(Live Mode)</div>
</div>
<div class="container"></div>
</body>
Expand Down

0 comments on commit d98b9ce

Please sign in to comment.