Avatar photo

Visualize activity at your next event with lasers. Lasers!

A little while back we sat down with the good people over at Particle and figured out a way we could show off how to use Keen IO and Particle at an in person event called DevGuild. Particle has some seriously cool hardware and software tools for prototyping, scaling, and manage IoT products.

We ended up deciding to put some trip wires around the room that were connected to WiFi via a Particle board. When it was tripped it sent an event to Keen with a unique id for the laser that was tripped.

We got some hardware to attach to our particle. It included this laser, with this mount, and this photoresistor.

Here was the code we wrote to get the laser working with Particle:

int laser = 0;
unsigned int lastPublish = 0;
bool lastState = false;

void setup() {
    pinMode(A0, INPUT_PULLDOWN);
    pinMode(D7, OUTPUT);
}

void loop() {
    int val = analogRead(A0);
    unsigned int now = millis();
    unsigned int elapsed = now - lastPublish;


    bool broken = val = 1000) {
        String topic = ((broken) ? "laser/alarm" :  "laser/reset");
        String message = String(val);
        Particle.publish(topic, message);
        lastState = broken;
        lastPublish = millis();
    }
}

Once we had the laser working we configured a Particle webhook to post to a Keen IO endpoint.

The webhook code looks a little something like this!

{
   "eventName": "laser",
   "url": "http://api.keen.io/3.0/projects/YOUR_PROJECT_ID/events lasers",
   "requestType": "POST",
   "headers": {
    "Authorization": "YOUR_AUTH_KEY"
   },
    "json": {
      "lasers": {
         "test": "1"
      }
    }
 }

Then events started rolling in!

The next thing we had to do was make some sense out of the data, so we made a real time interactive visualization. We used SVG to make a very rough view of the rooms layout, which happened to just be a rectangle, and then popped two dot’s connected by a line on the page when a new new laser ID showed up in Keen. Once the dots are on the page you can move them around and place them where they physically are in the room.

The last step was to have the line blink to represent the laser getting tripped, in real time. We also added a fun little feature where, if a trip laser is getting a ton of activity, the line gets bigger to represent heavy traffic in that area of the venue.

We used Firebase for persistence, Node + ReactJS for the interface, and Keen IO for the rest. Here’s the app code:

var React = require('react'),
    Keen = require('keen-js');

React.initializeTouchEvents(true);

var config = require('../config');

// Configure Firebase
var firebaseRef = new Firebase(config['firebase']);

// Configure Keen
// 

https://keen.io/project/KEEN_PROJECT_ID/workbench


var client = new Keen(config['keen']);
var gates = new Keen.Query('count', {
  eventCollection: 'lasers',
  groupBy: 'coreid',
  timeframe: 'this_1_minute'
});
var totals = new Keen.Query('count', {
  eventCollection: 'lasers',
  groupBy: 'coreid'
});


var Gate = React.createClass({

  getInitialState: function(){
    var rando = Math.round(Math.random()*300) + 50;
    return {
      a: {
        active: false,
        radius: 15,
        x: rando,
        y: rando - 50
      },
      b: {
        active: false,
        radius: 15,
        x: rando,
        y: rando + 50
      },
      stroke: '#808080',
      strokeWidth: 5
    }
  },

  handleMove: function(i, e){
    var state = this.state[i];
    if (!state.active) return;
    state.x = this.props.trace[0];
    state.y = this.props.trace[1];
    this.setState(state);
    firebaseRef.child('/gates/' + this.props.id).update(this.state);
  },
  handleDown: function(i,e){
    var state = this.state[i];
    state.active = true;
    state.radius = 25;
    this.setState(state);
    firebaseRef.child('/gates/' + this.props.id).update(this.state);
  },
  handleUp: function(i,e){
    var state = this.state[i];
    state.active = false;
    state.radius = 15;
    this.setState(state);
    firebaseRef.child('/gates/' + this.props.id).update(this.state);
  },

  tick: function(){
    var self = this;
    var diff = self.props.weight - self.state.weight;
    console.log(diff > 0, self.props.total);
    var interval = setInterval(function(){
      if (diff > 0) {
        self.setState({
          weight: self.state.weight ? 0 : self.props.weight,
          total: self.props.total
        });
        diff--;
      }
      else {
        clearInterval(interval);
        if (!self.state.weight) {
          self.setState({
            weight: self.props.weight
          });
        }
      }
    }, 500);
  },

  componentDidMount: function(){
    this.setState(this.props);
    setInterval(this.tick, 1000);
  },

  render: function(){
    return 



      {'ID: ' + this.props.id}
      {'Total: ' + this.state.total}

  }
});


var Stage = React.createClass({

  getDefaultProps: function(){
    return {
      gates: []
    }
  },

  getInitialState: function() {
    return {
      ix: 100,
      iy: 100,
      height: window.innerHeight,
      width: window.innerWidth,
      position: {
        x: window.innerWidth / 2,
        y: window.innerHeight / 2
      }
    };
  },

  getKeenResults: function(){
    client.run([gates, totals], function(err, res){
      res[0].result.forEach(function(record, i){
        var id = record.coreid;
        if (id) {
          firebaseRef.child('/gates/' + id).update({
            id: id,
            weight: record.result,
            total: res[1].result[i].result
          });
        }
      });
    });
  },

  componentDidMount: function() {
    var self = this;

    this.getKeenResults();
    setInterval(this.getKeenResults, 1000 * 5);

    firebaseRef.child('/gates').on('child_added', function(data){
      self.props.gates.push(data.val());
      self.forceUpdate();
    });

    firebaseRef.child('/gates').on('child_changed', function(data){
      self.props.gates.forEach(function(gate, i){
        if (gate.id === data.key()) {
          self.props.gates[i] = data.val();
        }
      });
    });

    firebaseRef.child('/gates').on('child_removed', function(data){
      self.props.gates.forEach(function(gate, i){
        if (gate.id === data.key()) {
          self.props.gates.splice(i, 1);
        }
      });
    });

    window.addEventListener('resize', this.handleResize);
    document.addEventListener('touchmove', this.preventBehavior, false);
  },

  handleResize: function(){
    this.setState({
      'height': window.innerHeight,
      'width': window.innerWidth
    });
  },

  preventBehavior: function(e){
    e.preventDefault();
  },

  handleMove: function(e){
    var touch = (e.touches) ? e.touches[0] : false;
    this.setState({
      ix: touch ? touch.pageX : e.clientX,
      iy: touch ? touch.pageY : e.clientY
    });
  },

  render: function(){
    var gates = this.props.gates.map(function(gate, index){
      // console.log(gate);
      return 
    }, this);

    return 






        {gates}

  }

});

React.render( , document.getElementById('stage') );

module.exports = Stage;

The config.json file has the Keen API keys and the address of our Firebase app.

The index file puts our page on the internet and has is a div placeholder with an id of ‘stage’ and all the things are injected:

sparkeen
  <style scoped="scoped">
    body { margin: 0; }
    svg circle { cursor: move; }
  </style>

https://cdn.firebase.com/js/client/2.0.1/firebase.jshttp://./app/index.js

The result was a very fun interactive visual that people really seemed to enjoy. + we got to talk about lasers, double win!

Check it out and hookup your next event and/or let us know what you come up with! Big ups to Dustin and Zach for collaborating on this with me. High-Five