|
//------------------------------------------------------------------------------------------------------------------------------ |
|
/* STK500 constants list, from AVRDUDE */ |
|
const MESSAGE_START = 0x1B; |
|
const TOKEN = 0x0E; |
|
const STK_OK = 0x10; |
|
const STK_FAILED = 0x11; // Not used |
|
const STK_UNKNOWN = 0x12; // Not used |
|
const STK_NODEVICE = 0x13; // Not used |
|
const STK_INSYNC = 0x14; // ' ' |
|
const STK_NOSYNC = 0x15; // Not used |
|
const ADC_CHANNEL_ERROR = 0x16; // Not used |
|
const ADC_MEASURE_OK = 0x17; // Not used |
|
const PWM_CHANNEL_ERROR = 0x18; // Not used |
|
const PWM_ADJUST_OK = 0x19; // Not used |
|
const CRC_EOP = 0x20; // 'SPACE' |
|
const STK_GET_SYNC = 0x30; // '0' |
|
const STK_GET_SIGN_ON = 0x31; // '1' |
|
const STK_SET_PARAMETER = 0x40; // '@' |
|
const STK_GET_PARAMETER = 0x41; // 'A' |
|
const STK_SET_DEVICE = 0x42; // 'B' |
|
const STK_SET_DEVICE_EXT = 0x45; // 'E' |
|
const STK_ENTER_PROGMODE = 0x50; // 'P' |
|
const STK_LEAVE_PROGMODE = 0x51; // 'Q' |
|
const STK_CHIP_ERASE = 0x52; // 'R' |
|
const STK_CHECK_AUTOINC = 0x53; // 'S' |
|
const STK_LOAD_ADDRESS = 0x55; // 'U' |
|
const STK_UNIVERSAL = 0x56; // 'V' |
|
const STK_PROG_FLASH = 0x60; // '`' |
|
const STK_PROG_DATA = 0x61; // 'a' |
|
const STK_PROG_FUSE = 0x62; // 'b' |
|
const STK_PROG_LOCK = 0x63; // 'c' |
|
const STK_PROG_PAGE = 0x64; // 'd' |
|
const STK_PROG_FUSE_EXT = 0x65; // 'e' |
|
const STK_READ_FLASH = 0x70; // 'p' |
|
const STK_READ_DATA = 0x71; // 'q' |
|
const STK_READ_FUSE = 0x72; // 'r' |
|
const STK_READ_LOCK = 0x73; // 's' |
|
const STK_READ_PAGE = 0x74; // 't' |
|
const STK_READ_SIGN = 0x75; // 'u' |
|
const STK_READ_OSCCAL = 0x76; // 'v' |
|
const STK_READ_FUSE_EXT = 0x77; // 'w' |
|
const STK_READ_OSCCAL_EXT = 0x78; // 'x' |
|
|
|
|
|
//------------------------------------------------------------------------------------------------------------------------------ |
|
function HEXDUMP(buf, len = null) { |
|
if (buf == null) return "null"; |
|
if (len == null) { |
|
len = (typeof buf == "blob") ? buf.tell() : buf.len(); |
|
} |
|
|
|
local dbg = ""; |
|
for (local i = 0; i < len; i++) { |
|
local ch = buf[i]; |
|
dbg += format("0x%02X ", ch); |
|
} |
|
|
|
return format("%s (%d bytes)", dbg, len) |
|
} |
|
|
|
|
|
//------------------------------------------------------------------------------------------------------------------------------ |
|
function SERIAL_READ(len = 100, timeout = 300) { |
|
|
|
local rxbuf = blob(len); |
|
local write = rxbuf.writen.bindenv(rxbuf); |
|
local read = SERIAL.read.bindenv(SERIAL); |
|
local hw = hardware; |
|
local ms = hw.millis.bindenv(hw); |
|
local started = ms(); |
|
|
|
local charsRead = 0; |
|
(LINK ? LINK : ACTIVITY).write(0); //Turn LED on |
|
do { |
|
local ch = read(); |
|
if (ch != -1) { |
|
write(ch, 'b') |
|
charsRead++; |
|
if (charsRead == len) break; |
|
} |
|
} while (ms() - started < timeout); |
|
(LINK ? LINK : ACTIVITY).write(1); //Turn LED off |
|
|
|
// Clean up any extra bytes |
|
while (SERIAL.read() != -1); |
|
|
|
if (rxbuf.tell() == 0) { |
|
return null; |
|
} else { |
|
return rxbuf; |
|
} |
|
} |
|
|
|
|
|
//------------------------------------------------------------------------------------------------------------------------------ |
|
function execute(command = null, param = null, response_length = 100, response_timeout = 300) { |
|
|
|
local send_buffer = null; |
|
if (command == null) { |
|
send_buffer = format("%c", CRC_EOP); |
|
} else if (param == null) { |
|
send_buffer = format("%c%c", command, CRC_EOP); |
|
} else if (typeof param == "array") { |
|
send_buffer = format("%c", command); |
|
foreach (datum in param) { |
|
switch (typeof datum) { |
|
case "string": |
|
case "blob": |
|
case "array": |
|
case "table": |
|
foreach (adat in datum) { |
|
send_buffer += format("%c", adat); |
|
} |
|
break; |
|
default: |
|
send_buffer += format("%c", datum); |
|
} |
|
} |
|
send_buffer += format("%c", CRC_EOP); |
|
} else { |
|
send_buffer = format("%c%c%c", command, param, CRC_EOP); |
|
} |
|
|
|
// server.log("Sending: " + HEXDUMP(send_buffer)); |
|
SERIAL.write(send_buffer); |
|
|
|
local resp_buffer = SERIAL_READ(response_length+2, response_timeout); |
|
// server.log("Received: " + HEXDUMP(resp_buffer)); |
|
|
|
assert(resp_buffer != null); |
|
assert(resp_buffer.tell() >= 2); |
|
assert(resp_buffer[0] == STK_INSYNC); |
|
assert(resp_buffer[resp_buffer.tell()-1] == STK_OK); |
|
|
|
local tell = resp_buffer.tell(); |
|
if (tell == 2) return blob(0); |
|
resp_buffer.seek(1); |
|
return resp_buffer.readblob(tell-2); |
|
} |
|
|
|
|
|
//------------------------------------------------------------------------------------------------------------------------------ |
|
function check_duino() { |
|
// Clear the read buffer |
|
SERIAL_READ(); |
|
|
|
// Check everything we can check to ensure we are speaking to the correct boot loader |
|
local major = execute(STK_GET_PARAMETER, 0x81, 1); |
|
local minor = execute(STK_GET_PARAMETER, 0x82, 1); |
|
local invalid = execute(STK_GET_PARAMETER, 0x83, 1); |
|
local signature = execute(STK_READ_SIGN); |
|
assert(major.len() == 1 && major[0] == 0x04); |
|
assert(minor.len() == 1 && minor[0] == 0x04); |
|
assert(invalid.len() == 1 && invalid[0] == 0x03); |
|
assert(signature.len() == 3 && signature[0] == 0x1E && signature[1] == 0x95 && signature[2] == 0x0F); |
|
} |
|
|
|
|
|
//------------------------------------------------------------------------------------------------------------------------------ |
|
function program_duino(address16, data) { |
|
|
|
local addr8_hi = (address16 >> 8) & 0xFF; |
|
local addr8_lo = address16 & 0xFF; |
|
local data_len = data.len(); |
|
|
|
execute(STK_LOAD_ADDRESS, [addr8_lo, addr8_hi], 0); |
|
execute(STK_PROG_PAGE, [0x00, data_len, 0x46, data], 0) |
|
local data_check = execute(STK_READ_PAGE, [0x00, data_len, 0x46], data_len) |
|
|
|
assert(data_check.len() == data_len); |
|
for (local i = 0; i < data_len; i++) { |
|
assert(data_check[i] == data[i]); |
|
} |
|
|
|
} |
|
|
|
|
|
//------------------------------------------------------------------------------------------------------------------------------ |
|
function bounce() { |
|
|
|
// Bounce the reset pin |
|
server.log("Bouncing the Arduino reset pin"); |
|
imp.sleep(0.5); |
|
ACTIVITY.write(0); |
|
RESET.write(1); |
|
imp.sleep(0.2); |
|
RESET.write(0); |
|
imp.sleep(0.3); |
|
check_duino(); |
|
ACTIVITY.write(1); |
|
} |
|
|
|
|
|
//------------------------------------------------------------------------------------------------------------------------------ |
|
function burn(pline) { |
|
if ("first" in pline) { |
|
server.log("Starting to burn"); |
|
SERIAL.configure(115200, 8, PARITY_NONE, 1, NO_CTSRTS); |
|
bounce(); |
|
} else if ("last" in pline) { |
|
server.log("Done!") |
|
agent.send("done", true); |
|
SERIAL.configure(115200, 8, PARITY_NONE, 1, NO_CTSRTS, scan_serial); |
|
} else { |
|
program_duino(pline.addr, pline.data); |
|
} |
|
} |
|
|
|
|
|
class NeoPixels { |
|
|
|
// when instantiated, the neopixel class will fill this array with blobs to |
|
// represent the waveforms to send the numbers 0 to 255. This allows the blobs to be |
|
// copied in directly, instead of being built for each pixel - which makes the class faster. |
|
bits = null; |
|
|
|
// Like bits, this blob holds the waveform to send the color [0,0,0], to clear pixels faster |
|
clearblob = null; |
|
|
|
// private variables passed into the constructor |
|
spi = null; // imp SPI interface (pre-configured) |
|
width = null; // the maximum X dimension |
|
height = null; // the maximum Y dimension |
|
frameSize = null; // number of pixels per frame (height x width) |
|
frame = null; // a blob to hold the current frame buffer |
|
canvas = null; // 2d array holding the next buffer to be drawn |
|
snapshot = null; // holds a copy of the canvas for quick drawing of a background/template |
|
cache = null; // holds the hsl2rgb cache |
|
|
|
// _spi - A configured spi (MSB_FIRST, 7.5MHz) |
|
// _width - X pixels wide |
|
// _height - Y pixels high |
|
constructor(_spi, _width, _height = 1) { |
|
// This class uses SPI to emulate the newpixels' one-wire protocol. |
|
// This requires one byte per bit to send data at 7.5 MHz via SPI. |
|
// These consts define the "waveform" to represent a zero or one |
|
const SPICLK = 7500; // kHz |
|
const ZERO = 0xC0; |
|
const ONE = 0xF8; |
|
const BYTESPERPIXEL = 24; |
|
|
|
spi = _spi; |
|
width = _width; |
|
height= _height; |
|
|
|
spi.configure(MSB_FIRST, SPICLK); |
|
|
|
frameSize = width * height; |
|
frame = blob(frameSize*BYTESPERPIXEL + 1); |
|
|
|
clearblob = blob(BYTESPERPIXEL); |
|
cache = {}; |
|
|
|
// prepare the bits array and the clearblob blob |
|
initialize(); |
|
|
|
// Blank the screen |
|
clear(); |
|
write(); |
|
|
|
} |
|
|
|
// fill the array of representative 1-wire waveforms. |
|
// done by the constructor at instantiation. |
|
function initialize() { |
|
// fill the bits array first |
|
bits = array(256); |
|
for (local i = 0; i < 256; i++) { |
|
local valblob = blob(BYTESPERPIXEL / 3); |
|
valblob.writen((i & 0x80) ? ONE:ZERO,'b'); |
|
valblob.writen((i & 0x40) ? ONE:ZERO,'b'); |
|
valblob.writen((i & 0x20) ? ONE:ZERO,'b'); |
|
valblob.writen((i & 0x10) ? ONE:ZERO,'b'); |
|
valblob.writen((i & 0x08) ? ONE:ZERO,'b'); |
|
valblob.writen((i & 0x04) ? ONE:ZERO,'b'); |
|
valblob.writen((i & 0x02) ? ONE:ZERO,'b'); |
|
valblob.writen((i & 0x01) ? ONE:ZERO,'b'); |
|
bits[i] = valblob; |
|
} |
|
|
|
// now fill the clearblob |
|
for(local j = 0; j < BYTESPERPIXEL; j++) { |
|
clearblob.writen(ZERO, 'b'); |
|
} |
|
|
|
// finally, prepare the canvas |
|
canvas = array(width); |
|
for (local x = 0; x < width; x++) { |
|
canvas[x] = array(height); |
|
for (local y = 0; y < height; y++) { |
|
canvas[x][y] = null; |
|
} |
|
} |
|
} |
|
|
|
// draw a single pixel onto the canvas |
|
function drawPixel(x, y, color) { // or pdp |
|
if (x >= 0 && x < width && y >= 0 && y < height) { |
|
canvas[x][y] = color; |
|
} |
|
} |
|
|
|
// draw a box or line on the canvas |
|
function drawBox(x1, y1, x2, y2, color) { // or pdb |
|
// Swap the coordinates to its always drawing uphill |
|
if (x2 < x1) { local x3 = x1; x1 = x2; x2 = x3 } |
|
if (y2 < y1) { local y3 = y1; y1 = y2; y2 = y3 } |
|
|
|
for (local x = x1; x <= x2; x++) { |
|
for (local y = y1; y <= y2; y++) { |
|
if (x >= 0 && x < width && y >= 0 && y < height) { |
|
canvas[x][y] = color; |
|
} |
|
} |
|
} |
|
} |
|
|
|
// wipes the canvas to a single colour (or black) |
|
function clear(color = null) { // or pcf |
|
for (local x = 0; x < width; x++) { |
|
for (local y = 0; y < height; y++) { |
|
canvas[x][y] = color; |
|
} |
|
} |
|
} |
|
|
|
// sends the canvas to the neopixels |
|
function write() { |
|
|
|
local color, x, y, yy, alt = true; |
|
local fwb = frame.writeblob.bindenv(frame); |
|
|
|
frame.seek(0); |
|
for (x = 0; x < width; x++) { |
|
alt = !alt; |
|
for (y = 0; y < height; y++) { |
|
// Alternate direction of every alternate row |
|
yy = alt ? (height - y - 1) : y; |
|
color = canvas[x][yy]; |
|
if (color) { |
|
fwb(bits[color[1]]); |
|
fwb(bits[color[0]]); |
|
fwb(bits[color[2]]); |
|
} else { |
|
fwb(clearblob); |
|
} |
|
} |
|
} |
|
frame.writen(0, 'b'); // Drive MOSI low |
|
|
|
// All done. Send. |
|
spi.write(frame); |
|
} |
|
|
|
// stores the current canvas for future fast use |
|
function storeSnapshot() { |
|
snapshot = array(width); |
|
for (local x = 0; x < width; x++) { |
|
snapshot[x] = clone canvas[x]; |
|
} |
|
} |
|
|
|
// restores a snapshot as the primary canvas |
|
function restoreSnapshot() { |
|
if (snapshot) { |
|
for (local x = 0; x < width; x++) { |
|
canvas[x] = clone snapshot[x]; |
|
} |
|
return true; |
|
} else { |
|
return false; |
|
} |
|
} |
|
|
|
/** |
|
* Converts an HSL color value to RGB. Conversion formula |
|
* adapted from http://en.wikipedia.org/wiki/HSL_color_space. |
|
* Assumes h, s, and l are contained in the set [0, 255] and |
|
* returns r, g, and b in the set [0, 255]. |
|
* |
|
* @param Number h The hue |
|
* @param Number s The saturation |
|
* @param Number l The lightness |
|
* @return Array The RGB representation |
|
*/ |
|
function hsl2rgb(h, s, l, _cache=false) { |
|
|
|
local cachekey = format("%02X%02X%02X", h, s, l); |
|
if (cachekey in cache) return cache[cachekey]; |
|
|
|
local hue2rgb = function(p, q, t) { |
|
if (t < 0.0) t += 1.0; |
|
if (t > 1.0) t -= 1.0; |
|
if (t < 0.16666666) return p + (q - p) * 6.0 * t; |
|
if (t < 0.5) return q; |
|
if (t < 0.66666666) return p + (q - p) * (0.66666666 - t) * 6.0; |
|
return p; |
|
} |
|
|
|
|
|
local r = 0.0; |
|
local g = 0.0; |
|
local b = 0.0; |
|
|
|
h /= 255.0; |
|
s /= 255.0; |
|
l /= 255.0; |
|
|
|
if (s == 0) { |
|
r = g = b = l; // achromatic |
|
} else if (l == 0) { |
|
r = g = b = l; // off |
|
} else { |
|
local q = l < 0.5 ? l * (1.0 + s) : l + s - l * s; |
|
local p = 2.0 * l - q; |
|
r = hue2rgb(p, q, h + 0.33333333); |
|
g = hue2rgb(p, q, h); |
|
b = hue2rgb(p, q, h - 0.33333333); |
|
} |
|
|
|
local rgb = [math.floor(r*255.0), math.floor(g*255.0), math.floor(b*255.0)]; |
|
if (_cache) cache[cachekey] <- rgb; |
|
|
|
return rgb; |
|
} |
|
} |
|
|
|
//------------------------------------------------------------------------------------------------------------------------------ |
|
// Flicker the activity LED |
|
activity_timer <- null; |
|
function indicate_activity() { |
|
ACTIVITY.write(0) |
|
if (activity_timer) imp.cancelwakeup(activity_timer); |
|
activity_timer = imp.wakeup(0.1, function() { |
|
activity_timer = null; |
|
ACTIVITY.write(1) |
|
}) |
|
} |
|
|
|
|
|
//------------------------------------------------------------------------------------------------------------------------------ |
|
spectrum <- array(8); spectrum_len <- -1; |
|
log <- ""; log_len <- -1; |
|
function scan_serial() { |
|
local ch = null; |
|
local str = ""; |
|
do { |
|
ch = SERIAL.read(); |
|
|
|
if (ch == -1) { |
|
// Nothing here |
|
} else if (ch == 0xFC) { |
|
// Got a log start byte |
|
log = ""; |
|
log_len = 0; |
|
} else if (ch == 0xFD) { |
|
indicate_activity(); |
|
server.log("[Android] " + log) |
|
log = ""; |
|
log_len = -1; |
|
} else if (ch == 0xFE) { |
|
// Got a start byte |
|
spectrum_len = 0; |
|
} else if (ch == 0xFF) { |
|
// Got an end byte |
|
if (spectrum_len == 8) { |
|
// Got a full spectrum |
|
local s = hardware.micros(); |
|
draw(spectrum); |
|
local d = hardware.micros(); |
|
pixels.write(); |
|
local w = hardware.micros(); |
|
if (frame % 100 == 0) server.log(format("Draw = %d micros, Write = %d micros", d-s, w-d)) |
|
|
|
if (spectrum[2] > 3) { |
|
// indicate_activity(); |
|
} |
|
} |
|
spectrum_len = -1; |
|
} else if (spectrum_len >= 0 && spectrum_len < 8) { |
|
// Got a single character |
|
spectrum[spectrum_len++] = ch; |
|
} else if (log_len >= 0 && log_len < 1024) { |
|
log += format("%c", ch); |
|
} else if (ch == 0x00) { |
|
// Skip unexpected nulls |
|
} else { |
|
str += format("%c", ch); |
|
} |
|
|
|
} while (ch != -1); |
|
|
|
if (str.len() > 0) { |
|
server.log("??? " + HEXDUMP(str)) |
|
} |
|
} |
|
|
|
|
|
//------------------------------------------------------------------------------------------------------------------------------ |
|
peak <- [0, 0, 0, 0, 0, 0, 0, 0]; |
|
frame <- 0; |
|
function draw(spectrum) { |
|
// server.log(HEXDUMP(spectrum)) |
|
frame++; |
|
|
|
// Fill background w/colors, then idle parts of columns will erase |
|
if (!pixels.restoreSnapshot()) { |
|
local b = -12, d = 12; |
|
for (local y = 0; y < 8; y++) { |
|
pdb(0, 7-y, 7, 7-y, pixels.hsl2rgb(b += d, 0xFF, 0x20, true)); |
|
} |
|
pixels.storeSnapshot(); |
|
} |
|
|
|
for (local x = 0; x < 8; x++) { |
|
|
|
// Draw black over the unused parts of the bar |
|
local c = spectrum[x]; |
|
|
|
if (c > peak[x]) peak[x] = c; // Keep dot on top |
|
if (peak[x] <= 0) { // Empty column? |
|
pdb(x, 0, x, 7, null); |
|
continue; |
|
} else if (c < 8) { // Partial column? |
|
pdb(x, c, x, 7, null); |
|
} |
|
|
|
// Draw the peak dot |
|
local y = peak[x]-1; |
|
pdp(x, y, [0, 0, 0x80]); |
|
|
|
// Slowly bring the peak dot down |
|
if (peak[x] > 0 && frame % 4 == 0) peak[x]--; |
|
} |
|
|
|
} |
|
|
|
|
|
//------------------------------------------------------------------------------------------------------------------------------ |
|
server.setsendtimeoutpolicy(RETURN_ON_ERROR, WAIT_TIL_SENT, 30); |
|
server.log("Device started, impee_id " + hardware.getimpeeid() + " and mac = " + imp.getmacaddress() ); |
|
|
|
// Pin 5 and 7 - Serial to Arduino |
|
SERIAL <- hardware.uart57; |
|
|
|
// Pin 8 - Display - Neopixels |
|
spi <- hardware.spi189; |
|
pixels <- NeoPixels(spi, 8, 8); |
|
|
|
// Pin 1 - Drive high for reset |
|
RESET <- hardware.pin1; |
|
RESET.configure(DIGITAL_OUT); |
|
RESET.write(0); |
|
|
|
// Pin 2 - Drive low for red LED |
|
ACTIVITY <- hardware.pin2; |
|
ACTIVITY.configure(DIGITAL_OUT); |
|
ACTIVITY.write(1); |
|
|
|
// Pin 8 is the orange LED, SPI MOSI to the NeoPixels or UART TX to the Hypnocube |
|
LINK <- hardware.pin8; |
|
LINK <- null; |
|
if (LINK) { |
|
LINK.configure(DIGITAL_OUT); |
|
LINK.write(1); |
|
} |
|
|
|
pdp <- pixels.drawPixel.bindenv(pixels); |
|
pdb <- pixels.drawBox.bindenv(pixels); |
|
|
|
agent.on("burn", burn); |
|
agent.send("ready", true); |
|
|
|
// Start receiving audio data |
|
imp.wakeup(1, function() { |
|
SERIAL.configure(115200, 8, PARITY_NONE, 1, NO_CTSRTS, scan_serial); |
|
}) |
|
|