28 Jul 2021, 11:11

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;