Flic Home

    Community

    • Login
    • Search
    • Popular
    • Users

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

    Flic Hub SDK
    4
    8
    869
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • oskaremilsson
      oskaremilsson last edited by

      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;
      
      1 Reply Last reply Reply Quote 2
      • andreas.lorentsen
        andreas.lorentsen last edited by andreas.lorentsen

        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 🙂

        ❤️👍🏼

        1 Reply Last reply Reply Quote 0
        • jitmo
          jitmo @oskaremilsson last edited by

          Hey @oskaremilsson, works for me 🙂

          1 Reply Last reply Reply Quote 0
          • oskaremilsson
            oskaremilsson @jitmo last edited by

            @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?

            jitmo 1 Reply Last reply Reply Quote 0
            • jitmo
              jitmo last edited by jitmo

              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;
              
              oskaremilsson 1 Reply Last reply Reply Quote 1
              • oskaremilsson
                oskaremilsson @Emil last edited by

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

                1 Reply Last reply Reply Quote 0
                • Emil
                  Emil FlicTeam last edited by

                  Nice!
                  This is great work, like to see more of this.

                  oskaremilsson 1 Reply Last reply Reply Quote 0
                  • oskaremilsson
                    oskaremilsson last edited by

                    I forgot to add how I'm calling this from my homecreen of my phone.
                    I'm using HTTP Shortcuts-app widget.

                    1 Reply Last reply Reply Quote 0
                    • First post
                      Last post