Last active
April 22, 2022 10:00
-
-
Save ElectricImpSampleCode/360de8bc774cb4a933a041086c108fb0 to your computer and use it in GitHub Desktop.
impOS 42 UDP Local Networking Example
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
// ********** Imports ********** | |
#require "Rocky.class.nut:2.0.2" | |
// ********** Web UI ********** | |
const HTML_STRING = @"<!DOCTYPE html> | |
<html lang='en'> | |
<head> | |
<meta charset='UTF-8'> | |
<meta name='viewport' content='width=device-width, initial-scale=1.0'> | |
<title>UDP Demo</title> | |
<link rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css'> | |
<link href='https://fonts.googleapis.com/css?family=Rubik|Questrial' rel='stylesheet'> | |
<style> | |
.uicontent {border: 2px solid #33cc00;} | |
.container {padding: 20px;} | |
.center {margin-left: auto; margin-right: auto; margin-bottom: auto; margin-top: auto;} | |
.tabborder {width: 20%%;} | |
.tabcontent {width: 60%%;} | |
.btn-success {background-color: #33cc00;} | |
body {background-color: #333333;} | |
p {color: white; font-family: Questrial, sans-serif; font-size: 1em;} | |
h4 {color: white; font-family: Questrial, sans-serif;} | |
td {color: white; font-family: Questrial, sans-serif;} | |
hr {border-color: #33cc00;} | |
img {max-width: 100%%; height: auto;} | |
@media only screen and (max-width: 640px) { | |
.container {padding: 5px} | |
.uicontent {border: 0px} | |
.col-2 {max-width: 0%%; flex: 0 0 0%%} | |
.col-8 {max-width: 100%%; flex: 0 0 100%%} | |
} | |
</style> | |
</head> | |
<body> | |
<div class='container'> | |
<div class='uicontent'> | |
<div class='row'> | |
<div class='col-2'> </div> | |
<div class='col-8'> | |
<p> </p> | |
<h4 align='center'>UDP Demo<br /> </h4> | |
<div> | |
<h4 align='center'>Nodes</h4> | |
<p class='node-list text-center'><span></span></p> | |
<div align='center'> | |
<button type='submit' class='btn btn-success' id='updatebutton' style='width:200px;font-family:Rubik;'>Update List</button> | |
</div> | |
</div> | |
<p> </p> | |
</div> | |
<div class='col-2'> </div> | |
</div> | |
</div> | |
</div> | |
<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js'></script> | |
<script> | |
var agenturl = '%s'; | |
// Get initial readings | |
getState(updateReadout); | |
// Buttons | |
$('#updatebutton').click(updateState); | |
// Functions | |
function updateReadout(j) { | |
var d = JSON.parse(j); | |
showNodes(d); | |
} | |
function showNodes(nodes) { | |
if (nodes.length == 0) { | |
// No alarms so just show a simple message | |
$('.node-list span').text('No Nodes found'); | |
} else { | |
// Build an HTML table to show the alarms | |
var h = '<table width=""100%%"" class=""table table-striped table-sm"">'; | |
h = h + '<tr><th>Node</th><th>IP</th><th>Device ID</th><th>Reading</th><th>Timestamp</th></tr>'; | |
for (var i = 0 ; i < nodes.length ; i++) { | |
let node = nodes[i]; | |
if (node.value == -99.0) { | |
node.value = ' '; | |
} else { | |
node.value = node.value.toFixed(1).toString() + '°C'; | |
} | |
h = h + '<tr><td width=""10%%"" align=""center"">' + (i + 1).toString() + '</td><td width=""20%%"" align=""center"">' + node.address + '</td><td width=""20%%"" align=""center"">' + node.id + '</td><td width=""20%%"" align=""center"">' + node.value + '</td><td width=""30%%"" align=""center"">' + node.timestamp + '</td></tr>'; | |
} | |
h = h + '</table>'; | |
$('.node-list span').html(h); | |
} | |
} | |
function updateState() { | |
$('.node-list span').text('Updating...'); | |
getState(updateReadout); | |
} | |
function getState(c) { | |
// Request the current data | |
$.ajax({ | |
url: agenturl + '/nodes', | |
type: 'GET', | |
cache: false, | |
success: function(r) { | |
if (c) c(r); | |
}, | |
error: function(xhr, sta, err) { | |
if (err) $('.clock-status span').text(err); | |
} | |
}); | |
} | |
</script> | |
</body> | |
</html>"; | |
// ********** Globals ********** | |
local nodes; | |
local api; | |
local count = 0; | |
// ********** Function Definitions ********** | |
function debugAPI(context, next) { | |
// Display a UI API activity report | |
server.log("API received a request at " + time() + ": " + context.req.method.toupper() + " @ " + context.req.path.tolower()); | |
if (context.req.rawbody.len() > 0) server.log("Request body: " + context.req.rawbody.tolower()); | |
// Invoke the next middleware | |
next(); | |
} | |
function sortNodes(a, b) { | |
local p = split(a.address, "."); | |
local x = p[3].tointeger(); | |
p = split(b.address, "."); | |
local y = p[3].tointeger(); | |
if (x < y) return -1; | |
if (x > y) return 1; | |
return 0; | |
} | |
// ********** Device message handlers ********** | |
device.on("gateway.routed.reading", function(data) { | |
count++; | |
server.log("Data received from node " + data.node.address + " at " + data.timestamp + " (reading " + count + ")"); | |
server.log("Recorded temperature: " + format("%.2f", data.reading.tofloat()) + " Celsius"); | |
}); | |
device.on("gateway.routed.forecast", function(data) { | |
count++; | |
server.log("Data received from node " + data.node.address + " at " + data.timestamp + " (reading " + count + ")"); | |
server.log("Forecast: " + data.forecast); | |
}); | |
device.on("gateway.set.nodes", function(data) { | |
// Update the local nodes list | |
nodes = data; | |
}); | |
// ********** Runtime Start ********** | |
nodes = []; | |
api = Rocky(); | |
api.use(debugAPI); | |
// Set up UI access security: HTTPS only | |
api.authorize(function(context) { | |
// Mandate HTTPS connections | |
if (context.getHeader("x-forwarded-proto") != "https") return false; | |
return true; | |
}); | |
api.onUnauthorized(function(context) { | |
// Incorrect level of access security | |
context.send(401, "Insecure access forbidden"); | |
}); | |
// GET to root: just return the UI HTML | |
api.get("/", function(context) { | |
context.send(200, format(HTML_STRING, http.agenturl())); | |
}); | |
// GET to /dimmer: return the dimmer status | |
api.get("/nodes", function(context) { | |
nodes.sort(sortNodes); | |
context.send(200, http.jsonencode(nodes)); | |
}); |
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
// ********** Imports ********** | |
#require "JSONEncoder.class.nut:2.0.0" | |
#require "JSONParser.class.nut:1.0.1" | |
// ********** Early setup code ********** | |
// Keep the hub running even if the Internet connection is lost | |
server.setsendtimeoutpolicy(RETURN_ON_ERROR, WAIT_TIL_SENT, 10); | |
// ********** Constants ********** | |
// Message source indicators | |
enum UDP_MESSAGE_SOURCE { | |
HUB = "IMPH", | |
NODE = "IMPN" | |
} | |
// Message type indicators | |
enum UDP_MESSAGE_TYPE { | |
RESET = "RT", | |
GENERAL = "GL" | |
} | |
// UDP constants | |
const UDP_LONGEST_IMPLEMENTED_MESSAGE_LENGTH = 141; // 10 bytes header + 99 bytes data + 32 bytes footer | |
const UDP_LOW_MEMORY_THRESHOLD = 20000; | |
const UDP_PORT = 20001; | |
// Node state constants | |
enum NODE_STATE { | |
NO_DATA_RECEIVED = 1, | |
DATA_RECEIVED = 2 | |
} | |
// ********** Globals ********** | |
local interface; | |
local udpsocket; | |
local nodes; | |
local lastKey; | |
local signingKey; | |
local keys; | |
local keyIndex; | |
local broadcastAddress; | |
local hubAddress; | |
local readingTimer; | |
local ping = true; | |
// ********** Function Definitions ********** | |
function wifiRXCallback(fromAddress, fromPort, toAddress, toPort, data) { | |
// This is the data-received callback for all incoming UDP datagrams | |
// Log the incomoimg receipt and then decode the datagram | |
if (toAddress != hubAddress && toAddress != broadcastAddress) { | |
server.log("RX from: " + formatAddress(fromAddress) + ":" + fromPort + " (Unicast to other Node)"); | |
return; | |
} else { | |
server.log("RX from: " + formatAddress(fromAddress) + ":" + fromPort + " " + (toAddress == hubAddress ? "(Unicast to this Hub)" : "(Broadcast packet)")); | |
} | |
local response = decodeMessage(data, UDP_MESSAGE_SOURCE.NODE); | |
if ("command" in response) { | |
server.log(JSONEncoder.encode(response)); | |
if (response.command == "imp.identify") { | |
// The hub has received a identification message from a node | |
local node; | |
if (nodes == null) nodes = []; | |
if (nodes.len() > 0) { | |
// See if we have the node recorded already | |
local got = false; | |
foreach (node in nodes) { | |
if (node.id == response.id) { | |
got = true; | |
break; | |
} | |
} | |
if (!got) { | |
// If the node is new, record its address, etc. | |
node = makeNewNode(fromAddress, response.seqnum, response.id); | |
nodes.append(node); | |
} else { | |
// The node is not new, so just update its details | |
node = getNodeFromID(response.id); | |
if (node.address != fromAddress) node.address = fromAddress; | |
node.seqnum = response.seqnum; | |
node.enrolled = false; | |
} | |
} else { | |
// The node is new, so record its address, etc. | |
node = makeNewNode(fromAddress, response.seqnum, response.id); | |
nodes.append(node); | |
} | |
// Finally, respond to the identification message with an ACK | |
local payload = JSONEncoder.encode({"command":"imp.identify.ack"}); | |
if (node != null) send(node, payload); | |
} else if (response.command == "imp.request.enroll") { | |
// The hub has received an enroll request from a node | |
local node = getNodeFromID(response.id); | |
if (node != null) { | |
// Node has responded with a new sequence number, so apply it | |
server.log("New NODE SN: " + response.seqnum); | |
node.seqnum = response.seqnum; | |
node.enrolled = true; | |
} | |
// Update the agent's list of current nodes | |
agent.send("gateway.set.nodes", nodes); | |
} else if (response.command == "imp.reading.ack") { | |
// The hub has received a data-bearing message from a node | |
local node = getNodeFromID(response.id); | |
if (node != null) { | |
// First check the sequencing | |
local diff = response.seqnum - node.seqnum; | |
if (diff > 0 && diff < 101) { | |
node.seqnum = response.seqnum; | |
node.state = NODE_STATE.DATA_RECEIVED; | |
node.value = response.data.tofloat(); | |
node.timestamp = time(); | |
server.log("Node SN: " + node.seqnum); | |
// Send the reading to the agent | |
local data = { "node" : node, | |
"reading": response.data, | |
"timestamp": time() }; | |
agent.send("gateway.routed.reading", data); | |
agent.send("gateway.set.nodes", nodes); | |
} else { | |
server.error("HUB Bad sequence number - delta: " + diff); | |
} | |
// Just check IP address hasn't changed (and update if it has) | |
if (node.address != fromAddress) node.address = fromAddress; | |
} | |
} else if (response.command == "imp.forecast.ack") { | |
// The hub has received a forecast-bearing message from a node | |
local node = getNodeFromID(response.id); | |
if (node != null) { | |
// First check the sequencing | |
local diff = response.seqnum - node.seqnum; | |
if (diff > 0 && diff < 101) { | |
node.seqnum = response.seqnum; | |
node.timestamp = time(); | |
node.state = NODE_STATE.DATA_RECEIVED; | |
// Send the reading to the agent | |
local data = { "node" : node, | |
"forecast": response.forecast, | |
"timestamp": time() }; | |
agent.send("gateway.routed.forecast", data); | |
agent.send("gateway.set.nodes", nodes); | |
} else { | |
server.error("HUB Bad sequence number - delta: " + diff); | |
} | |
// Just check IP address hasn't changed (and update if it has) | |
if (node.address != fromAddress) node.address = fromAddress; | |
} | |
} | |
} else { | |
// There was an error decoding the message | |
if ("err" in response) server.error(response.err); | |
} | |
} | |
function makeNewNode(fromAddress, seqNumber, msgID) { | |
// Common code for node creation | |
local newNode = {"address":fromAddress, | |
"seqnum":seqNumber, | |
"id":msgID, | |
"enrolled":false, | |
"state":NODE_STATE.NO_DATA_RECEIVED, | |
"value":-99.0, | |
"timestamp":time()}; | |
return newNode; | |
} | |
function getNodeFromAddress(addr) { | |
// Return the node with the specified IP address, or null | |
foreach (node in nodes) { | |
if (node.address == addr) return node; | |
} | |
return null; | |
} | |
function getNodeFromID(id) { | |
// Return the node with the specified device ID, or null | |
foreach (node in nodes) { | |
if (node.id == id) return node; | |
} | |
return null; | |
} | |
function decodeMessage(data, expectedMessageSource) { | |
// Determine whether the message is genuine or not. if it is not, just fail | |
// This is legitimate with UDP comms, which has no ACKS to grind... | |
// Is the message well sized, and have we space for it? | |
if (data.len() > UDP_LONGEST_IMPLEMENTED_MESSAGE_LENGTH || imp.getmemoryfree() < UDP_LOW_MEMORY_THRESHOLD) return {"err":"Bad message length"}; | |
// Check the header start marker is from the hub | |
data.seek(0); | |
local messageSource = data.readstring(4); | |
if (messageSource != expectedMessageSource) return {"err":"Bad message source: " + messageSource}; | |
// Get the sequence number | |
local messageSeqNumber = swap4(data.readn('i')); | |
// Check the code | |
local messageType = data.readstring(2); | |
if (messageType != UDP_MESSAGE_TYPE.GENERAL && packetType != UDP_MESSAGE_TYPE.RESET) return {"err":"Bad message type: " + messageType}; | |
// Check the crytographic signature (final 32 bytes of data) | |
data.seek(-32, 'e'); | |
local messageSignature = data.readblob(32); | |
data.seek(0); | |
local signedMessage = data.readblob(data.len() - 32); | |
local count = 0; | |
local done = false; | |
// Iterate through the available keys to see if the message | |
// was signed by one of them | |
do { | |
lastKey = signingKey; | |
signingKey = keys[keyIndex]; | |
local expectedSignature = crypto.hmacsha256(signedMessage, signingKey); | |
if (expectedSignature != null && crypto.equals(expectedSignature, messageSignature)) { | |
// Key worked | |
done = true; | |
} else { | |
// Key failed; try the next one | |
keyIndex++; | |
if (keyIndex > 3) keyIndex = 0; | |
count++; | |
// Tried all the available keys? Bail | |
if (count > keys.len() - 1) return {"err":"Bad key"}; | |
} | |
} while (!done); | |
// Extract and return the message's payload | |
data.seek(10); | |
local messageData = data.readblob(data.len() - data.tell() - 32); | |
local resp = JSONParser.parse(messageData.tostring()); | |
resp.seqnum <- messageSeqNumber; | |
resp.type <- messageType; | |
return resp; | |
} | |
function send(node, payload, messageType = UDP_MESSAGE_TYPE.GENERAL) { | |
// Send the string payload to the specified address via UDP - after signing the data | |
// Increase the sequence number | |
node.seqnum++; | |
node.state == NODE_STATE.NO_DATA_RECEIVED | |
local message = encodeMessage(payload, lastKey, UDP_MESSAGE_SOURCE.HUB, messageType, node.seqnum); | |
udpsocket.send(node.address, UDP_PORT, message); | |
} | |
function encodeMessage(data, key, messageSource, messageType, messageSeqNumber) { | |
// Construct a signed, suitable coded message for sending | |
local message = blob(42 + data.len()) // 10 bytes header + body length + 32 bytes footer | |
// Write the message header: | |
// Bytes 1-4: Four-character message type indicator | |
// 5-8: 32-bit message sequence number | |
// 9-10: Two-character packet type indicator | |
message.seek(0); | |
message.writestring(messageSource); | |
message.writen(swap4(messageSeqNumber), 'i'); | |
message.writestring(messageType); | |
// Write the message body (assumes an input string) | |
if (data.len() > 0) message.writestring(data); | |
// Write the message footer: | |
// 32-byte 256-bit HMACSHA as message signature | |
message.seek(0); | |
local signature = crypto.hmacsha256(message.readblob(message.len() - 32), key); | |
message.seek(message.len() - 32); | |
message.writeblob(signature); | |
return message; | |
} | |
function interfaceHandler(state) { | |
// Called whenever the local networking interface state changes | |
server.log("HUB State: " + state); | |
if (state == imp.net.CONNECTED && udpsocket == null) { | |
// We're connected, so initiate UDP | |
server.log("Setting up UDP"); | |
udpsocket = interface.openudp(wifiRXCallback, UDP_PORT); | |
// The hub will now wait for 'identify' packets from nearby nodes | |
} else if ((imp.net.WIFI_STOPPED_UNHAPPY || imp.net.WIFI_STOPPED) && udpsocket != null) { | |
// The WiFi network has gone, null 'udpsocket' so we know it's ready for re-use | |
server.log("Closing UDP"); | |
udpsocket = null; | |
} | |
} | |
function requestReadings() { | |
// Ask for data from all known nodes - recorded in the 'nodes' array | |
if (nodes != null && nodes.len() > 0) { | |
foreach (node in nodes) { | |
if (node.enrolled) { | |
// Send a data request to every known device that is enrolled | |
server.log("Asking NODE @ " + formatAddress(node.address) + " for a reading"); | |
local payload = JSONEncoder.encode({"command":"imp.request.reading"}); | |
// If we didn't receive a response last time, bump the sequence number | |
if (node.state == NODE_STATE.NO_DATA_RECEIVED) node.seqnum += 1; | |
// Transmit the request to the node | |
send(node, payload); | |
} | |
} | |
} | |
// Set a timer to get another set of readings in 60s' time | |
imp.wakeup(60, requestReadings); | |
} | |
function formatAddress(ip) { | |
// Reformat an IP address for logging | |
local ps = split(ip, "."); | |
local op = "" | |
foreach (oct in ps) { | |
if (oct.len() == 2) oct = "0" + oct; | |
if (oct.len() == 1) oct = "00" + oct; | |
op += (oct + "."); | |
} | |
return op.slice(0, op.len() - 1); | |
} | |
// ********** RUNTIME START ********** | |
// Set up the keys we will be using for the demo | |
// A real-world application would source these elsewhere, eg. on-board flash or via the agent | |
keys = []; | |
keys.append("\x58\xED\x26\xD3\x90\x08\x4B\x4E\xA6\x92\x2E\x8A\x7E\x0A\x65\xBE\x19\x72\xB8\x1F\x48\x10\x4A\x1A\x89\xA1\x61\x2B\x7B\x57\xEB\x01"); | |
keys.append("\x7C\xD7\x7C\x25\x7A\x7A\x40\xE5\xB7\x97\x79\x80\x85\x77\x82\x46\xBB\xD8\x20\xE4\xBF\xFD\x40\xCA\x9C\x9A\xCB\x8D\x2A\x58\x01\xAB"); | |
keys.append("\xB6\x57\x93\xAE\x2A\xF3\x46\x9C\x84\x04\xD2\xC9\xE1\xD4\x8C\x08\xDC\xDC\x98\x55\xDD\x52\x4E\xAE\x9D\x56\x0D\xFA\x80\x8E\x01\x7D"); | |
keys.append("\xB3\xED\x7A\x18\xB8\x5B\x40\xE5\xB6\x95\x50\x9A\x3B\x8B\x3B\x1A\xB7\x1C\x38\xEC\xEE\xA0\x45\x70\x8D\x55\xDA\x70\x13\x9A\xE2\xDD"); | |
// Pick a random key to start with | |
keyIndex = (4.0 * math.rand() / RAND_MAX).tointeger(); | |
signingKey = keys[keyIndex]; | |
lastKey = signingKey; | |
// Log the hub's IP address | |
local i = imp.net.info(); | |
hubAddress = i.ipv4.address; | |
broadcastAddress = i.ipv4.broadcast; | |
server.log("HUB IP: " + hubAddress); | |
server.log("Broadcast IP: " + broadcastAddress); | |
// Open the WiFi connection for local use | |
server.log("Opening WiFi for local networking use"); | |
interface = imp.net.open({"interface": "wl0"}, interfaceHandler); | |
// Start asking nodes, if any, for readings | |
requestReadings(); |
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
// ********** Imports ********** | |
#require "JSONEncoder.class.nut:2.0.0" | |
#require "JSONParser.class.nut:1.0.1" | |
#require "HTS221.device.lib.nut:2.0.2" | |
#require "WS2812.class.nut:3.0.0" | |
// ********** Early setup code ********** | |
server.setsendtimeoutpolicy(RETURN_ON_ERROR, WAIT_TIL_SENT, 10); | |
// ********** Constants ********** | |
// Message source indicators | |
enum UDP_MESSAGE_SOURCE { | |
HUB = "IMPH", | |
NODE = "IMPN" | |
} | |
// Message type indicators | |
enum UDP_MESSAGE_TYPE { | |
RESET = "RT", | |
GENERAL = "GL" | |
} | |
// UDP constants | |
const UDP_LONGEST_IMPLEMENTED_MESSAGE_LENGTH = 141; // 10 bytes header + 99 bytes data + 32 bytes footer | |
const UDP_LOW_MEMORY_THRESHOLD = 20000; | |
const UDP_PORT = 20001; | |
// ********** Globals ********** | |
local sensor; | |
local interface; | |
local udpsocket; | |
local lastKey; | |
local signingKey; | |
local keys; | |
local keyIndex; | |
local currentSeqNumber; | |
local broadcastAddress; | |
local nodeAddress; | |
local enrollTimer; | |
local lastMessageTimestamp; | |
local isEnrolled = false; | |
local led; | |
local spi; | |
// ********** Function Definitions ********** | |
function attemptEnroll() { | |
if (!isEnrolled) { | |
// Node not enrolled yet, so try ask again in 30s | |
enrollTimer = imp.wakeup(30, attemptEnroll); | |
// Broadcast an 'identify' packet - only a hub will respond | |
local payload = JSONEncoder.encode({"command":"imp.identify","id":hardware.getdeviceid()}); | |
sendMessage(broadcastAddress, payload); | |
} | |
} | |
function hubWatchdog() { | |
// Simple watchdog to check for an absent hub | |
local diff = time() - lastMessageTimestamp; | |
if (diff > 120) { | |
// No hub messages seen for 120s (2m) - has the hub rebooted? | |
// It might have, so re-enroll | |
isEnrolled = false; | |
attemptEnroll(); | |
server.log("NODE says HUB down for 2+ minutes - re-enrolling"); | |
} else { | |
// All good - check again in 10s | |
imp.wakeup(10, hubWatchdog); | |
} | |
} | |
function wifiRXCallback(fromAddress, fromPort, toAddress, toPort, data) { | |
// Node has eceived a message - log then decode it | |
if (toAddress != nodeAddress && toAddress != broadcastAddress) { | |
server.log("RX from: " + formatAddress(fromAddress) + ":" + fromPort + " (Unicast to other Node)"); | |
return; | |
} else { | |
server.log("RX from: " + formatAddress(fromAddress) + ":" + fromPort + " " + (toAddress == nodeAddress ? "(Unicast to this Node)" : "(Broadcast packet)")); | |
} | |
local response = decodeMessage(data, UDP_MESSAGE_SOURCE.HUB); | |
if ("command" in response) { | |
server.log("Payload: " + JSONEncoder.encode(response)); | |
if (response.command == "imp.identify.ack") { | |
// Received an enrollment confirmation from the hub, so update the node's state | |
lastMessageTimestamp = time(); | |
currentSeqNumber = math.pow(-1, math.rand() % 2).tointeger() * math.rand(); | |
server.log("NODE SN: " + currentSeqNumber); | |
isEnrolled = true; | |
// Send the new sequence number to the hub | |
local payload = JSONEncoder.encode({"command":"imp.request.enroll","id":hardware.getdeviceid()}); | |
sendMessage(fromAddress, payload); | |
// Start the 'hub missing' watchdog | |
hubWatchdog(); | |
} else if (response.command == "imp.request.reading") { | |
// The hub has requested a 16-bit sensor reading, so take one and send it back | |
// Only continue if the sequence number is valid | |
lastMessageTimestamp = time(); | |
server.log("--> SN: " + response.seqnum); | |
if (checkSequenceNumber(response.seqnum)) { | |
led.set(0, [255,0,0]).draw(); | |
sensor.read(function(result) { | |
if ("err" in result) { | |
// Whoops! Report the error | |
server.error("A sensor error occurred: " + result.err); | |
} else { | |
// Got a reading, so get the temperature and add it into | |
// the message in the form the hub expects | |
local temp = format("%.4f", result.temperature); | |
local payload = JSONEncoder.encode({"command":"imp.reading.ack","data":temp,"id":hardware.getdeviceid()}); | |
sendMessage(fromAddress, payload); | |
led.set(0, [0,0,0]).draw(); | |
} | |
}.bindenv(this)); | |
} | |
server.log("NEW SN: " + currentSeqNumber); | |
} | |
} else { | |
// There was an error decoding the message | |
if ("err" in response) server.error(response.err); | |
} | |
} | |
function checkSequenceNumber(messageSeqNumber) { | |
// Check that the incoming sequence number is valid | |
local diff = messageSeqNumber - currentSeqNumber; | |
if (diff <= 0 || diff > 100) { | |
server.log("Bad sequence number - delta: " + diff); | |
return false; | |
} | |
// Update the current sequence number for the next send | |
currentSeqNumber = messageSeqNumber + 1; | |
return true; | |
} | |
function decodeMessage(data, expectedMessageSource) { | |
// Determine whether the message is genuine or not. if it is not, just fail | |
// This is legitimate with UDP comms, which has no ACKS to grind... | |
// Is the message well sized, and have we space for it? | |
if (data.len() > UDP_LONGEST_IMPLEMENTED_MESSAGE_LENGTH || imp.getmemoryfree() < UDP_LOW_MEMORY_THRESHOLD) return {"err":"Bad message length"}; | |
// Check the header start marker is from the hub | |
data.seek(0); | |
local messageSource = data.readstring(4); | |
if (messageSource != expectedMessageSource) { | |
// Bad message type, but ignore message from nodes | |
if (messageSource == UDP_MESSAGE_SOURCE.NODE) return {}; | |
return {"err":"Bad message source: " + messageSource}; | |
} | |
// Get the sequence number | |
local messageSeqNumber = swap4(data.readn('i')); | |
// Check the code | |
local messageType = data.readstring(2); | |
if (messageType != UDP_MESSAGE_TYPE.GENERAL && packetType != UDP_MESSAGE_TYPE.RESET) return {"err":"Bad message type: " + messageType}; | |
// Check the crytographic signature (final 32 bytes of data) | |
data.seek(-32, 'e'); | |
local messageSignature = data.readblob(32); | |
data.seek(0); | |
local signedMessage = data.readblob(data.len() - 32); | |
local count = 0; | |
local done = false; | |
// Iterate through the available keys to see if the message | |
// was signed by one of them | |
do { | |
lastKey = signingKey; | |
signingKey = keys[keyIndex]; | |
local expectedSignature = crypto.hmacsha256(signedMessage, signingKey); | |
if (expectedSignature != null && crypto.equals(expectedSignature, messageSignature)) { | |
// Key worked | |
done = true; | |
} else { | |
// Key failed; try the next one | |
keyIndex++; | |
if (keyIndex > 3) keyIndex = 0; | |
count++; | |
// Tried all the available keys? Bail | |
if (count > keys.len() - 1) return {"err":"Bad key"}; | |
} | |
} while (!done); | |
// Extract and return the message's payload and other data | |
data.seek(10); | |
local messageData = data.readblob(data.len() - data.tell() - 32); | |
local resp = JSONParser.parse(messageData.tostring()); | |
resp.seqnum <- messageSeqNumber; | |
resp.type <- messageType; | |
return resp; | |
} | |
function sendMessage(fromAddress, payload, messageType = UDP_MESSAGE_TYPE.GENERAL) { | |
// Send the string payload to the specified address via UDP - after signing the data | |
local message = encodeMessage(payload, lastKey, UDP_MESSAGE_SOURCE.NODE); | |
udpsocket.send(fromAddress, UDP_PORT, message); | |
} | |
function encodeMessage(data, key, messageSource, messageType = UDP_MESSAGE_TYPE.GENERAL) { | |
// Construct a signed, suitable coded message for sending | |
local message = blob(42 + data.len()) // 10 bytes header + body length + 32 bytes footer | |
// Write header | |
message.seek(0); | |
message.writestring(messageSource); | |
message.writen(swap4(currentSeqNumber), 'i'); // Obfuscate the sequence number | |
message.writestring(messageType); | |
// Write body (assumes an input string) | |
if (data.len() > 0) message.writestring(data); | |
// Write footer | |
message.seek(0); | |
local signature = crypto.hmacsha256(message.readblob(message.len() - 32), key); | |
message.seek(message.len() - 32); | |
message.writeblob(signature); | |
return message; | |
} | |
function interfaceHandler(state) { | |
// Called when the local networking interface state changes | |
server.log("NODE State: " + state); | |
if (state == imp.net.CONNECTED && udpsocket == null) { | |
// We're connected, so initiate UDP | |
server.log("Setting up UDP"); | |
udpsocket = interface.openudp(wifiRXCallback, UDP_PORT); | |
// Now attempt to enroll | |
attemptEnroll(); | |
} else if ((imp.net.WIFI_STOPPED_UNHAPPY || imp.net.WIFI_STOPPED) && udpsocket != null) { | |
// The WiFi network has gone, null 'udpsocket' so we know it's ready for re-use | |
server.log("Closing UDP"); | |
udpsocket = null; | |
isEnrolled = false; | |
} | |
} | |
function formatAddress(ip) { | |
// Reformat an IP address for logging | |
local ps = split(ip, "."); | |
local op = "" | |
foreach (oct in ps) { | |
if (oct.len() == 2) oct = "0" + oct; | |
if (oct.len() == 1) oct = "00" + oct; | |
op += (oct + "."); | |
} | |
return op.slice(0, op.len() - 1); | |
} | |
// ********** RUNTIME START ********** | |
// Write the keys we will be using. | |
// A real-world application would source these elsewhere, eg. on-board flash or via the agent | |
keys = []; | |
keys.append("\x58\xED\x26\xD3\x90\x08\x4B\x4E\xA6\x92\x2E\x8A\x7E\x0A\x65\xBE\x19\x72\xB8\x1F\x48\x10\x4A\x1A\x89\xA1\x61\x2B\x7B\x57\xEB\x01"); | |
keys.append("\x7C\xD7\x7C\x25\x7A\x7A\x40\xE5\xB7\x97\x79\x80\x85\x77\x82\x46\xBB\xD8\x20\xE4\xBF\xFD\x40\xCA\x9C\x9A\xCB\x8D\x2A\x58\x01\xAB"); | |
keys.append("\xB6\x57\x93\xAE\x2A\xF3\x46\x9C\x84\x04\xD2\xC9\xE1\xD4\x8C\x08\xDC\xDC\x98\x55\xDD\x52\x4E\xAE\x9D\x56\x0D\xFA\x80\x8E\x01\x7D"); | |
keys.append("\xB3\xED\x7A\x18\xB8\x5B\x40\xE5\xB6\x95\x50\x9A\x3B\x8B\x3B\x1A\xB7\x1C\x38\xEC\xEE\xA0\x45\x70\x8D\x55\xDA\x70\x13\x9A\xE2\xDD"); | |
// Pick a random key to start with | |
keyIndex = (4.0 * math.rand() / RAND_MAX).tointeger(); | |
signingKey = keys[keyIndex]; | |
lastKey = signingKey; | |
currentSeqNumber = math.pow(-1, math.rand() % 2).tointeger() * math.rand(); | |
// Set up the sensor | |
hardware.i2c89.configure(CLOCK_SPEED_400_KHZ); | |
sensor = HTS221(hardware.i2c89); | |
sensor.setResolution(16, 8); | |
sensor.setMode(HTS221_MODE.CONTINUOUS, 7); | |
// Set up the LED | |
spi = hardware.spi257; | |
spi.configure(MSB_FIRST, 7500); | |
hardware.pin1.configure(DIGITAL_OUT, 1); | |
led = WS2812(spi, 1); | |
// Get the broadcast address | |
local i = imp.net.info(); | |
nodeAddress = i.ipv4.address; | |
broadcastAddress = i.ipv4.broadcast; | |
server.log("Node IP: " + nodeAddress); | |
server.log("Broacast IP: " + broadcastAddress); | |
// Open the WiFi connection for local use | |
server.log("Opening WiFi for local networking"); | |
interface = imp.net.open({"interface":"wl0"}, interfaceHandler); | |
lastMessageTimestamp = time(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment