Nubstep: Overview and Part I - knob.io
Nubstep: Overview and Part I - knob.io
Nubstep is the project @pedrogte, @telmofcosta and me presented at Codebits 2011.
It is a sound synthethizer that collects inputs from an arduino, and generates sound through Core Audio. It also includes a web console for looking at readings (not shown at the competition presentation due to presenter lameness). All the data flowing from the arduino to the synthetizer is channeled through hook.io.
Arduino: Collect data from analog inputs (Detailed in this post)
knob.io: Get readings from arduino and send them to hook.io (Also detailed in this post)
knob-view.io: Show arduino readings on a browser
nubstep: Synthetize sound waves and feed them to roar
roar: Node library to output sound through Core Audio
Reading the analog inputs on the arduino and sending them through the serial port is fairly easy. Here's the arduino sketch:
(If you're an arduino newbie, you can read "Serial port" as "USB")
void setup() { Serial.begin(9600); } void loop() { int analogValues[6]; int i; for(i=0; i<6; i++) analogValues[i] = analogRead(i); for(i=0; i<6; i++) Serial.println(analogValues[i]); Serial.println(); delay(100); }
This code may seem familiar, and it is, because it's basically this arduino example adapted to read all the 6 analog inputs.
Here's the fritzing sketch for our box:
Reading the serial port on node.js requires the node-serialport module. Currently this module is stable enough, so, the hard work consists on interpreting the arduino output correctly.
Here's code for reading the serial port and sending it to hook.io:
(See below for more details)
#!/usr/bin/env node var util = require("util"); var serialport = require("serialport"); var Hook = require('hook.io').Hook; var SerialPort = serialport.SerialPort; var SerialHook = exports.SerialHook = function (options) { var self = this; Hook.call(this, options); this.on('hook::ready', function () { var sp = new SerialPort("/dev/tty.usbserial-A7006xos", { parser: serialport.parsers.readline("\r\n\r\n") }); var first_time = true; var values = [-1, -1, -1, -1, -1, -1]; sp.on("data", function (data) { // always ignore the first sample // data may be incomplete -> no way to know if(first_time) { first_time = false; return; } var knobs = data.split("\r\n").map(function(k) { return parseInt(k,10); }); if(knobs.length !== 6) { return; } var changes = 0; knobs = knobs.reduce(function(map, k, i) { if(k !== values[i]) { map[i] = values[i] = k; changes++; } return map; }, {}); if(changes !== 0) { self.emit('knobs', knobs); console.log('Emitting: ' + util.inspect(knobs)); } }); }); }; util.inherits(SerialHook, Hook); var serialhook = new SerialHook({ name: 'serial', 'hook-host' : '0.0.0.0' }); serialhook.start();
Reading the first valid sample
The data stream sent by the arduino has the following structure:
... <analog input 0>(end of line -> \r\n) <analog input 1>(end of line) <analog input 2>(end of line) <analog input 3>(end of line) <analog input 4>(end of line) <analog input 5>(end of line) (empty line, which means just an end of line) <analog input 0>(end of line) <analog input 1>(end of line) <analog input 2>(end of line) <analog input 3>(end of line) <analog input 4>(end of line) <analog input 5>(end of line) ...
... 511 255 767 1023 0 511 511 239 767 1023 0 511 ...
To read input from the arduino, node-serialport is initialized with:
var sp = new SerialPort("/dev/tty.usbserial-A7006xos", { parser: serialport.parsers.readline("\r\n\r\n") });
The parser option is a recent feature of node-serialport and used like this allows us to split readings from the arduino easily. In this case, note that the empty line between readings results in two consequent end-of-lines on the data stream (one from the last input being read and the other from the empty line).
Using the parser option simplifies parsing a lot, so, getting each reading becomes just wait for data with an event listener:
sp.on("data", function (data) { //... }
There is no flow control nor any field delimiters (apart from the line endings). This means that the application reading the arduino feed may start reading it on the middle of something, so, the first input reaching the app (for the data given above) could be:
23 0 511 511 239 767 1023 0 511 ...
The the first number is a truncated 1023. This is why the code always ignores the first set of readings, even if it contains the expected 6 readings.
Reading 6 and only 6 inputs
The data transmission protocol presented here is too simple, not only there are no field definitions, packet delimiters, etc, but there are also no checksums. This results in the following stream being received from time to time:
... 511 255 767 1023 0 511 // Note no empty line between these values. 511 // 239 767 1023 0 511 ...
This kind of error results in a stream of 12 values being read instead of 2 sets of 6 values. Due to the nature of this application, ignoring this values is safe, so off they go.
Caveat emptor: If you're running this code, check for the device used to initialize SerialPort - it may be different according to the arduino version that you're using, so make it the same as the one that's configured on the arduino IDE.
To define a hook in hook.io you start by extending the Hook class:
var SerialHook = exports.SerialHook = function (options) { //... }; util.inherits(SerialHook, Hook);
To boot hook.io, instantiate the hook and start it:
var serialhook = new SerialHook({ name: 'serial', 'hook-host' : '0.0.0.0' }); serialhook.start();
The actual hook code relies on calling the super constructor and waiting for a hook::ready event:
// ... Hook.call(this, options); this.on('hook::ready', function () { // ...
For our data protocol we chose to only send value variations, so the following snippet deals with detecting those changes and sending them encoded as maps to hook.io:
var changes = 0; knobs = knobs.reduce(function(map, k, i) { if(k !== values[i]) { map[i] = values[i] = k; changes++; } return map; }, {}); if(changes !== 0) { self.emit('knobs', knobs); }