Created
May 22, 2018 22:24
-
-
Save betzrhodes/9615596761b5df5006fd851a4ef620fc to your computer and use it in GitHub Desktop.
emma code
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function update() { | |
server.log("in update") | |
} | |
// When the device reconnects, send it power and a holding pattern. | |
device.on("status", function (status) { | |
device.send("setPower", 100); | |
device.send("setSpeed", 4); | |
if (status == "boot") { | |
device.send("pushQueue", {"message": "26.35"}); | |
} | |
update(); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// the slave address for this device is set in hardware | |
const ALS_ADDR = 0x52; | |
// ======================================================================================== | |
// Class: Emma | |
// Description: Controls the Emma board including the 8-character LED panel plus the Lux meter. | |
// Notes: The important public methods are pushQueue(), getLux() and | |
class emma { | |
// Emma Default Firmware - print 8-character string to small digit display | |
// Pin 1 = load | |
// Pin 2 = oe_l | |
// Pin 5 = data | |
// Pin 7 = srclk | |
// Pin 8 = scl | |
// Pin 9 = sda | |
// Byte Ordering: | |
// [digit 0 (left)][1][2][3][4][5][6][7 (right)][decimal point word] | |
// Pin layout | |
// ---- ---- -0020- -0004- | |
// | \ | / | |0080| |0010| |0002| | |
// | \ | / | \0040\ /0008/ | |
// | --- --- | -8000- -0001- | |
// | / | \ | /1000/ \0200\ | |
// | / | \ | |4000| |0800| |0100| | |
// ---- ---- -2000- -0400- | |
// ======================================================================================== | |
// holds the last lux reading | |
lux = 0.0; | |
// Holds the screen buffer | |
drawBuffer = null; | |
// number of bytes needed to write full display | |
bufferSize = 18; | |
// Current animation frame | |
aniQueue = []; | |
// variables containing the current state and associated data | |
state_data = { "animation": "draw", | |
"message": " .", | |
"frame": 0, | |
"frames": -1, | |
"cycles": -1, | |
"nextPop": 0, | |
"fadeIn": false, | |
"fadeOut": false, | |
"power": null | |
}; | |
// Timers | |
updateDisplayTimer = 0.2; | |
updateLuxTimer = 0.5; | |
// ======================================================================================== | |
/* | |
* | |
*/ | |
constructor(max_power = 100.0) { | |
// Serial Interface to AS1110 Driver ICs | |
hardware.configure(SPI_257); | |
hardware.spi.configure(SIMPLEX_TX | LSB_FIRST | CLOCK_IDLE_LOW, CLOCK_SPEED_400_KHZ/1000); | |
// Configure oe_l and load as GPIO | |
// pin 2 is pulled up inside the AS1110 driver, nominally disable | |
// we will use pin 2 (oe_l) for brightness control at 1kHz | |
hardware.pin2.configure(PWM_OUT, 0.0001, 0.0); | |
hardware.pin1.configure(DIGITAL_OUT); | |
// pin 2 is pulled up inside the AS1110 driver, nominally disable | |
hardware.pin1.write(0); | |
// I2C Interface to TSL2561FN | |
hardware.configure(I2C_89); | |
hardware.i2c89.configure(CLOCK_SPEED_400_KHZ); | |
// Initialise the draw buffer | |
drawBuffer = emmaBuffer(bufferSize); | |
drawBuffer.setMaxPower(max_power); | |
drawBuffer.clear(); | |
// Start the timers | |
imp.wakeup(0, updateLux.bindenv(this)); | |
imp.wakeup(0, updateDisplay.bindenv(this)); | |
// Handle incoming agent requests | |
agent.on("pushQueue", pushQueue.bindenv(this)); | |
agent.on("clearQueue", clearQueue.bindenv(this)); | |
agent.on("setSpeed", setSpeed.bindenv(this)); | |
agent.on("setPower", drawBuffer.setMaxPower.bindenv(drawBuffer)); | |
agent.send("status", "boot"); | |
imp.wakeup(60, request_update.bindenv(this)); | |
} | |
// Keep it coming | |
function request_update() { | |
imp.wakeup(60, request_update.bindenv(this)); | |
agent.send("status", "ready"); | |
} | |
/* | |
* | |
*/ | |
function pushQueue(top) { | |
// Set the beginning time | |
if ("delay" in top) { | |
top.when <- time() + top.delay; | |
delete top.delay; | |
} else { | |
top.when <- time(); | |
} | |
if (!("animation" in top)) top.animation <- "draw"; | |
if (!("message" in top)) top.message <- ""; | |
if (!("frames" in top)) top.frames <- -1; | |
if (!("cycles" in top)) top.cycles <- -1; | |
if (!("duration" in top)) top.duration <- -1; | |
if (!("repeat" in top)) top.repeat <- 1; | |
if (!("interrupt" in top)) top.interrupt <- false; | |
if (!("fadeOut" in top)) top.fadeOut <- false; | |
if (!("fadeIn" in top)) top.fadeIn <- false; | |
if (!("power" in top)) top.power <- null; | |
if (!("speed" in top)) top.speed <- null; | |
if (top.duration == -1 && top.frames == -1) { | |
top.frames = drawBuffer.cleanCount(top.message)+8; | |
} | |
// Push this onto the queue | |
for (local i = 0; i < top.repeat; i++) { | |
if (top.interrupt) aniQueue.insert(0, top); | |
else aniQueue.push(top); | |
} | |
} | |
/* | |
* | |
*/ | |
function clearQueue(dummy) { | |
aniQueue.clear(); | |
} | |
/* | |
* | |
*/ | |
function popQueue() { | |
// server.log("Pop? queue (" + aniQueue.len() + ") > 0 && time (" + time() + ") >= nextPop (" + state_data.nextPop + ") && frames (" + state_data.frames + ") <= 0"); | |
if (aniQueue.len() > 0 && time() >= state_data.nextPop && state_data.frames <= 0) { | |
// Peek at the first item in the queue | |
local top = aniQueue[0]; | |
if (time() >= top.when) { | |
if (top.fadeOut) { | |
// server.log("Fading out: [" + state_data.message + "] to [" + top.message + "] with power starting at [" + drawBuffer.power + "]"); | |
// Mark the fadeOut as done and then start the fade but DO NOT move onto the next queue item | |
aniQueue[0].fadeOut = false; | |
state_data.fadeOut = true; | |
} else { | |
// There is a change of power or speed | |
if (top.power != null) { | |
drawBuffer.setMaxPower(top.power); | |
} | |
if (top.speed != null) { | |
setSpeed(top.speed); | |
} | |
if (top.fadeIn) { | |
// server.log("Fading in: [" + state_data.message + "] to [" + top.message + "] with power starting at [" + drawBuffer.power + "]"); | |
// Mark the fadeIn as done and then start the fade, but move on. | |
state_data.fadeIn = true; | |
drawBuffer.setPower(1.0); | |
} else { | |
drawBuffer.setPower(0.0); | |
} | |
// Pop the queue | |
aniQueue.remove(0); | |
// Push this onto the state machine for rendering | |
state_data.animation = top.animation; | |
state_data.message = top.message; | |
state_data.nextPop = top.duration + time(); | |
state_data.frames = top.frames; | |
state_data.cycles = top.cycles; | |
state_data.frame = 0; | |
} | |
return top; | |
} | |
} | |
return false; | |
} | |
/* | |
* | |
*/ | |
function updateDisplay() { | |
// Process fadeIn or fadeOut requests | |
if (state_data.fadeOut) { | |
if (!drawBuffer.incPower(drawBuffer.getFadeIncrement())) { | |
state_data.fadeOut = false; | |
} | |
} else if (state_data.fadeIn) { | |
if (!drawBuffer.incPower(-drawBuffer.getFadeIncrement())) { | |
state_data.fadeIn = false; | |
} | |
} | |
// Populate the display | |
drawBuffer.animate(state_data.animation, state_data.frame, state_data.message); | |
// Finally, update the screen. | |
if (drawBuffer.changed) { | |
redrawBuffer(); | |
} | |
// If we are done fading in or out then pop the next item off the queue | |
if (!state_data.fadeIn && !state_data.fadeOut) { | |
popQueue(); | |
} | |
// Update some counters | |
if (state_data.frames > 0) state_data.frames--; | |
state_data.frame++; | |
// Set the next update | |
imp.wakeup(updateDisplayTimer, updateDisplay.bindenv(this)); | |
} | |
/* | |
* | |
*/ | |
function setSpeed(speedFPS) { | |
if (speedFPS > 0) { | |
updateDisplayTimer = 1 / speedFPS.tofloat(); | |
} | |
} | |
/* | |
* | |
*/ | |
function redrawBuffer() { | |
// server.show(drawBuffer.toString()); | |
// Write the buffer | |
hardware.spi257.write(drawBuffer.getBuffer()); | |
// Toggle the load button | |
hardware.pin1.write(1); | |
imp.sleep(0.001); | |
hardware.pin1.write(0); | |
// Set the power level | |
hardware.pin2.write(drawBuffer.getPower()); | |
} | |
/* | |
* | |
*/ | |
function updateLux() { | |
local reg0 = hardware.i2c89.read(ALS_ADDR, "\xAC", 2); | |
local reg1 = hardware.i2c89.read(ALS_ADDR, "\xAE", 2); | |
if (reg0 != null && reg1 != null) { | |
local channel0 = ((reg0[1] & 0xFF) << 8) | (reg0[0] & 0xFF); | |
local channel1 = ((reg1[1] & 0xFF) << 8) | (reg1[0] & 0xFF); | |
local ratio = channel1/channel0.tofloat(); | |
if (ratio <= 0.52) { | |
lux = (0.0315 * channel0 - 0.0593 * channel0 * math.pow(ratio,1.4)); | |
} else if (0.52 < ratio && ratio <= 0.65) { | |
lux = (0.0229 * channel0 - 0.0291 * channel1); | |
} else if (0.65 < ratio && ratio <= 0.8) { | |
lux = (0.0157 * channel0 - 0.0180 * channel1); | |
} else if (0.80 < ratio && ratio <= 1.30) { | |
lux = (0.00338 * channel0 - 0.00260 * channel1); | |
} else { | |
lux = 0; | |
} | |
} | |
// start the next ALS conversion | |
hardware.i2c89.write(ALS_ADDR, "\x80\x03"); | |
imp.wakeup(updateLuxTimer, updateLux.bindenv(this)); | |
} | |
/* | |
* | |
*/ | |
function getLux() { | |
return lux; | |
} | |
} | |
// ======================================================================================== | |
class emmaBuffer { | |
buffer = null; | |
pos = 0; | |
len = 0; | |
power = 0.0; | |
max_power = 0.0; | |
changed = false; | |
lastSentence = ""; | |
lastOffset = 0; | |
lastPower = 0.0; | |
fade_frames = 10; | |
// hex translations (LSB) of characters (upper case / alphanum only) | |
hexTable = { | |
['0']=0x75AE, ['1']=0x0102, ['2']=0xE427, ['3']=0x252D, ['4']=0x8183, ['5']=0xA5A5, | |
['6']=0xE5A5, ['7']=0x082C, ['8']=0xE5A7, ['9']=0xA5A7, ['A']=0xC1A7, ['B']=0x2D37, | |
['C']=0x64A4, ['D']=0x2D36, ['E']=0xE4A5, ['F']=0xC0A5, ['G']=0x65A5, ['H']=0xC183, | |
['I']=0x2C34, ['J']=0x6506, ['K']=0xC288, ['L']=0x6480, ['M']=0x41CA, ['N']=0x43C2, | |
['O']=0x65A6, ['P']=0xC0A7, ['Q']=0x67A6, ['R']=0xC2A7, ['S']=0xA5A5, ['T']=0x0834, | |
['U']=0x6582, ['V']=0x5088, ['W']=0x6D82, ['X']=0x1248, ['Y']=0x0848, ['Z']=0x342C, | |
[' ']=0x0000, ['\'']=0x0008, ['$']=0xADB5, ['%']=0x9DB9, ['*']=0x9A59, ['-']=0x8001, | |
['+']=0x8811, ['<']=0x0208, ['>']=0x1040, ['[']=0x60A0, [']']=0x0506, ['(']=0x60A0, | |
[')']=0x0506, ['\\']=0x0240, ['/']=0x1008, ['^']=0x1200, ['_']=0x2400, [',']=0x1000, | |
['=']=0xA401, ['?']=0x022C, ['!']=0x0240, ['#']=0xFFFF, ['@']=0x64B7 | |
} | |
/* | |
* | |
*/ | |
constructor(size) { | |
len = size; | |
buffer = blob(len); | |
clear(); | |
} | |
/* | |
* | |
*/ | |
function getBuffer() { | |
changed = false; | |
buffer.seek(0, 'b'); | |
return buffer.readblob(len); | |
} | |
/* | |
* | |
*/ | |
function toString() { | |
local sentence = format("%d%% =>", 100.0 - 100.0 * power); | |
buffer.seek(0, 'b'); | |
for (local i = 0; i < buffer.len(); i+=2) { | |
local wrd = buffer.readn('w'); | |
sentence = sentence + " " + format("0x%04X", wrd); | |
} | |
buffer.seek(pos, 'b'); | |
return sentence; | |
} | |
/* | |
* | |
*/ | |
function start() { | |
pos = 2; | |
buffer.seek(2, 'b'); | |
} | |
/* | |
* | |
*/ | |
function clear() { | |
buffer.seek(0, 'b'); | |
for (local i = 0; i < len; i++) { | |
buffer.writen(0x00, 'b'); | |
} | |
start(); | |
changed = true; | |
} | |
/* | |
* | |
*/ | |
function setMaxPower(newmax) { | |
max_power = (1.0 - newmax/100.0); | |
if (power < max_power) { | |
power = max_power; | |
changed = true; | |
} | |
return max_power; | |
} | |
/* | |
* | |
*/ | |
function getPower() { | |
return power; | |
} | |
/* | |
* | |
*/ | |
function getFadeIncrement() { | |
return (1.0-max_power)/fade_frames; | |
} | |
/* | |
* | |
*/ | |
function setPower(newpower) { | |
if (newpower <= max_power) newpower = max_power; | |
if (newpower >= 1.0) newpower = 1.0; | |
if (newpower != lastPower) changed = true; | |
power = lastPower = newpower; | |
return power; | |
} | |
/* | |
* | |
*/ | |
function incPower(powerinc) { | |
local newpower = power + powerinc; | |
if (newpower <= max_power) newpower = max_power; | |
if (newpower >= 1.0) newpower = 1.0; | |
if (newpower != lastPower) changed = true; | |
power = lastPower = newpower; | |
return power > max_power && power < 1.0; | |
} | |
/* | |
* | |
*/ | |
function write(sentence, offset = 0) { | |
// Check if we have a new sentence | |
if (sentence == lastSentence && offset == lastOffset) return; | |
lastSentence = sentence; | |
lastOffset = offset; | |
// Start at the beginning of a clean buffer (also marks it as changed) | |
clear(); | |
// Write the character to the buffer | |
local i = 0; | |
for (; i < sentence.len(); i++) { | |
if (i < offset) continue; | |
local ch = sentence[i]; | |
if (ch == '.') { | |
// Take the dot out and store it in the first word as a bitfield | |
offset++; | |
buffer.seek(0, 'b'); | |
local periodWord = buffer.readn('w'); | |
periodWord = periodWord | (0x01 << 6+pos/2); | |
buffer.seek(0, 'b'); | |
buffer.writen(periodWord, 'w'); | |
buffer.seek(pos, 'b'); | |
} else if (pos < len) { | |
// Write the encoded character out | |
local ech = encodeCharacter(ch); | |
buffer.writen(ech, 'w'); | |
pos += 2; | |
} else { | |
// We have no more space in the buffer | |
break; | |
} | |
} | |
} | |
/* | |
* | |
*/ | |
function animate(style, frame, param=null) { | |
// Select the animation | |
local chars = []; | |
local fullFrame = null; | |
// server.log("Animate: style=" + style + ", frame=" + frame + ", param=" + param); | |
switch (style) { | |
case "draw": | |
fullFrame = clean(param); | |
break; | |
case "walk-left": | |
fullFrame = " " + clean(strip(param)) + " "; | |
local fullFrameLen = cleanCount(fullFrame); | |
fullFrame = cleanSlice(fullFrame, frame%(fullFrameLen-8)); | |
break; | |
case "walk-right": | |
fullFrame = " " + strip(param); | |
local fullFrameLen = cleanCount(fullFrame); | |
fullFrame = cleanSlice(fullFrame, fullFrameLen-frame%fullFrameLen); | |
break; | |
case "cycle-in": | |
chars = [0x8000, 0x0040, 0x0010, 0x0008, 0x0001, 0x0200, 0x0800, 0x1000]; | |
break; | |
case "cycle-out": | |
chars = [0x0004, 0x0002, 0x0100, 0x0400, 0x2000, 0x4000, 0x0080, 0x0020]; | |
break; | |
case "dashes": | |
if (frame % 2 == 0) fullFrame = "_.-_.-_.-_.-_.-_.-"; | |
else fullFrame = "-_.-_.-_.-_.-_.-_."; | |
break; | |
case "ribbon": | |
if (frame % 2 == 0) fullFrame = "/[]<>[]\\"; | |
else fullFrame = "\\][><][/"; | |
break; | |
case "time": | |
local d = date(time() - 7*60*60); | |
fullFrame = format(" %02d.%02d.%02d ", d.hour, d.min, d.sec); | |
break; | |
case "date": | |
local d = date(time() - 7*60*60); | |
switch (d.wday) { | |
case 0: d.sday <- "SUN"; break; | |
case 1: d.sday <- "MON"; break; | |
case 2: d.sday <- "TUE"; break; | |
case 3: d.sday <- "WED"; break; | |
case 4: d.sday <- "THU"; break; | |
case 5: d.sday <- "FRI"; break; | |
case 6: d.sday <- "SAT"; break; | |
} | |
switch (d.month) { | |
case 0: d.smonth <- "JAN"; break; | |
case 1: d.smonth <- "FEB"; break; | |
case 2: d.smonth <- "MAR"; break; | |
case 3: d.smonth <- "APR"; break; | |
case 4: d.smonth <- "MAY"; break; | |
case 5: d.smonth <- "JUN"; break; | |
case 6: d.smonth <- "JUL"; break; | |
case 7: d.smonth <- "AUG"; break; | |
case 8: d.smonth <- "SEP"; break; | |
case 9: d.smonth <- "OCT"; break; | |
case 10: d.smonth <- "NOV"; break; | |
case 11: d.smonth <- "DEC"; break; | |
} | |
fullFrame = "" + d.sday + " " + d.day + " " + d.smonth + " " + d.year; | |
return animate("walk-left", frame, fullFrame); | |
} | |
// If we have a full frame, use it | |
if (fullFrame != null) { | |
write(fullFrame); | |
// Otherwise animate the characters in sync | |
} else if (chars.len() > 0) { | |
changed = true; | |
buffer.seek(0, 'b'); | |
buffer.writen(0x0000, 'w'); | |
for (local i = 2; i < len; i+=2) { | |
buffer.writen(chars[frame % chars.len()], 'w'); | |
} | |
} | |
} | |
/* | |
* | |
*/ | |
function encodeCharacter(inputChar) { | |
if (inputChar in hexTable) { | |
return hexTable[inputChar]; | |
} else { | |
return hexTable[' ']; | |
} | |
} | |
/* | |
* | |
*/ | |
function clean(sentence) { | |
sentence = sentence.toupper(); | |
do { | |
// Replace all double-dots with dot-space-dot. | |
local l = sentence.find(".."); | |
if (l == null) { | |
break; | |
} else { | |
sentence = sentence.slice(0, l+1) + " " + sentence.slice(l+1); | |
} | |
} while (true); | |
do { | |
// Replace all colons with dot. | |
local l = sentence.find(":"); | |
if (l == null) { | |
break; | |
} else { | |
sentence = sentence.slice(0, l) + "." + sentence.slice(l+1); | |
} | |
} while (true); | |
// Add a dot after every question mark and exclamation point | |
for (local i = sentence.len()-1; i >= 0; i--) { | |
if (sentence[i] == '?' || sentence[i] == '!') { | |
sentence = sentence.slice(0, i+1) + "." + sentence.slice(i+1); | |
} | |
} | |
// Special case of . at the start. Needs a space padding. | |
if (sentence.len() > 0 && sentence[0] == '.') { | |
sentence = " " + sentence; | |
} | |
return sentence; | |
} | |
/* | |
* | |
*/ | |
function cleanCount(sentence) { | |
local count = 0; | |
for (local i = 0; i < sentence.len(); i++) { | |
local ch = sentence[i]; | |
if (ch != '.') count++; | |
} | |
return count; | |
} | |
/* | |
* | |
*/ | |
function cleanSlice(sentence, offset) { | |
local sliceFrom = 0; | |
for (; sliceFrom < offset && sliceFrom < sentence.len(); sliceFrom++) { | |
if (sentence[sliceFrom] == '.') offset++; | |
} | |
if (sliceFrom >= sentence.len()) return ""; | |
if (sentence[sliceFrom] == '.') sliceFrom++; | |
return sentence.slice(sliceFrom); | |
} | |
} | |
// ======================================================================================== | |
// TODO list: | |
// - Callbacks | |
// - Trigger at the end of each animation state | |
// - Trigger to request the next display buffer (5x a second) | |
// - Transitions | |
// - Change the fadeIn/fadeOut options to transition="fade" | |
// - Make some other transition animations. | |
// ======================================================================================== | |
server.log("Device booted with impeeid " + hardware.getimpeeid() + " and mac " + imp.getmacaddress()); | |
e <- emma(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment