(example code) Flic Hub HTTP server to IR bridge: record/play IR codes and put/get crowd-sourced IR codes
-
Cross-posting my reply to this topic I have built on top of @oskaremilsson's great work 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 (all GET so they can be used from a browser while testing):
GET /putIRarray/<IR_NAME>:<IR_ARRAY>
to store IR arrays crowd-sourced from othersGET /getIRarray/<IR_NAME>
to share your IR arrays with othersGET /recordIR/<IR_NAME>
to record an IR array, with timeoutGET /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
orplayIR/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 = true; 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;