Flic Home

    Community

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

    jitmo

    @jitmo

    1
    Reputation
    55
    Profile views
    7
    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

    Latest posts made by 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