Flic Home

    Community

    • Login
    • Search
    • Popular
    • Users

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

    Flic Hub SDK
    6
    17
    5912
    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 oskaremilsson

      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, "0.0.0.0", 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
      • Emil
        Emil FlicTeam @mullz last edited by

        @mullz Now I get it. There is another bug in both the code from @oskaremilsson as well as the code from @jitmo. By using the .on method on the IR object, you add a new listener (i.e. append it to the list of listeners). You never remove the listeners, however. That's why all the registered listeners get called on every recordComplete event. You can for example use the .once method instead. Just remember to also manually remove the listener when you cancel the recording process using for example the .removeAllListeners method.

        1 Reply Last reply Reply Quote 0
        • mullz
          mullz @Emil last edited by

          @Emil This is helpful info for investigation, but I still can't figure out why the console output appears to indicate overwriting multiple commands every time a new one is recorded:

          server connected
          GET /?record=test HTTP/1.1

          Host: flichub.local:1338

          Accept: /

          Accept-Language: en-US;q=1, ru-RU;q=0.9, es-US;q=0.8

          Connection: keep-alive

          Accept-Encoding: gzip, deflate

          User-Agent: Flic/7.0.5 (iPhone; iOS 26.1; Scale/3.00)

          recording signal...
          Signal testUp stored!
          Signal testDwn stored!
          Signal test stored!
          Signal test stored!
          server disconnected
          server connected
          GET /?cmd=test HTTP/1.1

          Host: flichub.local:1338

          Accept: /

          Accept-Language: en-US;q=1, ru-RU;q=0.9, es-US;q=0.8

          Connection: keep-alive

          Accept-Encoding: gzip, deflate

          User-Agent: Flic/7.0.5 (iPhone; iOS 26.1; Scale/3.00)

          server disconnected

          Unless I am reading it wrong, recording a new signal saves it, but also overwrites all previous recordings with the new IR signal, as well.

          (I sent these commands from the iOS flic app, using the Internet Request feature, from one of the widgets, if that is helpful info)

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

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

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

              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?

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

                The next firmware update will allow for the previous call signature as well.

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

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

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

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

                      @johan-0 could you try to change the listen call to

                      server.listen(1338, "0.0.0.0", function(...
                      

                      instead?

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

                        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)
                        
                        Emil 1 Reply Last reply Reply Quote 1
                        • 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