Flic Home

    Community

    • Login
    • Search
    • Popular
    • Users

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

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

      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