Flic Home

    Community

    • Login
    • Search
    • Popular
    • Users
    1. Home
    2. jitmo
    • Profile
    • Following 0
    • Followers 0
    • Topics 3
    • Posts 10
    • Best 2
    • Controversial 0
    • Groups 0

    jitmo

    @jitmo

    2
    Reputation
    56
    Profile views
    10
    Posts
    0
    Followers
    0
    Following
    Joined Last Online

    jitmo Unfollow Follow

    Best posts made by jitmo

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

      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;
      
      posted in Flic Hub SDK
      jitmo
      jitmo
    • RE: Access integrations in SDK?

      Hey @rs, very late to the party here.

      The Flic hub does not appear to have third party integrations, although the Hue API looks pretty straightforward.

      Did you find the Hue developer docs? https://developers.meethue.com/develop/get-started-2/

      Did you have much luck/progress?

      How can I help?

      posted in Flic Hub SDK
      jitmo
      jitmo

    Latest posts made by jitmo

    • RE: Access integrations in SDK?

      Hey @rs, very late to the party here.

      The Flic hub does not appear to have third party integrations, although the Hue API looks pretty straightforward.

      Did you find the Hue developer docs? https://developers.meethue.com/develop/get-started-2/

      Did you have much luck/progress?

      How can I help?

      posted in Flic Hub SDK
      jitmo
      jitmo
    • RE: Coding needed (paid task)

      Hey @jonas-wallin, Iโ€™m very late to the party although more than happy to help (pro bono), if you still need the assist!

      posted in Flic Hub SDK
      jitmo
      jitmo
    • RE: Some Hub SDK examples

      Hey @andy, a little late to the party here, looks like youโ€™re trying to integrate to the old LightwaveRF Connect series.

      I have Connect series in my house and have successfully used their API, although my Flic hub simply sends button clicks to a Raspberry Pi which does the integration to LightwaveRF.

      Have you had any luck/progress on this? How can I help?

      posted in Flic Hub SDK
      jitmo
      jitmo
    • RE: IRTCP v1.0.0 (Hub server for using IR module via HTTP)

      Hey @oskaremilsson, works for me ๐Ÿ™‚

      posted in Flic Hub SDK
      jitmo
      jitmo
    • (example code) Flic Hub HTTP server to IR bridge: record/play IR codes and put/get crowd-sourced IR codes

      Cross-posting my reply to this topic I have built on top of @oskaremilsson's great work 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 (all GET so they can be used from a browser while testing):

      • 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 = true;
      
      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;
      
      posted in Developers
      jitmo
      jitmo
    • RE: IRTCP v1.0.0 (Hub server for using IR module via HTTP)

      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;
      
      posted in Flic Hub SDK
      jitmo
      jitmo
    • RE: Flic Hub SDK errors when setting UDP setMulticastTTL and setTTL

      Hey @Emil, thanks for the quick response!

      Any tips for sending a broadcast UDP packet to multiple Raspberry Pi's (also using Node). Do I need to set the MCAST_ADDR when calling server.bind on the Flic Hub SDK? (I have tried many combinations of ports and addresses on the Flic Hub SDK and Pi's).

      To send a broadcast message from the Flic Hub SDK I use the following:

      const buttonManager = require('buttons');
      const dgram = require('dgram');
      const server = dgram.createSocket('udp4');
      
      const PORT = 31091;             // TCP/UDP port
      const MCAST_ADDR = '234.0.0.0'; // use a class D (multicast) IP address https://en.wikipedia.org/wiki/Classful_network
      
      server.on('message', function(msg, rinfo) {
        console.log('@ sever.on.message');
        console.log('# msg: ', msg);
        console.log('# rinfo: ', JSON.stringify(rinfo,null,2));
      });
      
      server.bind({
        port: PORT
      },function(){
        server.setBroadcast(true);
        server.addMembership(MCAST_ADDR);
        console.log('# server.address: ', JSON.stringify(server.address(),null,2));
      });
      
      buttonManager.on('buttonSingleOrDoubleClickOrHold', function(obj) {
      
        var button = buttonManager.getButton(obj.bdaddr);
        var clickType = obj.isSingleClick ? 'click' : obj.isDoubleClick ? 'double_click' : 'hold';
      
        var message = button.name+' '+clickType+' '+button.batteryStatus;
        console.log(message);
      
        server.send(message, 0, message.length, PORT, MCAST_ADDR, function(err){
          console.log('@ server.send callback');
          console.log('# err: ', err);
          console.log('# server.address: ', JSON.stringify(server.address(),null,2));
        });
      
      });
      

      Here is a relevant code snippet on the Pi (for brevity I am not showing the error, connect or listening event handlers) :

      const dgram = require('dgram');
      const client = dgram.createSocket('udp4');
      
      client.on('message', (msg, rinfo) => {
        console.log('@ client.on.message');
        console.log(`'${msg}' from ${rinfo.address}:${rinfo.port}`);
      });
      
      client.bind({
        address: '234.0.0.0',
        port: 31091,
        exclusive: false
      });
      

      When the Flic Hub SDK sends the UDP message I can see the UDP on.message event on the Flic SDK console, although the on.message event is not triggered on the Pi.

      On the Pi's I have tried address 0.0.0.0 as well as 255.255.255.255, I have tried omitting port. None of these combinations work.

      Could this be due to the TTL issue or am I simply doing something silly like wrong port/address combinations?

      UPDATE: I can get the Flic Hub to send UDP to just one Pi by sending server.send(message, 0, message.length, PORT, <RaspPi_IP>) from the Flic Hub SDK and omitting the address param of client.bind on the Pi (which defaults to 0.0.0.0)... although I'm looking for a broadcast to multiple Pi's.

      posted in Developers
      jitmo
      jitmo
    • Flic Hub SDK errors when setting UDP setMulticastTTL and setTTL

      SUMMARY: When I receive a button event I want to sent a UDP multicast message to several Raspberry Pi's on my network (as they all handle different parts of my home control/automation). When using setMulticastTTL or setTTL I get the error:

      ReferenceError: identifier 'checekType' undefined
      

      ASIDE: this topic is mostly to log the possible typo in duktape or dgram. It is also possible/probable that I am using multicast in the wrong way, so any help with using multicast would be great too ๐Ÿ™‚

      CODE: Here is the relevant code snippet (buttons.js) showing just the UDP parts; you can swap out setMulticastTTL for setTTL to get the same error:

      const dgram = require('dgram');
      const server = dgram.createSocket('udp4');
      
      const PORT = 31091;
      const MCAST_ADDR = '234.0.0.0'; // use a class D (multicast) IP address https://en.wikipedia.org/wiki/Classful_network
      
      server.bind({
        port: PORT
      },function(){
        server.setBroadcast(true);
        server.setMulticastTTL(128);
        server.addMembership(MCAST_ADDR);
        console.log('# server.address: ', JSON.stringify(server.address(),null,2));
      });
      

      ERROR: Here is the error message in the console (line numbers will not match above snippet):

      ReferenceError: identifier 'checekType' undefined
          at [anon] (duktape.c:81026) internal
          at setMulticastTTL (dgram.js:248)
          at [anon] (root/Test/buttons.js:21)
          at [anon] (events.js:27)
          at emit (events.js:59)
          at [anon] (dgram.js:117) preventsyield
          at runInit () native strict preventsyield
          at handlePacket (pipe_communication.js:48)
          at readCallback (pipe_communication.js:93) preventsyield
      

      EARLY ANALYSIS: It looks like there is a typo of checekType (should this be checkType?) in duktape.c or possibly dgram.js

      posted in Developers
      jitmo
      jitmo