Last active
August 29, 2015 14:02
-
-
Save blindman2k/dacd71ea3620c74a22f7 to your computer and use it in GitHub Desktop.
This New Blank model is designed to allow simple HTTP GET commands to read and write the imp and its pins.
This file contains hidden or 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
/* | |
* These are the currently supported commands (URLs) | |
* | |
* /device/hardware/pinX/configure (DIGITAL_OUT, DIGITAL_IN, ANALOG_OUT, ANALOG_IN, PWM_OUT) | |
* /device/hardware/pinX/read | |
* /device/hardware/pinX/write | |
* | |
* /device/hardware/X (all hardware.* commands like lightlevel, voltage, etc) | |
* | |
* /device/imp/X (all imp.* commands that take no parameters like getssid, rssi, etc) | |
* | |
*/ | |
// hardware.pin.configure constants | |
const DIGITAL_OUT = 1024 | |
const DIGITAL_IN = 256 | |
const ANALOG_OUT = 1793 | |
const ANALOG_IN = 1792 | |
const PWM_OUT = 2048 | |
const PWM_OUT_STEPS = 2049 | |
const PULSE_COUNTER = 2304 | |
const DIGITAL_OUT_OD = 1280 | |
const DIGITAL_OUT_OD_PULLUP = 1536 | |
const DIGITAL_IN_PULLUP = 512 | |
const DIGITAL_IN_PULLDOWN = 768 | |
const DIGITAL_IN_WAKEUP = 2560 | |
const PTPG_OUT_ACTIVE_HIGH = 2816 | |
const PTPG_OUT_ACTIVE_LOW = 3072 | |
// hardware.spi.configure constants | |
const SIMPLEX_TX = 2 | |
const SIMPLEX_RX = 1 | |
const CLOCK_IDLE_HIGH = 4 | |
const CLOCK_IDLE_LOW = 0 | |
const CLOCK_2ND_EDGE = 8 | |
const MSB_FIRST = 0 | |
const LSB_FIRST = 16 | |
// hardware.uart.configure constants | |
const PARITY_NONE = 0 | |
const PARITY_EVEN = 1 | |
const PARITY_ODD = 2 | |
const NO_TX = 1 | |
const NO_RX = 2 | |
const NO_CTSRTS = 4 | |
// hardware.i2c.configure constants | |
const CLOCK_SPEED_10_KHZ = 10000 | |
const CLOCK_SPEED_50_KHZ = 50000 | |
const CLOCK_SPEED_100_KHZ = 100000 | |
const CLOCK_SPEED_400_KHZ = 400000 | |
/******************** Library Classes ********************/ | |
class Rocky { | |
_handlers = null; | |
// Settings: | |
_timeout = 10; | |
_strictRouting = false; | |
_allowUnsecure = false; | |
constructor(settings = {}) { | |
if ("timeout" in settings) _timeout = settings.timeout; | |
if ("allowUnsecure" in settings) _allowUnsecure = settings.allowUnsecure; | |
if ("strictRouting" in settings) _strictRouting = settings.strictRouting; | |
_handlers = { | |
authorize = _defaultAuthorizeHandler.bindenv(this), | |
onUnauthorized = _defaultUnauthorizedHandler.bindenv(this), | |
onTimeout = _defaultTimeoutHandler.bindenv(this), | |
onNotFound = _defaultNotFoundHandler.bindenv(this), | |
onException = _defaultExceptionHandler.bindenv(this), | |
}; | |
http.onrequest(_onrequest.bindenv(this)); | |
} | |
/************************** [ PUBLIC FUNCTIONS ] **************************/ | |
function on(verb, signature, callback) { | |
// Register this signature and verb against the callback | |
verb = verb.toupper(); | |
signature = signature.tolower(); | |
if (!(signature in _handlers)) _handlers[signature] <- {}; | |
local routeHandler = Rocky.Route(callback); | |
_handlers[signature][verb] <- routeHandler; | |
return routeHandler; | |
} | |
function post(signature, callback) { | |
return on("POST", signature, callback); | |
} | |
function get(signature, callback) { | |
return on("GET", signature, callback); | |
} | |
function any(signature, callback) { | |
return on("*", signature, callback); | |
} | |
function put(signature, callback) { | |
return on("PUT", signature, callback); | |
} | |
function authorize(callback) { | |
_handlers.authorize <- callback; | |
return this; | |
} | |
function onUnauthorized(callback) { | |
_handlers.onUnauthorized <- callback; | |
return this; | |
} | |
function onTimeout(callback, timeout = 10) { | |
_handlers.onTimeout <- callback; | |
_timeout = timeout; | |
return this; | |
} | |
function onNotFound(callback) { | |
_handlers.onNotFound <- callback; | |
return this; | |
} | |
function onException(callback) { | |
_handlers.onException <- callback; | |
return this; | |
} | |
// This should come from the context bind not the class | |
function access_control() { | |
// We should probably put this as a default OPTION handler, but for now this will do | |
// It is probably never required tho as this is an API handler not a HTML handler | |
res.header("Access-Control-Allow-Origin", "*") | |
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); | |
} | |
/************************** [ PRIVATE FUNCTIONS ] *************************/ | |
function _onrequest(req, res) { | |
// Setup the context for the callbacks | |
local context = Rocky.Context(req, res); | |
// Check for unsecure reqeusts | |
if (_allowUnsecure == false && "x-forwarded-proto" in req.headers && req.headers["x-forwarded-proto"] != "https") { | |
context.send(405, "HTTP not allowed."); | |
return; | |
} | |
// Parse the request body back into the body | |
try { | |
req.body = _parse_body(req); | |
} catch (e) { | |
server.log("Parse error '" + e + "' when parsing:\r\n" + req.body) | |
context.send(400, e); | |
return; | |
} | |
// Look for a handler for this path | |
local route = _handler_match(req); | |
if (route) { | |
// if we have a handler | |
context.path = route.path; | |
context.matches = route.matches; | |
// parse auth | |
context.auth = _parse_authorization(context); | |
// Create timeout | |
local onTimeout = _handlers.onTimeout; | |
local timeout = _timeout; | |
if (route.handler.hasTimeout()) { | |
onTimeout = route.handler.onTimeout; | |
timeout = route.handler.timeout; | |
} | |
context.setTimeout(_timeout, onTimeout); | |
route.handler.execute(context, _handlers); | |
} else { | |
// if we don't have a handler | |
_handlers.onNotFound(context); | |
} | |
} | |
function _parse_body(req) { | |
if ("content-type" in req.headers && req.headers["content-type"] == "application/json") { | |
if (req.body == "" || req.body == null) return null; | |
return http.jsondecode(req.body); | |
} | |
if ("content-type" in req.headers && req.headers["content-type"] == "application/x-www-form-urlencoded") { | |
return http.urldecode(req.body); | |
} | |
if ("content-type" in req.headers && req.headers["content-type"].slice(0,20) == "multipart/form-data;") { | |
local parts = []; | |
local boundary = req.headers["content-type"].slice(30); | |
local bindex = -1; | |
do { | |
bindex = req.body.find("--" + boundary + "\r\n", bindex+1); | |
if (bindex != null) { | |
// Locate all the parts | |
local hstart = bindex + boundary.len() + 4; | |
local nstart = req.body.find("name=\"", hstart) + 6; | |
local nfinish = req.body.find("\"", nstart); | |
local fnstart = req.body.find("filename=\"", hstart) + 10; | |
local fnfinish = req.body.find("\"", fnstart); | |
local bstart = req.body.find("\r\n\r\n", hstart) + 4; | |
local fstart = req.body.find("\r\n--" + boundary, bstart); | |
// Pull out the parts as strings | |
local headers = req.body.slice(hstart, bstart); | |
local name = null; | |
local filename = null; | |
local type = null; | |
foreach (header in split(headers, ";\n")) { | |
local kv = split(header, ":="); | |
if (kv.len() == 2) { | |
switch (strip(kv[0]).tolower()) { | |
case "name": | |
name = strip(kv[1]).slice(1, -1); | |
break; | |
case "filename": | |
filename = strip(kv[1]).slice(1, -1); | |
break; | |
case "content-type": | |
type = strip(kv[1]); | |
break; | |
} | |
} | |
} | |
local data = req.body.slice(bstart, fstart); | |
local part = { "name": name, "filename": filename, "data": data, "content-type": type }; | |
parts.push(part); | |
} | |
} while (bindex != null); | |
return parts; | |
} | |
// Nothing matched, send back the original body | |
return req.body; | |
} | |
function _parse_authorization(context) { | |
if ("authorization" in context.req.headers) { | |
local auth = split(context.req.headers.authorization, " "); | |
if (auth.len() == 2 && auth[0] == "Basic") { | |
// Note the username and password can't have colons in them | |
local creds = http.base64decode(auth[1]).tostring(); | |
creds = split(creds, ":"); | |
if (creds.len() == 2) { | |
return { authType = "Basic", user = creds[0], pass = creds[1] }; | |
} | |
} else if (auth.len() == 2 && auth[0] == "Bearer") { | |
// The bearer is just the password | |
if (auth[1].len() > 0) { | |
return { authType = "Bearer", user = auth[1], pass = auth[1] }; | |
} | |
} | |
} | |
return { authType = "None", user = "", pass = "" }; | |
} | |
function _extract_parts(routeHandler, path, regexp = null) { | |
local parts = { path = [], matches = [], handler = routeHandler }; | |
// Split the path into parts | |
foreach (part in split(path, "/")) { | |
parts.path.push(part); | |
} | |
// Capture regular expression matches | |
if (regexp != null) { | |
local caps = regexp.capture(path); | |
local matches = []; | |
foreach (cap in caps) { | |
parts.matches.push(path.slice(cap.begin, cap.end)); | |
} | |
} | |
return parts; | |
} | |
function _handler_match(req) { | |
local signature = req.path.tolower(); | |
local verb = req.method.toupper(); | |
// ignore trailing /s if _strictRouting == false | |
if(!_strictRouting) { | |
while (signature.len() > 1 && signature[signature.len()-1] == '/') { | |
signature = signature.slice(0, signature.len()-1); | |
} | |
} | |
if ((signature in _handlers) && (verb in _handlers[signature])) { | |
// We have an exact signature match | |
return _extract_parts(_handlers[signature][verb], signature); | |
} else if ((signature in _handlers) && ("*" in _handlers[signature])) { | |
// We have a partial signature match | |
return _extract_parts(_handlers[signature]["*"], signature); | |
} else { | |
// Let's iterate through all handlers and search for a regular expression match | |
foreach (_signature,_handler in _handlers) { | |
if (typeof _handler == "table") { | |
foreach (_verb,_callback in _handler) { | |
if (_verb == verb || _verb == "*") { | |
try { | |
local ex = regexp(_signature); | |
if (ex.match(signature)) { | |
// We have a regexp handler match | |
return _extract_parts(_callback, signature, ex); | |
} | |
} catch (e) { | |
// Don't care about invalid regexp. | |
} | |
} | |
} | |
} | |
} | |
} | |
return null; | |
} | |
/*************************** [ DEFAULT HANDLERS ] *************************/ | |
function _defaultAuthorizeHandler(context) { | |
return true; | |
} | |
function _defaultUnauthorizedHandler(context) { | |
context.send(401, "Unauthorized"); | |
} | |
function _defaultNotFoundHandler(context) { | |
context.send(404, format("No handler for %s %s", context.req.method, context.req.path)); | |
} | |
function _defaultTimeoutHandler(context) { | |
context.send(500, format("Agent Request timed out after %i seconds.", _timeout)); | |
} | |
function _defaultExceptionHandler(context, ex) { | |
context.send(500, "Agent Error: " + ex); | |
} | |
} | |
class Rocky.Route { | |
handlers = null; | |
timeout = null; | |
_callback = null; | |
constructor(callback) { | |
handlers = {}; | |
timeout = 10; | |
_callback = callback; | |
} | |
/************************** [ PUBLIC FUNCTIONS ] **************************/ | |
function execute(context, defaultHandlers) { | |
try { | |
// setup handlers | |
foreach (handlerName, handler in defaultHandlers) { | |
if (!(handlerName in handlers)) handlers[handlerName] <- handler; | |
} | |
if(handlers.authorize(context)) { | |
_callback(context); | |
} | |
else { | |
handlers.onUnauthorized(context); | |
} | |
} catch(ex) { | |
handlers.onException(context, ex); | |
} | |
} | |
function authorize(callback) { | |
handlers.authorize <- callback; | |
return this; | |
} | |
function onException(callback) { | |
handlers.onException <- callback; | |
return this; | |
} | |
function onUnauthorized(callback) { | |
handlers.onUnauthorized <- callback; | |
return this; | |
} | |
function onTimeout(callback, t = 10) { | |
handlers.onTimeout <- callback; | |
timeout = t; | |
return this; | |
} | |
function hasTimeout() { | |
return ("onTimeout" in handlers); | |
} | |
} | |
class Rocky.Context { | |
req = null; | |
res = null; | |
sent = false; | |
id = null; | |
time = null; | |
auth = null; | |
path = null; | |
matches = null; | |
timer = null; | |
static _contexts = {}; | |
constructor(_req, _res) { | |
req = _req; | |
res = _res; | |
sent = false; | |
time = date(); | |
// Identify and store the context | |
do { | |
id = math.rand(); | |
} while (id in _contexts); | |
_contexts[id] <- this; | |
} | |
/************************** [ PUBLIC FUNCTIONS ] **************************/ | |
function get(id) { | |
if (id in _contexts) { | |
return _contexts[id]; | |
} else { | |
return null; | |
} | |
} | |
function isbrowser() { | |
return (("accept" in req.headers) && (req.headers.accept.find("text/html") != null)); | |
} | |
function getHeader(key, def = null) { | |
key = key.tolower(); | |
if (key in req.headers) return req.headers[key]; | |
else return def; | |
} | |
function setHeader(key, value) { | |
return res.header(key, value); | |
} | |
function send(code, message = null) { | |
// Cancel the timeout | |
if (timer) { | |
imp.cancelwakeup(timer); | |
timer = null; | |
} | |
// Remove the context from the store | |
if (id in _contexts) { | |
delete Rocky.Context._contexts[id]; | |
} | |
// Has this context been closed already? | |
if (sent) { | |
return false; | |
} | |
if (message == null && typeof code == "integer") { | |
// Empty result code | |
res.send(code, ""); | |
} else if (message == null && typeof code == "string") { | |
// No result code, assume 200 | |
res.send(200, code); | |
} else if (message == null && (typeof code == "table" || typeof code == "array")) { | |
// No result code, assume 200 ... and encode a json object | |
res.header("Content-Type", "application/json; charset=utf-8"); | |
res.send(200, http.jsonencode(code)); | |
} else if (typeof code == "integer" && (typeof message == "table" || typeof message == "array")) { | |
// Encode a json object | |
res.header("Content-Type", "application/json; charset=utf-8"); | |
res.send(code, http.jsonencode(message)); | |
} else { | |
// Normal result | |
res.send(code, message); | |
} | |
sent = true; | |
} | |
function setTimeout(timeout, callback) { | |
// Set the timeout timer | |
if (timer) imp.cancelwakeup(timer); | |
timer = imp.wakeup(timeout, function() { | |
if (callback == null) { | |
send(502, "Timeout"); | |
} else { | |
callback(this); | |
} | |
}.bindenv(this)) | |
} | |
} | |
/******************** Application Code ********************/ | |
app <- Rocky(); | |
const RESPONSE_TIMEOUT = 5; | |
request_id <- 0; | |
waitlist <- {}; | |
// ------------------------------------------------------------ | |
app.onException(function(context, exception) { | |
context.setHeader("Content-Type", "application-json"); | |
context.send(200, http.jsonencode({ "exception": exception})); | |
}); | |
// ------------------------------------------------------------ | |
app.on("*", "/device/hardware/[^/]*/.*", function(context) { | |
// Split out the subobject and method | |
local parts = split(context.req.path, "/"); | |
parts.remove(0); // Remove /device | |
parts.remove(0); // Remove /hardware | |
local object = "hardware", subobject = null, method = null; | |
if (parts.len() > 0) { | |
subobject = parts[0]; | |
parts.remove(0); | |
} | |
if (parts.len() > 0) { | |
method = parts[0]; | |
parts.remove(0); | |
} | |
// Check all the parameters | |
if (pinmux.pin.find(subobject) != null) { | |
switch (method) { | |
case "configure": | |
switch (parts[0]) { | |
case "DIGITAL_OUT": | |
if (parts.len() != 1) throw "DIGITAL_OUT requires no parameters" | |
parts[0] = DIGITAL_OUT; | |
break; | |
case "PWM_OUT": | |
if (parts.len() != 3) throw "PWM_OUT requires 2 parameters" | |
parts[0] = PWM_OUT; | |
parts[1] = parts[1].tofloat(); | |
parts[2] = parts[2].tofloat(); | |
break; | |
case "ANALOG_OUT": | |
if (parts.len() != 1) throw "ANALOG_OUT requires no parameters" | |
parts[0] = ANALOG_OUT; | |
break; | |
default: | |
throw "Pin type " + parts[0] + " not supported, yet."; | |
} | |
break; | |
case "write": | |
if (parts.len() != 1) throw "pin.write requires 1 parameter" | |
parts[0] = parts[0].tofloat(); | |
break; | |
case "read": | |
if (parts.len() != 0) throw "pin.read requires 0 parameter" | |
break; | |
default: | |
throw "Method not supported, yet."; | |
} | |
} else { | |
throw "Device " + subobject + " not supported, yet."; | |
} | |
// Send the request to the device | |
send(context, object, subobject, method, parts); | |
}); | |
// ------------------------------------------------------------ | |
app.on("*", "/device/hardware/[^/]*", function(context) { | |
local parts = split(context.req.path, "/"); | |
parts.remove(0); // Remove /device | |
parts.remove(0); // Remove /hardware | |
local object = "hardware", subobject = null, method = null; | |
if (parts.len() > 0) { | |
method = parts[0]; | |
} else { | |
throw "Invalid request" | |
} | |
// Send the request to the device | |
send(context, object, subobject, method); | |
}); | |
// ------------------------------------------------------------ | |
app.on("*", "/device/imp/[^/]*", function(context) { | |
local parts = split(context.req.path, "/"); | |
parts.remove(0); // Remove /device | |
parts.remove(0); // Remove /hardware | |
local object = "imp", subobject = null, method = null; | |
if (parts.len() > 0) { | |
method = parts[0]; | |
} else { | |
throw "Invalid request" | |
} | |
// Send the request to the device | |
send(context, object, subobject, method); | |
}); | |
// ------------------------------------------------------------ | |
device.on("response", function(response) { | |
if ("id" in response && response.id in waitlist) { | |
local id = response.id; | |
delete response.id; | |
waitlist[id].context.setHeader("Content-Type", "application-json"); | |
waitlist[id].context.send(200, http.jsonencode(response)); | |
imp.cancelwakeup(waitlist[id].timer); | |
delete waitlist[id]; | |
} | |
}) | |
// ------------------------------------------------------------ | |
function send(context, object, subobject, method, params = []) { | |
local data = { id=++request_id, object=object, subobject=subobject, method=method, params=params }; | |
device.send("request", data); | |
data.context <- context; | |
data.timer <- imp.wakeup(RESPONSE_TIMEOUT, function() { | |
server.log("Timeout waiting for request " + data.id); | |
context.setHeader("Content-Type", "application-json"); | |
context.send(502, http.jsonencode({ "exception": "Timeout waiting for device"})); | |
delete waitlist[data.id]; | |
}) | |
waitlist[data.id] <- data; | |
} | |
// ------------------------------------------------------------ | |
pinmux <- { "pin": [], "uart": [], "i2c": [], "spi": [] }; | |
pinmux = "pinmux" in server.load() ? server.load().pinmux : pinmux; | |
device.on("boot", function(boot) { | |
server.log("Welcome to your imp-" + boot.model); | |
pinmux = boot.pinmux; | |
server.save(boot); | |
}) | |
This file contains hidden or 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
// ============================================================================ | |
// This model is designed to be a starting point for a "Blank" firmware to | |
// replace the current default firmware that does absolutely nothing. | |
// | |
class Blank | |
{ | |
model = null; | |
pinmux = null; | |
// ......................................................................... | |
constructor() { | |
detectPinMux(); | |
setupCallbacks(); | |
} | |
// ......................................................................... | |
function detectPinMux() { | |
pinmux = { "pin": [], "uart": [], "i2c": [], "spi": [] }; | |
switch (true) { | |
case "pinW" in hardware: | |
model = "003"; | |
pinmux.pin = [ | |
"pinA", "pinB", "pinC", "pinD", "pinE", "pinF", "pinG", "pinH", | |
"pinJ", "pinK", "pinL", "pinM", "pinN", "pinP", "pinQ", "pinR", | |
"pinS", "pinT", "pinU", "pinV", "pinW", "pinX", "pinY" | |
]; | |
pinmux.uart = [ | |
"uartFG", "uartQRPW", "uartUVGD", "uartWJ", "uartDM" | |
]; | |
pinmux.i2c = [ | |
"i2cFG", "i2cAB" | |
]; | |
pinmux.spi = [ | |
"spiEBCA", "spiLGDK" | |
]; | |
break; | |
case "pinD" in hardware: | |
model = "002"; | |
pinmux.pin = [ | |
"pin1", "pin2", "pin5", "pin6", "pin7", "pin8", "pin9", | |
"pinA", "pinB", "pinC", "pinD", "pinE" | |
]; | |
pinmux.uart = [ | |
"uart1289", "uart57", "uart12", "uart6E", "uartB" | |
]; | |
pinmux.i2c = [ | |
"i2c89", "i2c12" | |
]; | |
pinmux.spi = [ | |
"spi257", "spi189" | |
]; | |
break; | |
default: | |
model = "001"; | |
pinmux.pin = [ | |
"pin1", "pin2", "pin5", "pin7", "pin8", "pin9" | |
]; | |
pinmux.uart = [ | |
"uart1289", "uart57", "uart12" | |
]; | |
pinmux.i2c = [ | |
"i2c89", "i2c12" | |
]; | |
pinmux.spi = [ | |
"spi257", "spi189" | |
]; | |
break; | |
} | |
} | |
// ......................................................................... | |
function setupCallbacks() { | |
agent.on("request", function(data) { | |
local response = { id=data.id }; | |
try { | |
switch (data.object) { | |
case "imp": | |
response.result <- toImp(data.object, data.subobject, data.method, data.params) | |
break; | |
case "hardware": | |
response.result <- toHardware(data.object, data.subobject, data.method, data.params) | |
break; | |
default: | |
throw "Unsupported object: " + data.object; | |
} | |
} catch (e) { | |
response.exception <- e; | |
} | |
agent.send("response", response) | |
}.bindenv(this)) | |
} | |
// ......................................................................... | |
function toImp(object, subobject, method, data) { | |
if (method in imp) { | |
local params = [imp]; | |
foreach (param in data) params.push(param); | |
return imp[method].acall(params); | |
} else { | |
throw "Method not found: imp." + command; | |
} | |
} | |
// ......................................................................... | |
function toHardware(object, subobject, method, data) { | |
if (subobject == null && method in hardware) { | |
local params = [hardware]; | |
foreach (param in data) params.push(param); | |
return hardware[method].acall(params); | |
} else if (subobject in hardware && method in hardware[subobject]) { | |
local params = [hardware[subobject]]; | |
foreach (param in data) params.push(param); | |
return hardware[subobject][method].acall(params); | |
} else if (subobject == null) { | |
throw "Method not found: hardware." + method; | |
} else { | |
throw "Method not found: hardware." + subobject + "." + method; | |
} | |
} | |
} | |
// ============================================================================ | |
blank <- Blank(); | |
agent.send("boot", { model = blank.model, pinmux = blank.pinmux }); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment