IRTCP v1.0.0 (Hub server for using IR module via HTTP)
-
@mullz I couldn't reproduce the issue. Seems to work as intended for me with oskaremilsson's code.
One thing I noticed is that the conversion to string might not always work correctly the way the encoding is done. The string that is to be saved needs to be stored in proper UTF-16 format; now you could theoretically accidentally create invalid surrogate pairs. -
Hello,
I've been trying to get this script up and running on my own Flic Hub LR (firmware v4.6.0), but I seem to be running into trouble with the datastore module; nothing appears to stay saved, or else entering one IR code overwrites ALL IR codes with with string from the most recently recorded one. When I tried to investigate what was being written to the datastore, I noticed the datastore.db file appears corrupt in some way (dB Browser for SQLite says it's unreadable/malformed). This happens in both the original "IRTCP" code @oskaremilsson posted, as well as the "ir-server" version posted by @jitmo.
Am I missing something silly, or is there a change in the way the datastore puts/gets work in the latest firmware?
-
The next firmware update will allow for the previous call signature as well.
-
@johan-0 This is the best thing I have ever encountered in my life of a developer. I just googled the issue I got in console and arrived at my own thread where the answer was already solved. hahah
-
@Emil It worked, thanks so much! I didn't get any email notification about a response so I just figured no one would answer, but now I got around to looking at it again

-
@johan-0 could you try to change the
listencall toserver.listen(1338, "0.0.0.0", function(...instead?
-
Hej @oskaremilsson and others using this, thanks for the code! I've been using this successfully for years, and uploaded it with some modifications to GitHub. Now it just stopped working and I don't understand why, but I suspect an API change? @emil maybe you can answer this? My flic hub is on version 4.3.4 and I'm getting this error
RangeError: host does not a contain a valid IPv4 address: undefined at listen (core.js:1677) at <anonymous> (root/IR Server/tcpServer.js:123) at require (core.js:2902) at <anonymous> (root/IR Server/main.js:3) at require (core.js:2902) at requireMain (core.js:2910) at handlePacket (core.js:2944) at onPipeData (core.js:3015) -
This is awesome!
In lack of better Javascript skills, I actually configured a Flic 2 button (through the "front door" main app interface ), just to be able to trigger the record action without a phone. Even though it worked, it felt kind of "offensive" to the code (like using a MacBook Pro as a bookend
). I guess I'll hack around with the example code just to get everything in one place 
️
-
Hey @oskaremilsson, works for me

-
@jitmo simply amazing!
If I were to put the original code on GitHub, would you be up to make these changes there so we get a nice history and stuff? -
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 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 or playIR/tvOn,lightsOffTo stagger when the IR codes start use
<IR_NAME>:<SECONDS_DELAY>(0.1 second precision), eg:playIR/tvOn:2.0,lightsOffHere'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; -
@Emil Thanks, and thank you for quick responses on questions
-
Nice!
This is great work, like to see more of this. -
I forgot to add how I'm calling this from my homecreen of my phone.
I'm usingHTTP Shortcuts-app widget.