Skip to content

Commit

Permalink
Gets testmode button emulation working. Fixes bugs.
Browse files Browse the repository at this point in the history
- Fixes mapping bugs; mappings were incorrectly read and used.
- Enables button emulation in testmode via clicking demo page UI
  (sending WebSocket events back to server).
- Disables long-press context menus on buttons on mobile.
- Bits of refactoring here and there. Needs more refactor, more tests.
- Adds support for HOST env var (for setting --host)
  • Loading branch information
mildmojo committed Sep 29, 2017
1 parent eb3e385 commit ef137ee
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 68 deletions.
72 changes: 44 additions & 28 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,35 @@ const SerialPort = require('serialport');
const nconf = require('nconf');
const cjson = require('cjson');
const chalk = require('chalk');
const server = require('./lib/server.js');
const {version} = require('./package.json');

nconf.use('memory').argv().env();
const configFile = nconf.get('CONFIG_FILE') || nconf.get('config_file') || 'config.json';
nconf.defaults(readConfig(configFile));
nconf.set('port', nconf.get('PORT') || nconf.get('port') || 3000)
nconf.set('port', nconf.get('PORT') || nconf.get('port') || 3000);
nconf.set('host', nconf.get('HOST') || nconf.get('host') || '127.0.0.1');
nconf.set('testmode', nconf.get('TESTMODE') || nconf.get('testmode'));
verifyConfig(nconf);

if (nconf.get('version')) showVersionQuit();
if (nconf.get('list')) showPortsQuit();
if (nconf.get('help')) showHelpQuit();

(async function() {
try {
await server.start(nconf);
} catch(e) {
console.error(`Failed to start server: ${e}`);
}

if (nconf.get('help')) {
console.log(chalk.blue.bold('Press CTRL+C to quit.'));
spawnChild(nconf.get('wrap-cmd'));
})();

/******************************************************************************/

function showHelpQuit() {
console.log('Options:');
console.log(' --help - Prints this usage info');
console.log(' --list - Prints available serial ports');
Expand All @@ -29,31 +45,28 @@ if (nconf.get('help')) {
process.exit();
}

const server = require('./lib/server.js');
server
.start(nconf)
.then(() => console.log(chalk.blue.bold('Press CTRL+C to quit.')))
.then(() => nconf.get('wrap-cmd') && spawnChild())
.catch(console.error);

function showVersionQuit() {
console.log(version);
process.exit();
}

function showPortsQuit() {
SerialPort.list()
.then(portList => {
console.log(chalk.yellow.bold("Paste one or more of these serial ports into 'config.json':"));
for (let port of portList) {
let attrs = Object.keys(port)
.filter(key => key !== 'comName')
.reduce((sum, key) => port[key] ? sum.concat(`${key}: ${port[key]}`) : sum, []);
console.log(chalk.yellow(`"${port.comName}", ` + (attrs.length ? `// ${attrs.join(', ')}` : '')));
}
process.exit();
})
.catch(console.error);
async function showPortsQuit() {
let portList;

try {
portList = await SerialPort.list();
} catch(e) {
return console.error(e);
}

console.log(chalk.yellow.bold("Paste one or more of these serial ports into 'config.json':"));
for (let port of portList) {
let attrs = Object.keys(port)
.filter(key => key !== 'comName')
.reduce((sum, key) => port[key] ? sum.concat(`${key}: ${port[key]}`) : sum, []);
console.log(chalk.yellow(`"${port.comName}", ` + (attrs.length ? `// ${attrs.join(', ')}` : '')));
}
process.exit();
}

function readConfig(file) {
Expand All @@ -75,8 +88,8 @@ function verifyConfig(config) {
}

// Spawn a child process with a suicide pact; parent & child die together.
function spawnChild() {
const cmd = nconf.get('wrap-cmd');
function spawnChild(cmd) {
if (!cmd) return;
const cwd = nconf.get('wrap-dir');

console.log(chalk.green(`Spawning subprocess '${cmd}'...`))
Expand All @@ -87,20 +100,23 @@ function spawnChild() {
};
const child = spawn(cmd, [], spawnOpts);

process.on('SIGINT', onExit);
process.on('exit', onExit);
child.on('exit', onChildExit)

function onChildExit(code) {
console.log(chalk.green('Wrapped process exited, server exiting...'));
process.removeListener('exit', onExit);
process.exit(code);
}

function onExit(code) {
console.log(chalk.green('Exiting, killing subprocess...'))

child.removeAllListeners();
process.removeListener('exit', onExit);

child.kill.bind(child);
process.exit(code);
}

process.on('SIGINT', onExit);
process.on('exit', onExit);
child.on('exit', onChildExit)
}
1 change: 1 addition & 0 deletions lib/bitfield.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class Bitfield {
let byteIdx = byteIndex(index);
let mask = bitMask(index);
this.buffer.copy(newBuf);

if (value) {
newBuf[byteIdx] = newBuf[byteIdx] | mask;
} else {
Expand Down
37 changes: 33 additions & 4 deletions lib/buttonStatus.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@
const EventEmitter = require('events').EventEmitter;
const Bitfield = require('./bitfield');

// MAPPINGS
// Mappings are key-value pairs with "actual board button number" => "Arduino pin"

class ButtonStatus extends EventEmitter {
constructor(mapping, packetByteLength = 8) {
super();
this.mapping = mapping;
this.mapHwToLogical = mapping;
this.mapLogicalToHw = invertArray(mapping);
// Bitfield states directly represent hardware state with hardware pin numbers.
// These need to be looked up in `this.mapping` to get the logical software
// button numbers.
this.state = new Bitfield(new Buffer(packetByteLength));
this.oldState = new Bitfield(new Buffer(packetByteLength));
this.dataQueue = new Buffer(0);
Expand Down Expand Up @@ -34,15 +42,28 @@ class ButtonStatus extends EventEmitter {
this.dataQueue = dataQueue;
}

set(buttonNum, value) {
this.state.copyTo(this.oldState);
const hardButtonNum = this.mapLogicalToHw[buttonNum];
this.state = this.state.setAt(hardButtonNum, value);
this._triggerEvents();
}

isPressed(i) {
return !!this.state.at(i);
return !!this.state.at(this.mapLogicalToHw[i]);
}

toBitfield() {
const newBitfield = new Bitfield(this.state.length);
newBitfield.copyFrom(this.state);
return newBitfield;
}

toJSON() {
let mappedStates = { length: 0, buttons: {} };
for (let buttonIdx = 0; buttonIdx < this.state.length; buttonIdx++) {
if (this.mapping[buttonIdx] === undefined) continue;
let mappedButtonIdx = this.mapping[buttonIdx];
let mappedButtonIdx = this.mapLogicalToHw[buttonIdx];
if (mappedButtonIdx === undefined) continue;
mappedStates.buttons[mappedButtonIdx] = this.isPressed(buttonIdx);
mappedStates.length++;
}
Expand All @@ -51,7 +72,7 @@ class ButtonStatus extends EventEmitter {

_triggerEvents() {
for (let {index, value} of this._changedList()) {
this.emit(value ? 'buttonDown' : 'buttonUp', this.mapping[index]);
this.emit(value ? 'buttonDown' : 'buttonUp', this.mapHwToLogical[index]);
}
}

Expand All @@ -60,3 +81,11 @@ class ButtonStatus extends EventEmitter {
}
}
module.exports = ButtonStatus;

function invertArray(list) {
var newList = [];
for (let i = 0; i < list.length; i++) {
newList[list[i]] = i;
}
return newList;
}
33 changes: 17 additions & 16 deletions lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ async function start(config) {
const serverPort = config.get('port');
const serverHost = config.get('host') || '127.0.0.1';
const serialPortNames = config.get('serialPorts');

const buttonStatusCount = config.get('testmode') ? 2 : serialPortNames.length;
// Read configs and create things.
const mappings = await readMappings(config.get('mappingFiles'));
const buttonStatuses = createButtonStatuses(serialPortNames.length, mappings);
const buttonStatuses = createButtonStatuses(buttonStatusCount, mappings);
const serialPorts = openSerialPorts(serialPortNames, config.get('testmode'));

// Connect serial port data inflow to button status decoders.
Expand All @@ -38,12 +38,18 @@ function stop(done) {
}

async function readMappings(mappingFiles) {
const mappings = [];
for (let file of mappingFiles) {
const parsedXML = await readFileXML(file);
mappings.push(parsedXML.ArrayOfInt.int);
const mappingContents = [];
for (let i = 0; i < mappingFiles.length; i++) {
mappingContents.push(await readFileXML(mappingFiles[i]));
}
return mappings;
return mappingContents.reduce((memo, xml) => {
const mapping = [];
xml.ArrayOfInt.int.forEach((hardwareNum, interpretedNum) => {
mapping[hardwareNum] = interpretedNum;
});
memo.push(mapping.reverse());
return memo;
}, []);
}

function createButtonStatuses(count, mappings) {
Expand Down Expand Up @@ -135,6 +141,7 @@ function startHTTPServer(app, serverPort, serverHost) {
}

function startWebSocketServer(config, httpServer, buttonStatuses) {
const self = this;
const testmode = config.get('testmode');
const serialPorts = config.get('serialPorts');
const wsServer = new WebSocket.Server({ server: httpServer });
Expand All @@ -146,14 +153,8 @@ function startWebSocketServer(config, httpServer, buttonStatuses) {
if (testmode) {
socket.on('message', data => {
if (!data.match(/^\w+:\d+:\d+$/)) return;
const [name, deviceNum, buttonNum] = data.split(':');
const event = {
name,
deviceNum,
deviceName: `Fake Device ${deviceNum}`,
buttonNum
};
wsServer.broadcast(JSON.stringify(event));
const [eventName, deviceNum, buttonNum] = data.split(':');
buttonStatuses[deviceNum].set(buttonNum, eventName === 'buttonDown' ? 1 : 0);
});
}
});
Expand All @@ -179,7 +180,7 @@ function createMessageSender(name, deviceIdx, deviceName, wsServer) {
const message = {
name: name,
deviceNum: deviceIdx,
deviceName: deviceName,
deviceName: deviceName || `Device ${deviceIdx}`,
buttonNum: buttonNum
};
wsServer.broadcast(JSON.stringify(message));
Expand Down
Loading

0 comments on commit ef137ee

Please sign in to comment.