Flic Home

    Community

    • Login
    • Search
    • Popular
    • Users

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

    Flic Hub SDK
    6
    16
    5892
    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.
    • 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