IRTCP v1.0.0 (Hub server for using IR module via HTTP)



  • What is it?
    Hub TCP server for using the IR module via HTTP.
    Both to record of new signals with a given name and to play them back.
    Using net since SDK doesn't have the nodejs HTTP-module available.

    Why?
    I really wanted to be able to call my IR module from my phone homescreen or Google Home (via IFTTT). I got it working so I wanted to share it if anyone else find it helpful :)

    The code is what could be called a POC, so code feedback and/or changes are totally welcome.

    How does it work?

    1. Record a signal GET {ip}:1338?record={name_of_signal}
    2. Plays a signal GET {ip}:1338?cmd={name_of_signal}
    3. Cancel recording GET {ip}:1338?cancelRecord=true

    Example;

    1. call GET 192.168.0.100:1338?record=volume_up
    2. Press Volume Up on remote towards IR module
    3. returns 200 OK Signal volume_up stored!
    4. call GET 192.168.0.100:1338?cmd=volume_up
    5. returns 200 OK Signal sent!
    6. Volume goes up :)

    The code

    module.json

    {
    	"name": "IRTCP",
    	"version": "1.0.0"
    }
    

    main.js

    // main.js
    const net = require("net");
    const ir = require("ir");
    const datastore = require("datastore");
    
    const utils = require("./utils");
    
    const respondToClient = function(c, statusCode, message) {
    	c.write(
    		"HTTP/1.1 " + statusCode + "\r\n" +
    		"\r\n"
    	);
    	c.write(message);
    	c.end();
    }
    
    const handleCmd = function(c, cmd) {
    	datastore.get(cmd, function(err, strBuffer) {
    		if (!err && strBuffer) {
    			var signal = new Uint32Array(utils.str2ab(strBuffer));
    			if (!ArrayBuffer.isView(signal)) {
    				return respondToClient(c, "422 Unprocessable Entity", "Unknown signal");
    			}
    
    			ir.play(signal, function(err) {
    				if (!err) {
    					return respondToClient(c, "200 OK", "Signal sent");
    				}
    				return respondToClient(c, "500 Internal Server Error", "Couldn't send signal");
    			});
    		} else {
    			return respondToClient(c, "422 Unprocessable Entity", "Unknown cmd");
    		}
    	});
    }
    
    const handleRecord = function(c, name) {
    	/* TODO: add a timeout for listening for complete */
    	ir.record();
    	console.log("recording signal...");
    
    	ir.on("recordComplete", function(signal) {	
    		datastore.put(name, utils.ab2str(signal.buffer), function(err) {
    			if (!err) {
    				console.log("Signal " + name + " stored!");
    				return respondToClient(c, "200 OK", "Signal " + name + " stored!");
    			} else {
    				return respondToClient(c, "500 Internal Server Error", "Couldn't store signal");
    			}
    		});
    	});
    }
    
    
    var server = net.createServer(function(c) {
      console.log('server connected');
      c.on('end', function() {
        console.log('server disconnected');
      });
    
    	c.on('data', function(data) {
    		console.log(data);
    		var match = data.toString().match(/GET \/\?.[^ ]*/);
    
    		if (!match) {
    			return respondToClient(c, "403 Forbidden", "Forbidden method");
    		}
    
    		var params = utils.parseParams(match[0].split("GET /?")[1]);
    		
    		if (params.cmd) {
    			return handleCmd(c, params.cmd);
    		} else if (params.record && params.record.length > 0) {
    			return handleRecord(c, params.record);
    		} else if (params.cancelRecord) {
    			ir.cancelRecord();
    			console.log("recording canceled!");
    			return respondToClient(c, "200 OK", "Recording canceled!");
    		} else {
    			return respondToClient(c, "403 Forbidden", "Forbidden params");
    		}
    	});
    });
    
    server.listen(1338, function() {
      console.log('server bound', server.address().port);
    });
    

    utils.js

    const ab2str = function(buf) {
      return String.fromCharCode.apply(null, new Uint16Array(buf));
    }
    
    const str2ab = function(str) {
      var buf = new ArrayBuffer(str.length*2);
      var bufView = new Uint16Array(buf);
      for (var i=0, strLen=str.length; i < strLen; i++) {
        bufView[i] = str.charCodeAt(i);
      }
      return buf;
    }
    
    const parseParams = function (s) {
    	var obj = {};
    	var arr = s.split("&");
    	var temp;
    
    	for (i = 0; i < arr.length; i++) {
    		temp = arr[i].split("=");
    		obj[temp[0]] = temp[1];
    	}
    	
    	return obj;
    };
    
    exports.ab2str = ab2str;
    exports.str2ab = str2ab;
    exports.parseParams = parseParams;
    


  • 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 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;
    


  • @Emil Thanks, and thank you for quick responses on questions


  • FlicTeam

    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 using HTTP Shortcuts-app widget.


Log in to reply