Flic Home

    Community

    • Login
    • Search
    • Popular
    • Users

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

    Flic Hub SDK
    5
    13
    2448
    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.
    • 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