Hardware components | ||||||
![]() |
| × | 1 | |||
![]() |
| × | 1 | |||
![]() |
| × | 1 | |||
| × | 1 | ||||
Software apps and online services | ||||||
![]() |
|
Trigger your hardware project from anywhere, and make it sparkle! A Particle Spark Core or Photon puts WiFi everywhere. I built this little demo for a workshop at CascadiaFest 2015, and did a road test using my colleague Monica's phone tethering.
P.S.: I didn't have a resistor on hand. Use a resistor! Don't burn out your pins!
If you're interested in teaching this as a workshop, feel free to use my walkthrough presentation: Google Slides
First, make sure you've got Node.js installed and updated.
1. Say hello to your Particle
Create your Particle account: Go to https://build.particle.io and sign up.
Install the Node module for Particle: npm install -g particle-cli
Claim your Particle device: particle setup
- Save the device ID you get at the end of this process, in a file somewhere.
Explore the Particle online IDE: http://build.particle.io/build
- Grab your API token from the "Settings" pane – the cog icon at the bottom of the left sidebar. Put it in the file with your device ID.
2. Introduce it to Javascript
Set up VoodooSpark: This is a sketch that you save to the Core, so that it will listen for API commands coming over the web. Here's the process, taken from http://voodoospark.me/:
- Grab voodoospark.cpp and paste it into the Particle online editor.
- Click "Verify" and "Flash" to plonk it onto the device.
- Now, open your terminal and enter this curl command – subbing in the device ID and API token you grabbed earlier:
curl "https://api.spark.io/v1/devices/{DEVICE-ID}/endpoint?access_token={ACCESS-TOKEN}"
You don't need to do anything more from that tutorial. See how you're supposed to make a TCP connection and send commands defined in bytes? We can turn that into something a little friendlier, with Cylon!
Set up Cylon.js: Following the instructions given in the GitHub README for use with VoodooSpark, do this:
- Install cylon:
npm install cylon cylon-spark
- Save this example code as a .js file (for example, cascadia.js) – again, pasting in your device ID and API token from before.
3. Get blinking!
The code you just saved is a JavaScript app that blinks an LED on and off.
- So, go ahead and connect your LED's positive lead to pin d0 (digital pin 0).
- Connect the negative leg to one end of a 220-ohm resistor.
- The resistor's other leg gets connected to GND.
- Now, open your terminal window and navigate to the directory where you stored the cascadia.js app (or whatever you called it). Run:
node cascadia.js
- If you get an error, you may need to install cylon-gpio:
npm install cylon-gpio
Remember, this app isn't stored on your Core! So when you unplug your device and plug it into another power source, it won't start running the program again. But you can launch it from your terminal anytime, since it has your personal and device identifiers in the app.
Woohoo! Now you can write Javascript to connect your Core with about a million different platforms and APIs. Cylon was designed specifically for robots, but don't let that limit you :)
/**
******************************************************************************
* @file voodoospark.cpp
* @author Chris Williams
* @version V2.7.2
* @date 16-June-2015
* @brief Exposes the firmware level API through a TCP Connection initiated
* to the spark device
******************************************************************************
Copyright (c) 2015 Chris Williams (voodootikigod) All rights reserved.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
******************************************************************************
*/
#include "application.h"
#define DEBUG 0
#define PORT 48879
#define MAX_DATA_BYTES 128
// table of action codes
// to do: make this an enum?
#define PIN_MODE 0x00
#define DIGITAL_WRITE 0x01
#define ANALOG_WRITE 0x02
#define DIGITAL_READ 0x03
#define ANALOG_READ 0x04
#define REPORTING 0x05
#define SET_SAMPLE_INTERVAL 0x06
#define INTERNAL_RGB 0x07
/* NOTE GAP */
// #define SERIAL_BEGIN 0x10
// #define SERIAL_END 0x11
// #define SERIAL_PEEK 0x12
// #define SERIAL_AVAILABLE 0x13
// #define SERIAL_WRITE 0x14
// #define SERIAL_READ 0x15
// #define SERIAL_FLUSH 0x16
/* NOTE GAP */
// #define SPI_BEGIN 0x20
// #define SPI_END 0x21
// #define SPI_SET_BIT_ORDER 0x22
// #define SPI_SET_CLOCK 0x23
// #define SPI_SET_DATA_MODE 0x24
// #define SPI_TRANSFER 0x25
// /* NOTE GAP */
// #define WIRE_BEGIN 0x30
// #define WIRE_REQUEST_FROM 0x31
// #define WIRE_BEGIN_TRANSMISSION 0x32
// #define WIRE_END_TRANSMISSION 0x33
// #define WIRE_WRITE 0x34
// #define WIRE_AVAILABLE 0x35
// #define WIRE_READ 0x36
/* NOTE GAP */
#define SERVO_WRITE 0x41
#define ACTION_RANGE 0x46
uint8_t bytesToExpectByAction[] = {
// digital/analog I/O
2, // PIN_MODE
2, // DIGITAL_WRITE
2, // ANALOG_WRITE
1, // DIGITAL_READ
1, // ANALOG_READ
2, // REPORTING
2, // SET_SAMPLE_INTERVAL
3, // INTERNAL_RGB
// gap from 0x08-0x0f
0, // 0x08
0, // 0x09
0, // 0x0a
0, // 0x0b
0, // 0x0c
0, // 0x0d
0, // 0x0e
0, // 0x0f
// serial I/O
2, // SERIAL_BEGIN
1, // SERIAL_END
1, // SERIAL_PEEK
1, // SERIAL_AVAILABLE
2, // SERIAL_WRITE -- variable length message!
1, // SERIAL_READ
1, // SERIAL_FLUSH
// gap from 0x17-0x1f
0, // 0x17
0, // 0x18
0, // 0x19
0, // 0x1a
0, // 0x1b
0, // 0x1c
0, // 0x1d
0, // 0x1e
0, // 0x1f
// SPI I/O
0, // SPI_BEGIN
0, // SPI_END
1, // SPI_SET_BIT_ORDER
1, // SPI_SET_CLOCK
1, // SPI_SET_DATA_MODE
1, // SPI_TRANSFER
// gap from 0x26-0x2f
0, // 0x26
0, // 0x27
0, // 0x28
0, // 0x29
0, // 0x2a
0, // 0x2b
0, // 0x2c
0, // 0x2d
0, // 0x2e
0, // 0x2f
// wire I/O
1, // WIRE_BEGIN
3, // WIRE_REQUEST_FROM
1, // WIRE_BEGIN_TRANSMISSION
1, // WIRE_END_TRANSMISSION
1, // WIRE_WRITE -- variable length message!
0, // WIRE_AVAILABLE
0, // WIRE_READ
// gap from 0x37-0x3f
0, // 0x37
0, // 0x38
0, // 0x39
0, // 0x3a
0, // 0x3b
0, // 0x3c
0, // 0x3d
0, // 0x3e
0, // 0x3f
0, // 0x40
// servo
2, // SERVO_WRITE
1, // SERVO_DETACH
};
TCPServer server = TCPServer(PORT);
TCPClient client;
bool hasAction = false;
bool isConnected = false;
byte buffer[MAX_DATA_BYTES];
byte cached[4];
byte reporting[20];
byte pinModeFor[20];
byte analogReporting[20];
byte portValues[2];
int reporters = 0;
int bytesRead = 0;
int bytesExpecting = 0;
int action, available;
unsigned long lastms;
unsigned long nowms;
unsigned long sampleInterval = 100;
unsigned long SerialSpeed[] = {
600, 1200, 2400, 4800, 9600, 14400, 19200, 28800, 38400, 57600, 115200
};
/*
PWM/Servo support is CONFIRMED available on:
D0, D1, A0, A1, A4, A5, A6, A7
Allocate 8 servo objects:
*/
Servo servos[8];
/*
The Spark board can only support PWM/Servo on specific pins, so
based on the pin number, determine the servo index for the allocated
servo object.
*/
int ToServoIndex(int pin) {
// D0, D1
if (pin == 0 || pin == 1) return pin;
// A0, A1
if (pin == 10 || pin == 11) return pin - 8;
// A4, A5, A6, A7
if (pin >= 14) return pin - 10;
}
void send(int action, int pinOrPort, int pinOrPortValue) {
uint8_t buf[4];
// See https://github.com/voodootikigod/voodoospark/issues/20
// to understand why the send function splits values
// into two 7-bit bytes before sending.
buf[0] = action;
buf[1] = pinOrPort;
// LSB
buf[2] = pinOrPortValue & 0x7f;
// MSB
buf[3] = pinOrPortValue >> 0x07 & 0x7f;
server.write(buf, 4);
}
void report() {
if (isConnected) {
#if DEBUG
Serial.println("--------------REPORTING");
#endif
int pin;
int pinValue;
int i;
for (int k = 0; k < 2; k++) {
bool shouldSend = false;
// D0-D7
// portValues[0] = 0;
// A0-A7
// portValues[1] = 0;
portValues[k] = 0;
for (i = 0; i < 8; i++) {
pin = (k * 10) + i;
if (reporting[pin] == 1) {
shouldSend = true;
pinValue = digitalRead(pin);
if (pinValue) {
portValues[k] = (portValues[k] | pinValue) << i;
}
}
}
if (shouldSend) {
#if DEBUG
Serial.print("Reporting: ");
Serial.print(k, DEC);
Serial.println(portValues[k], DEC);
#endif
send(REPORTING, k, portValues[k]);
}
}
for (i = 10; i < 18; i++) {
if (analogReporting[i] == 1) {
int adc = analogRead(i);
#if DEBUG
Serial.print("Analog Reporting: ");
Serial.print(i, DEC);
Serial.print(": ");
Serial.println(adc, DEC);
#endif
send(ANALOG_READ, i, adc);
delay(1);
}
}
}
}
void restore() {
#if DEBUG
Serial.println("--------------RESTORING");
#endif
hasAction = false;
isConnected = false;
reporters = 0;
bytesRead = 0;
bytesExpecting = 0;
lastms = 0;
nowms = 0;
sampleInterval = 100;
memset(&buffer[0], 0, MAX_DATA_BYTES);
memset(&cached[0], 0, 4);
memset(&pinModeFor[0], 0, 20);
memset(&reporting[0], 0, 20);
memset(&analogReporting[0], 0, 20);
memset(&portValues[0], 0, 2);
for (int i = 0; i < 8; i++) {
if (servos[i].attached()) {
servos[i].detach();
}
}
// Restore defaults.
for (int i = 0; i < 8; i++) {
pinMode(i, OUTPUT);
pinMode(i + 10, INPUT);
pinModeFor[i] = 1;
pinModeFor[i + 10] = 0;
}
}
void setup() {
server.begin();
#if DEBUG
Serial.begin(115200);
#endif
IPAddress ip = WiFi.localIP();
static char ipAddress[24] = "";
// https://community.particle.io/t/network-localip-to-string-to-get-it-via-spark-variable/2581/5
sprintf(ipAddress, "%d.%d.%d.%d:%d", ip[0], ip[1], ip[2], ip[3], PORT);
Spark.variable("endpoint", ipAddress, STRING);
}
void processInput() {
int pin, mode, val, type, speed, address, stop, len, k, i;
int byteCount = bytesRead;
#if DEBUG
Serial.println("--------------PROCESSING");
#endif
// Only check if buffer[0] is possibly an action
// when there is no action in progress.
if (hasAction == false) {
if (buffer[0] < ACTION_RANGE) {
action = buffer[0];
bytesExpecting = bytesToExpectByAction[action] + 1;
hasAction = true;
#if DEBUG
Serial.print("Bytes Read: ");
Serial.println(bytesRead, DEC);
Serial.print("Bytes Required: ");
Serial.println(bytesExpecting, DEC);
Serial.print("Bytes Remaining: ");
Serial.println(bytesRead - bytesExpecting, DEC);
#endif
}
}
if ((bytesRead - bytesExpecting) < 0) {
hasAction = false;
bytesExpecting = 0;
#if DEBUG
Serial.println("Not Enough Bytes.");
#endif
return;
}
// When the first byte of buffer is an action and
// enough bytes are read, begin processing the action.
if (hasAction && bytesRead >= bytesExpecting) {
// Copy the expected bytes into the cache and shift
// the unused bytes to the beginning of the buffer
for (k = 0; k < byteCount; k++) {
// Cache the bytes that we're expecting for
// this action.
if (k < bytesExpecting) {
cached[k] = buffer[k];
// #if DEBUG
// Serial.print("Cached: ");
// Serial.println(cached[k], DEC);
// #endif
}
// Shift the unused buffer to the front
buffer[k] = buffer[k + bytesExpecting];
}
byteCount -= bytesExpecting;
#if DEBUG
Serial.print("ACTION: ");
Serial.println(action, DEC);
#endif
// Proceed with action processing
switch (action) {
case PIN_MODE: // pinMode
pin = cached[1];
mode = cached[2];
#if DEBUG
Serial.print("PIN: ");
Serial.println(pin);
Serial.print("MODE: ");
Serial.println(mode, HEX);
#endif
if (pinModeFor[pin] != mode) {
if (pinModeFor[pin] == 0x04) {
servos[ToServoIndex(pin)].detach();
}
pinModeFor[pin] = mode;
// The following modes were derived
// from uses in core-firmware.
if (mode == 0x00) {
// INPUT
pinMode(pin, INPUT_PULLDOWN);
}
if (mode == 0x01) {
// OUTPUT
pinMode(pin, OUTPUT);
}
if (mode == 0x02) {
// ANALOG INPUT
pinMode(pin, INPUT);
}
if (mode == 0x03) {
// ANALOG (PWM) OUTPUT
pinMode(pin, OUTPUT);
}
if (mode == 0x04) {
// SERVO
pinMode(pin, OUTPUT);
servos[ToServoIndex(pin)].attach(pin);
}
}
break;
case DIGITAL_WRITE: // digitalWrite
pin = cached[1];
val = cached[2];
#if DEBUG
Serial.print("PIN: ");
Serial.println(pin, DEC);
Serial.print("VALUE: ");
Serial.println(val, HEX);
#endif
digitalWrite(pin, val);
break;
case ANALOG_WRITE: // analogWrite
pin = cached[1];
val = cached[2];
#if DEBUG
Serial.print("PIN: ");
Serial.println(pin, DEC);
Serial.print("VALUE: ");
Serial.println(val, HEX);
#endif
analogWrite(pin, val);
break;
case DIGITAL_READ: // digitalRead
pin = cached[1];
val = digitalRead(pin);
#if DEBUG
Serial.print("PIN: ");
Serial.println(pin, DEC);
Serial.print("VALUE: ");
Serial.println(val, HEX);
#endif
send(0x03, pin, val);
break;
case ANALOG_READ: // analogRead
pin = cached[1];
val = analogRead(pin);
#if DEBUG
Serial.print("PIN: ");
Serial.println(pin, DEC);
Serial.print("VALUE: ");
Serial.println(val, HEX);
#endif
send(0x04, pin, val);
break;
case REPORTING: // Add pin to
pin = cached[1];
val = cached[2];
#if DEBUG
Serial.print("PIN: ");
Serial.println(pin, DEC);
Serial.print("TYPE: ");
Serial.println(val, DEC);
#endif
if (analogReporting[pin] == 0 || reporting[pin] == 0) {
reporters++;
}
if (val == 2) {
analogReporting[pin] = 1;
} else {
reporting[pin] = 1;
}
break;
case SET_SAMPLE_INTERVAL: // set the sampling interval in ms
sampleInterval = cached[1] + (cached[2] << 7);
#if DEBUG
Serial.print("SET_SAMPLE_INTERVAL (2 bytes): ");
Serial.println(sampleInterval, DEC);
#endif
// Lower than ~100ms will likely crash the spark,
// but
if (sampleInterval < 20) {
sampleInterval = 20;
}
break;
// // Serial API
// case SERIAL_BEGIN: // serial.begin
// type = cached[1];
// speed = cached[2];
// if (type == 0) {
// Serial.begin(SerialSpeed[speed]);
// } else {
// Serial1.begin(SerialSpeed[speed]);
// }
// break;
// case SERIAL_END: // serial.end
// type = cached[1];
// if (type == 0) {
// Serial.end();
// } else {
// Serial1.end();
// }
// break;
// case SERIAL_PEEK: // serial.peek
// type = cached[1];
// if (type == 0) {
// val = Serial.peek();
// } else {
// val = Serial1.peek();
// }
// send(0x07, type, val);
// break;
// case SERIAL_AVAILABLE: // serial.available()
// type = cached[1];
// if (type == 0) {
// val = Serial.available();
// } else {
// val = Serial1.available();
// }
// send(0x07, type, val);
// break;
// case SERIAL_WRITE: // serial.write
// type = cached[1];
// len = cached[2];
// for (i = 0; i < len; i++) {
// if (type == 0) {
// Serial.write(client.read());
// } else {
// Serial1.write(client.read());
// }
// }
// break;
// case SERIAL_READ: // serial.read
// type = cached[1];
// if (type == 0) {
// val = Serial.read();
// } else {
// val = Serial1.read();
// }
// send(0x16, type, val);
// break;
// case SERIAL_FLUSH: // serial.flush
// type = cached[1];
// if (type == 0) {
// Serial.flush();
// } else {
// Serial1.flush();
// }
// break;
// SPI API
// case SPI_BEGIN: // SPI.begin
// SPI.begin();
// break;
// case SPI_END: // SPI.end
// SPI.end();
// break;
// case SPI_SET_BIT_ORDER: // SPI.setBitOrder
// type = cached[1];
// SPI.setBitOrder((type ? MSBFIRST : LSBFIRST));
// break;
// case SPI_SET_CLOCK: // SPI.setClockDivider
// val = cached[1];
// if (val == 0) {
// SPI.setClockDivider(SPI_CLOCK_DIV2);
// } else if (val == 1) {
// SPI.setClockDivider(SPI_CLOCK_DIV4);
// } else if (val == 2) {
// SPI.setClockDivider(SPI_CLOCK_DIV8);
// } else if (val == 3) {
// SPI.setClockDivider(SPI_CLOCK_DIV16);
// } else if (val == 4) {
// SPI.setClockDivider(SPI_CLOCK_DIV32);
// } else if (val == 5) {
// SPI.setClockDivider(SPI_CLOCK_DIV64);
// } else if (val == 6) {
// SPI.setClockDivider(SPI_CLOCK_DIV128);
// } else if (val == 7) {
// SPI.setClockDivider(SPI_CLOCK_DIV256);
// }
// break;
// case SPI_SET_DATA_MODE: // SPI.setDataMode
// val = cached[1];
// if (val == 0) {
// SPI.setDataMode(SPI_MODE0);
// } else if (val == 1) {
// SPI.setDataMode(SPI_MODE1);
// } else if (val == 2) {
// SPI.setDataMode(SPI_MODE2);
// } else if (val == 3) {
// SPI.setDataMode(SPI_MODE3);
// }
// break;
// case SPI_TRANSFER: // SPI.transfer
// val = cached[1];
// val = SPI.transfer(val);
// server.write(0x24);
// server.write(val);
// break;
// // Wire API
// case WIRE_BEGIN: // Wire.begin
// address = cached[1];
// if (address == 0) {
// Wire.begin();
// } else {
// Wire.begin(address);
// }
// break;
// case WIRE_REQUEST_FROM: // Wire.requestFrom
// address = cached[1];
// val = cached[2];
// stop = cached[3];
// Wire.requestFrom(address, val, stop);
// break;
// case WIRE_BEGIN_TRANSMISSION: // Wire.beginTransmission
// address = cached[1];
// Wire.beginTransmission(address);
// break;
// case WIRE_END_TRANSMISSION: // Wire.endTransmission
// stop = cached[1];
// val = Wire.endTransmission(stop);
// server.write(0x33); // could be (action)
// server.write(val);
// break;
// case WIRE_WRITE: // Wire.write
// len = cached[1];
// uint8_t wireData[len];
// for (i = 0; i< len; i++) {
// wireData[i] = cached[1];
// }
// val = Wire.write(wireData, len);
// server.write(0x34); // could be (action)
// server.write(val);
// break;
// case WIRE_AVAILABLE: // Wire.available
// val = Wire.available();
// server.write(0x35); // could be (action)
// server.write(val);
// break;
// case WIRE_READ: // Wire.read
// val = Wire.read();
// server.write(0x36); // could be (action)
// server.write(val);
// break;
case SERVO_WRITE:
pin = cached[1];
val = cached[2];
#if DEBUG
Serial.print("PIN: ");
Serial.println(pin);
Serial.print("WRITING TO SERVO: ");
Serial.println(val);
#endif
servos[ToServoIndex(pin)].write(val);
break;
case INTERNAL_RGB:
byte red;
byte green;
byte blue;
red = cached[1];
green = cached[2];
blue = cached[3];
#if DEBUG
Serial.println("WRITING TO INTERNAL RGB LED.");
Serial.print("Red: ");
Serial.println(red);
Serial.print("Green: ");
Serial.println(green);
Serial.print("Blue: ");
Serial.println(blue);
#endif
RGB.control(true);
RGB.color(red, green, blue);
break;
default: // noop
break;
} // <-- This is the end of the switch
memset(&cached[0], 0, 4);
#if DEBUG
Serial.print("Unprocessed Bytes: ");
Serial.println(byteCount, DEC);
#endif
// Reset hasAction flag (no longer needed for this opertion)
// action and byte read expectation flags
hasAction = false;
bytesExpecting = 0;
// If there were leftover bytes available,
// call processInput. This mechanism will
// continue until the buffer is exhausted.
bytesRead = byteCount;
if (byteCount > 2) {
#if DEBUG
Serial.println("Calling processInput ");
#endif
processInput();
} else {
#if DEBUG
Serial.println("RETURN TO LOOP!");
#endif
}
}
}
void loop() {
if (client.connected()) {
if (!isConnected) {
restore();
#if DEBUG
Serial.println("--------------CONNECTED");
#endif
}
isConnected = true;
// Process incoming bytes first
available = client.available();
if (available > 0) {
int received = 0;
#if DEBUG
Serial.println("--------------BUFFERING AVAILABLE BYTES");
Serial.print("Byte Offset: ");
Serial.println(bytesRead, DEC);
#endif
// Move all available bytes into the buffer,
// this avoids building up back pressure in
// the client byte stream.
for (int i = 0; i < available && i < MAX_DATA_BYTES - bytesRead; i++) {
buffer[bytesRead++] = client.read();
received++;
}
#if DEBUG
Serial.print("Bytes Received: ");
Serial.println(received, DEC);
Serial.print("Bytes In Buffer: ");
Serial.println(bytesRead, DEC);
for (int i = 0; i < bytesRead; i++) {
Serial.print("[");
Serial.print(i, DEC);
Serial.print("] ");
Serial.println(buffer[i], DEC);
}
#endif
processInput();
}
// Reporting must be limited to every ~100ms
// Otherwise the spark becomes unreliable and
// exhibits a higher crash frequency.
nowms = millis();
if (nowms - lastms > sampleInterval && reporters > 0) {
// possible just assign the value of nowms?
lastms += sampleInterval;
report();
}
} else {
// Upon disconnection, restore init state.
if (isConnected) {
restore();
}
// If no client is yet connected, check for a new connection
client = server.available();
}
}
Comments