Hey @oskaremilsson and @Emil, great work on getting this together. I have built on top of this with the following:
- timeout when recording an IR signal
- optional logging of TCP requests/responses
- ignore favicon.ico requests when using a browser
- the server now has 4 methods using a semantic url path
The four server methods are:
GET /putIRarray/<IR_NAME>:<IR_ARRAY>
to store IR arrays crowd-sourced from others
GET /getIRarray/<IR_NAME>
to share your IR arrays with others
GET /recordIR/<IR_NAME>
to record an IR array, with timeout
GET /playIR/<IR_SEQUENCE>
to play IR sequences
<IR_NAME>
(string) is the name you choose for your IR code(s)
<IR_ARRAY>
(string) is, without spaces, in the form from the Flic Hub SDK docs, eg: [<CARRIER_FREQ>,<ON_us>,<OFF_us>,<ON_us>,...]
<IR_SEQUENCE>
(string) is a comma-separated list of <IR_NAME>
, eg: /playIR/tvOn or playIR/tvOn,lightsOff
To stagger when the IR codes start use <IR_NAME>:<SECONDS_DELAY>
(0.1 second precision), eg: playIR/tvOn:2.0,lightsOff
Here's the code:
// main.js
const tcpServer = require('./tcpServer');
// tcpServer.js
// Based on work by Oskar Emilsson https://community.flic.io/topic/18043/irtcp-v1-0-0-hub-server-for-using-ir-module-via-http
const net = require('net');
const irUtils = require('./irUtils');
// Set true to log TCP (HTTP) requests/responses
const logRequestResponse = false;
const respondToClient = function(c, statusCode, message, log) {
var content = 'HTTP/1.1 '+statusCode+'\r\n'+'\r\n'+message;
if(typeof log === 'undefined') log = logRequestResponse;
if(log) {
console.log('\n# HTTP RESPONSE');
console.log(content);
console.log('# END HTTP RESPONSE\n');
}
c.write(content);
c.end();
}
var server = net.createServer(function(c) {
console.log('Server connected');
c.on('end', function() {
console.log('Server disconnected\n');
});
c.on('data', function(data) {
// Convert TCP content byte array to string
var content = data.toString();
// Ignore favicon requests from a browser
if(content.indexOf('GET /favicon.ico') !== -1) {
console.log('# ignoring favicon.ico request');
return respondToClient(c, '200 OK', '', false);
}
if(logRequestResponse) {
console.log('\n# HTTP REQUEST');
console.log(content);
console.log('# END HTTP REQUEST\n');
}
// The first line of the raw TCP will look something like this "GET playIR/tvOn:2.0,lightsOff HTTP/1.1"
// Check for URL paths /recordIR/<IR_NAME>, /playIR/<IR_SEQUENCE>, /putIRarray/<IR_NAME>:<IR_ARRAY> or /getIRarray/<IR_NAME>
// <IR_ARRAY> is, without spaces, in the form from the docs [<CARRIER_FREQ>,<ON_us>,<OFF_us>,<ON_us>,...]
// <IR_SEQUENCE> is a comma-separated list of <IR_NAME>, eg: playIR/tvOn or playIR/tvOn,lightsOff
// To stagger when the IR codes start use <IR_NAME>:<SECONDS_DELAY> (0.1 second precision), eg: playIR/tvOn:2.0,lightsOff
// From the Hub SDK documentation "If another play is started before a previous one has completed, it gets enqueued and starts as soon as the previous completes (max 100 enqueued signals)"
var recordIRmatch = content.match(/GET \/recordIR\/(.[^ ]*) HTTP/);
var playIRmatch = content.match(/GET \/playIR\/(.[^ ]*) HTTP/);
var putIRarraymatch = content.match(/GET \/putIRarray\/(.[^ ]*) HTTP/);
var getIRarraymatch = content.match(/GET \/getIRarray\/(.[^ ]*) HTTP/);
if(recordIRmatch && recordIRmatch[1]) {
// Start recording an IR signal
irUtils.record(c, recordIRmatch[1]);
}
else if(playIRmatch && playIRmatch[1]) {
// Play an IR signal or IR signal sequence
var items = playIRmatch[1].split(',');
irUtils.play(c, items);
}
else if(putIRarraymatch && putIRarraymatch[1]) {
// Store an IR signal
var splitPath = putIRarraymatch[1].split(':');
if(splitPath.length == 2) {
var irArray = JSON.parse(splitPath[1]);
if(Array.isArray(irArray) && irArray.length % 2 === 0) {
irUtils.put(c, splitPath[0], splitPath[1]);
}
else {
respondToClient(c, '400 Bad Request', 'Use the form /putIRarray/<IR_NAME>:<IR_ARRAY>\r\n\r\n<IR_ARRAY> is, without spaces, in the form from the Flic Hub SDK docs [<CARRIER_FREQ>,<ON_us>,<OFF_us>,<ON_us>,...] and must have an even number of items (finishing with an <ON_us> item)');
}
}
else {
respondToClient(c, '400 Bad Request', 'Use the form /putIRarray/<IR_NAME>:<IR_ARRAY>\r\n\r\n<IR_ARRAY> is, without spaces, in the form from the Flic Hub SDK docs [<CARRIER_FREQ>,<ON_us>,<OFF_us>,<ON_us>,...] and must have an even number of items (finishing with an <ON_us> item)');
}
}
else if(getIRarraymatch && getIRarraymatch[1]) {
// Retrieve an IR signal
irUtils.get(c, getIRarraymatch[1]);
}
else {
respondToClient(c, '400 Bad Request', 'Valid url paths are recordIR/<IR_NAME> and playIR/<IR_SEQUENCE> \r\n\r\nWhere <IR_SEQUENCE> is a comma-separated list of <IR_NAME>, eg: playIR/tvOn or playIR/tvOn,lightsOff \r\n\r\nTo stagger when the IR codes start use <IR_NAME>:<SECONDS_DELAY> (0.1 second precision), eg: playIR/tvOn:2.0,lightsOff');
}
}); // on.data
}); // net.createServer
server.listen(1338, function() {
console.log('Server bound', server.address().port);
});
exports.respondToClient = respondToClient;
// irUtils.js
const ir = require('ir');
const datastore = require('datastore');
const server = require('./tcpServer');
// Set true to respond before playing IR signals (and after checking all signals have been recorded)
// You may want a faster response for the system requesting the IR signal(s) playback, although you will not know if ir.play() fails
const respondBeforePlaying = false;
const _irSignal2str = function(signal) {
// Instead of using signal.buffer use the JS object (undocumented, this might not always be available)
// The object keys are "0", "1", etc and the values are the integers that need to be in the array
// Convert the JS object to an array of integers
var items = [];
for(var i=0; i<Object.keys(signal).length;i++) {
items.push(signal[i]);
}
return JSON.stringify(items);
}
const _str2irSignal = function(str) {
return new Uint32Array(JSON.parse(str));
}
const put = function(c, name, data) {
console.log('@ irUtils.put',name,data);
datastore.put(name, data, function(err) {
console.log('@ datastore.put callback');
if (!err) {
console.log('IR signal '+name+' stored');
server.respondToClient(c, '200 OK', 'IR signal '+name+' stored');
} else {
console.error('# error: ', error);
server.respondToClient(c, '500 Internal Server Error', 'Could not store IR signal');
}
}); // datastore.put
}
const get = function(c, name) {
console.log('@ irUtils.get '+name);
datastore.get(name, function(err, str) {
console.log('@ datastore.get callback');
if(!err && typeof str === 'string' && str.length > 0) {
server.respondToClient(c, '200 OK', str);
}
else {
server.respondToClient(c, '404 Not Found', 'Could not find IR signal '+name);
}
}); // datastore.get
}
const record = function(c, name) {
console.log('@ irUtils.record '+name);
// Start recording
ir.record();
// Set up a timeout timer for 5 seconds
var timeoutTimer = setTimeout(function(){
ir.cancelRecord();
console.log('Recording IR signal '+name+' TIMEOUT');
clearTimeout(timeoutTimer);
server.respondToClient(c, '408 Request Timeout', 'Recording IR signal '+name+' TIMEOUT');
return;
},5000);
// Wait for recordComplete event
ir.on('recordComplete', function(signal) {
console.log('@ ir.on.recordComplete');
// Stop the timeout timer
clearTimeout(timeoutTimer);
// Convert the signal to a string
var data = _irSignal2str(signal);
console.log(data);
// Store the data
put(c, name, data);
}); // ir.on.recordComplete
}
const play = function(c, items) {
console.log('@ irUtils.play '+items);
// Check all the IR codes exist
const retrievalMs = 20;
var index = 0;
var irCodes = {};
var errors = '';
// datastore is async, so give each item time to be retrieved
var fetchingTimer = setInterval(function(){
var item = items[index].split(':')[0];
if(typeof irCodes[item] !== 'undefined') {
console.log('# '+item+' already retrieved');
if(++index === items.length) clearTimeout(fetchingTimer);
}
else {
console.log('# getting '+item+' from datastore')
datastore.get(item, function(err, str) {
console.log('@ datastore.get callback');
if(!err && typeof str === 'string' && str.length > 0) {
irCodes[item] = str;
}
else {
console.error('Cannot find IR code '+item+' in datastore.');
errors += 'Cannot find IR code '+item+' in datastore. ';
}
if(++index === items.length) clearTimeout(fetchingTimer);
}); // datastore.get
}
},retrievalMs); // setInterval
// Wait for datastore to finish
setTimeout(function(){
if(errors !== '') {
server.respondToClient(c, '400 Bad Request', errors);
return;
}
console.log(JSON.stringify(irCodes,null,2));
if(respondBeforePlaying) server.respondToClient(c, '200 OK', 'Sending IR signal(s)');
// Set up a timer to process the queue and pauses
var pausingTenths = 0;
var sendingTimer = setInterval(function(){
if(pausingTenths > 0) {
// Keep pausing
pausingTenths--;
}
else {
if(items.length > 0) {
var itemSplit = items.shift().split(':');
// Play the IR code
console.log('# Sending IR code '+itemSplit[0]);
var signal = _str2irSignal(irCodes[itemSplit[0]]);
ir.play(signal, function(err) {
if(err) {
clearTimeout(sendingTimer);
if(!respondBeforePlaying) server.respondToClient(c, '500 Internal Server Error', 'Could not send IR signal '+itemSplit[0]);
return;
}
});
// Add a pause if requested
if(itemSplit[1] && typeof parseFloat(itemSplit[1]) === 'number') {
var pause = parseFloat(itemSplit[1]);
console.log('# Adding '+pause+' seconds pause');
pausingTenths = parseInt(pause*10);
}
}
else {
// Finish up
console.log('# Finished IR send');
clearTimeout(sendingTimer);
if(!respondBeforePlaying) server.respondToClient(c, '200 OK', 'Sent IR signal(s)');
}
}
},100); // setInterval
},retrievalMs*(items.length+1)); // setTimeout
}
exports.put = put;
exports.get = get;
exports.record = record;
exports.play = play;