Skip to content

Instantly share code, notes, and snippets.

@darylteo
Created May 24, 2015 07:32
Show Gist options
  • Save darylteo/bd1c0730cfca2ae78367 to your computer and use it in GitHub Desktop.
Save darylteo/bd1c0730cfca2ae78367 to your computer and use it in GitHub Desktop.
Custom build of Spark IO - accepts http urls for local cloud connections (some errors were annoying and removed for simplicity)
var es6 = require("es6-shim");
var net = require("net");
var Emitter = require("events").EventEmitter;
var https = require("https");
var http = require("http");
var priv = new Map();
var errors = {
cloud: "Unable to connect to spark cloud.",
firmware: "Unable to connect to the voodoospark firmware, has it been loaded?",
instance: "Expected instance of Spark.",
pwm: "PWM is only available on D0, D1, A0, A1, A4, A5, A6, A7"
};
var pins = [
{ id: "D0", modes: [0, 1, 3, 4] },
{ id: "D1", modes: [0, 1, 3, 4] },
{ id: "D2", modes: [0, 1] },
{ id: "D3", modes: [0, 1] },
{ id: "D4", modes: [0, 1] },
{ id: "D5", modes: [0, 1] },
{ id: "D6", modes: [0, 1] },
{ id: "D7", modes: [0, 1] },
{ id: "", modes: [] },
{ id: "", modes: [] },
{ id: "A0", modes: [0, 1, 2, 3, 4] },
{ id: "A1", modes: [0, 1, 2, 3, 4] },
{ id: "A2", modes: [0, 1, 2] },
{ id: "A3", modes: [0, 1, 2] },
{ id: "A4", modes: [0, 1, 2, 3, 4] },
{ id: "A5", modes: [0, 1, 2, 3, 4] },
{ id: "A6", modes: [0, 1, 2, 3, 4] },
{ id: "A7", modes: [0, 1, 2, 3, 4] }
];
var modes = Object.freeze({
INPUT: 0,
OUTPUT: 1,
ANALOG: 2,
PWM: 3,
SERVO: 4
});
var modesMap = [
"INPUT",
"OUTPUT",
"ANALOG",
"PWM",
"SERVO"
];
var DIGITAL_READ = 0x03;
var ANALOG_READ = 0x04;
var REPORTING = 0x05;
function service(cloudAddress, deviceId) {
cloudAddress = cloudAddress || "https://api.particle.io";
return cloudAddress + "/v1/devices/" + deviceId + "/";
}
function from7BitBytes(lsb, msb) {
if (Array.isArray(lsb)) {
msb = lsb[1];
lsb = lsb[0];
}
return lsb | (msb << 0x07);
}
function to7BitBytes(value) {
return [value & 0x7f, value >> 0x07 & 0x7f];
}
function processReceived(spark, data) {
var dlength = data.length;
var length, action, pin, pinName, pinIndex, port,
lsb, msb, value, portValue, type, event;
for (var i = 0; i < dlength; i++) {
spark.buffer.push(data.readUInt8(i));
}
length = spark.buffer.length;
if (length >= 4) {
while (length && (length % 4) === 0) {
action = spark.buffer.shift();
pin = spark.buffer.shift();
lsb = spark.buffer.shift();
msb = spark.buffer.shift();
value = from7BitBytes(lsb, msb);
// Digital reads are allowed to be
// reported on Analog pins
//
if (action === REPORTING) {
port = +pin;
portValue = +value;
for (var k = 0; k < 8; k++) {
pinIndex = k + (10 * port);
event = "digital-read-" + (port ? "A" : "D") + k;
value = portValue & (1 << k);
if (typeof spark._events[event] !== "undefined") {
spark.pins[pinIndex].value = value;
spark.emit(event, value);
}
}
}
if (action === DIGITAL_READ ||
action === ANALOG_READ) {
if (action === ANALOG_READ) {
pinName = "A" + (pin - 10);
type = "analog";
// This shifts the value 2 places to the left
// for compatibility with firmata's 10-bit ADC
// analog values. In the future it might be nice
// to allow some
value >>= 2;
}
if (action === DIGITAL_READ) {
pinName = "D" + pin;
type = "digital";
}
event = type + "-read-" + pinName;
spark.pins[pin].value = value;
spark.emit(event, value);
}
length = spark.buffer.length;
}
}
}
function Spark(opts) {
Emitter.call(this);
if (!(this instanceof Spark)) {
return new Spark(opts);
}
var state = {
isConnected: false,
isReading: false,
deviceId: opts.deviceId,
token: opts.token,
service: service(opts.cloudAddress, opts.deviceId),
host: opts.host || null,
port: opts.port || 8001,
client: null,
socket: null
};
this.name = "spark-io";
this.buffer = [];
this.isReady = false;
this.pins = pins.map(function(pin) {
return {
name: pin.id,
supportedModes: pin.modes,
mode: pin.modes[0],
value: 0
};
});
this.analogPins = this.pins.slice(10).map(function(pin, i) {
return i;
});
// Store private state
priv.set(this, state);
var afterCreate = function(error) {
if (error) {
this.emit("error", error);
} else {
state.isConnected = true;
this.emit("connect");
}
}.bind(this);
this.connect(function(error, data) {
// console.log( "connect -> connect -> handler" );
if (error !== undefined && error !== null) {
this.emit("error", error);
} else if (data.cmd !== "VarReturn") {
this.emit("error", errors.firmware);
} else {
var address = data.result.split(":");
state.host = address[0];
state.port = parseInt(address[1], 10);
// Moving into after connect so we can obtain the ip address
Spark.Client.create(this, afterCreate);
}
}.bind(this));
}
Spark.Client = {
create: function(spark, afterCreate) {
if (!(spark instanceof Spark)) {
throw new Error(errors.instance);
}
var state = priv.get(spark);
var connection = {
host: state.host,
port: state.port
};
var socket = net.connect(connection, function() {
// Set ready state bit
spark.isReady = true;
spark.emit("ready");
if (!state.isReading) {
state.isReading = true;
socket.on("data", function(data) {
processReceived(spark, data);
});
}
});
state.socket = socket;
afterCreate();
}
};
Spark.prototype = Object.create(Emitter.prototype, {
constructor: {
value: Spark
},
MODES: {
value: modes
},
HIGH: {
value: 1
},
LOW: {
value: 0
}
});
Spark.prototype.connect = function(handler) {
var state = priv.get(this);
var url = state.service;
var action = "endpoint";
var request;
if (state.isConnected) {
return this;
}
handler = handler.bind(this);
var targetUrl = url + action + "?access_token=" + state.token;
var options = require('url').parse(targetUrl);
options.rejectUnauthorized = false;
if(options.protocol.startsWith('https')) {
options.port = 443;
options.agent = new https.Agent(options);
options.client = https;
} else {
options.client = http;
}
request = options.client.get(options, function(res) {
var body = "", err;
res.on("data", function(d) {
body += d;
});
res.on("end", function () {
if (res.statusCode === 200) {
var data = JSON.parse(body);
if (data.error) {
err = "ERROR: " + data.code + " " + data.error_description;
}
if (handler) {
handler(err, data);
}
} else {
err = errors.cloud + ": code: " + res.statusCode;
if (handler) {
handler(new Error(err));
} else {
throw new Error(err);
}
}
});
}).on("error", function(e) {
console.log(e);
});
return this;
};
Spark.prototype.pinMode = function(pin, mode) {
var state = priv.get(this);
var buffer;
var offset;
var pinInt;
var sMode;
sMode = mode = +mode;
// Normalize when the mode is ANALOG (2)
if (mode === 2) {
// Normalize to pin string name if numeric pin
if (typeof pin === "number") {
pin = "A" + pin;
}
}
// For PWM (3), writes will be executed via analogWrite
if (mode === 3) {
sMode = 1;
}
offset = pin[0] === "A" ? 10 : 0;
pinInt = (pin.replace(/A|D/, "") | 0) + offset;
// Throw if attempting to create a PWM or SERVO on an incapable pin
// True PWM (3) is CONFIRMED available on:
//
// D0, D1, A0, A1, A5
//
//
if (this.pins[pinInt].supportedModes.indexOf(mode) === -1) {
throw new Error("Unsupported pin mode: " + modesMap[mode] + " for " + pin);
}
// Track the mode that user expects to see.
this.pins[pinInt].mode = mode;
// Send the coerced mode
buffer = new Buffer([ 0x00, pinInt, sMode ]);
// console.log(buffer);
state.socket.write(buffer);
return this;
};
["analogWrite", "digitalWrite", "servoWrite"].forEach(function(fn) {
var isAnalog = fn === "analogWrite";
var isServo = fn === "servoWrite";
var action = isAnalog ? 0x02 : (isServo ? 0x41 : 0x01);
Spark.prototype[fn] = function(pin, value) {
var state = priv.get(this);
var buffer = new Buffer(3);
var offset = pin[0] === "A" ? 10 : 0;
var pinInt = (pin.replace(/A|D/i, "") | 0) + offset;
buffer[0] = action;
buffer[1] = pinInt;
buffer[2] = value;
// console.log(buffer);
state.socket.write(buffer);
this.pins[pinInt].value = value;
return this;
};
});
// TODO: Define protocol for gather this information.
["analogRead", "digitalRead"].forEach(function(fn) {
var isAnalog = fn === "analogRead";
// Use 0x05 to get a continuous read.
var action = 0x05;
// var action = isAnalog ? 0x04 : 0x03;
// var offset = isAnalog ? 10 : 0;
var value = isAnalog ? 2 : 1;
var type = isAnalog ? "analog" : "digital";
Spark.prototype[fn] = function(pin, handler) {
var state = priv.get(this);
var buffer = new Buffer(3);
var pinInt;
var event;
if (isAnalog && typeof pin === "number") {
pin = "A" + pin;
}
var offset = pin[0] === "A" ? 10 : 0;
pinInt = (pin.replace(/A|D/i, "") | 0) + offset;
event = type + "-read-" + pin;
buffer[0] = action;
buffer[1] = pinInt;
buffer[2] = value;
// register a handler for
this.on(event, handler);
if (!state.isReading) {
state.isReading = true;
state.socket.on("data", function(data) {
processReceived(this, data);
}.bind(this));
}
// Tell the board we have a new pin to read
state.socket.write(buffer);
return this;
};
});
/**
* Compatibility Shimming
*/
Spark.prototype.setSamplingInterval = function(interval) {
var state = priv.get(this);
var safeInterval = Math.max(Math.min(Math.pow(2, 14) - 1, interval), 10);
priv.get(this).interval = safeInterval;
state.socket.write(new Buffer([0x06].concat(to7BitBytes(safeInterval))));
return this;
};
Spark.prototype.reset = function() {
return this;
};
Spark.prototype.close = function() {
var state = priv.get(this);
state.socket.close();
state.server.close();
};
module.exports = Spark;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment