Last active
October 17, 2018 15:23
-
-
Save ElectricImpSampleCode/ead5412517ebc16fd235a30ed99e4176 to your computer and use it in GitHub Desktop.
Zigbee 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
// Zigbee Lamp Simulator - Gateway Agent Code | |
// Copyright Electric Imp, Inc. 2018 | |
// IMPORTS | |
#require "Rocky.class.nut:2.0.1" | |
// CONSTANTS | |
const HTML_STRING = @" | |
<!DOCTYPE html> | |
<html lang='en-US'> | |
<head> | |
<meta charset='UTF-8'> | |
<meta name='viewport' content='width=device-width, initial-scale=1.0'> | |
<link rel='stylesheet' href='https://netdna.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css'> | |
<link href='https://fonts.googleapis.com/css?family=Abel|Oswald' rel='stylesheet'> | |
<link rel='shortcut icon' href='https://smittytone.github.io/images/ico-imp.ico'> | |
<title>Zigbee Lamp Demo</title> | |
<style> | |
.center {margin-left: auto; margin-right: auto; margin-bottom: auto; margin-top: auto;} | |
body {background-color: #07a9a9;} | |
p {color: white; font-family: Abel, sans-serif;} | |
p.colophon {font-family: Oswald, sans-serif;} | |
p.header {font-size: 22px;color: #54585A; font-weight:bold;} | |
p.controls {font-size: 18px;} | |
h2 {color: #FFCD00; font-family: Abel, sans-serif; font-weight:bold;} | |
h4 {color: white; font-family: Abel, sans-serif;} | |
td {color: white; font-family: Abel, sans-serif;} | |
hr {border-color: white;} | |
.container {padding: 20px;} | |
.uicontent {border: 2px solid white;} | |
.btn-dark {width: 200px;} | |
@media only screen and (max-width: 768px) { | |
.container {padding: 0px;} | |
.uicontent {border: 0px;} | |
.btn-dark {width: 160px;} | |
.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='row uicontent' align='center'> | |
<div class='col'> | |
<!-- Title and Data Readout Row --> | |
<div class='row' align='center'> | |
<div class='col'> | |
<h2 class='text-center'> <br />Zigbee Lamp Demo<br /> </h2> | |
<h4 class='text-center lamp-status'><span>The Lamp is On</span></h4> | |
<h4 class='text-center connect-status'><span>The Lamp is Online</span></h4> | |
<p class='text-center error-message'><i><span></span></i></p> | |
</div> | |
</div> | |
<!-- Controls and Settings --> | |
<div class='row'> | |
<div class='col-3'> </div> | |
<div class='col-6' style='border: 1px solid white;''> | |
<p class='header' align='center'>Lamp Settings</p> | |
<div class='row'> | |
<div class='col-6 power-button' align='center' style='font-family:Abel, sans-serif'> | |
<button class='btn btn-dark' type='submit' id='poweron'>Turn Lamp On</button> | |
</div> | |
<div class='col-6 power-button' align='center' style='font-family:Abel, sans-serif'> | |
<button class='btn btn-dark' type='submit' id='poweroff'>Turn Lamp Off</button> | |
</div> | |
</div> | |
<p style='font-size:8px;'> </p> | |
<div class='row'> | |
<div class='reboot-button col-12' align='center' style='font-family:Abel, sans-serif'> | |
<button class='btn btn-dark' type='submit' id='toggler'>Toggle Lamp</button> | |
</div> | |
</div> | |
| |
</div> | |
<div class='col-3'> </div> | |
</div> | |
<p style='font-size:14px;'> </p> | |
<div class='row'> | |
<div class='col-3'> </div> | |
<div class='col-6' style='border: 1px solid white;''> | |
<p class='header' align='center'>Advanced Settings</p> | |
<p style='font-size:8px;'> </p> | |
<div class='identify-button' align='center' style='font-family:Abel, sans-serif'> | |
<button class='btn btn-dark' type='submit' id='identifier'>Identify Lamp</button> | |
</div> | |
<hr> | |
<div class='debug-checkbox' style='color:white;font-family:Abel, sans-serif' align='center'> | |
<input type='checkbox' name='debug' id='debug' value='debug'> Debug Mode<br /> | |
</div> | |
</div> | |
<div class='col-3'> </div> | |
</div> | |
<!-- Colophon --> | |
<p class='colophon'><small>Zigbee Lamp Demo © Electric Imp, 2018</small></p> | |
</div> | |
</div> | |
</div> | |
<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js'></script> | |
<script> | |
var agenturl = '%s'; | |
var power = true; | |
// Get initial readings | |
getState(updateReadout); | |
// Set UI click actions | |
$('#toggler').click(doToggle); | |
$('#poweron').click(setPowerOn); | |
$('#poweroff').click(setPowerOff); | |
$('#debug').click(setDebug); | |
$('#identifier').click(doIdentify); | |
function updateReadout(data) { | |
if (data.error) { | |
$('.error-message span').text(data.error); | |
} else { | |
power = data.on; | |
$('.lamp-status span').text(power ? 'The Lamp is On' : 'The Lamp is Off'); | |
$('.connect-status span').text(data.connected ? 'The Lamp is Online' : 'The Lamp is Offline'); | |
document.getElementById('debug').checked = data.debug; | |
} | |
setTimeout(function() { | |
getState(updateReadout); | |
}, 30000); | |
} | |
function getState(callback) { | |
// Request the current data | |
$.ajax({ | |
url : agenturl + '/state', | |
type: 'GET', | |
success : function(response) { | |
response = JSON.parse(response); | |
if (callback) { | |
callback(response); | |
} | |
}, | |
cache: false | |
}); | |
} | |
function doToggle() { | |
// Trigger a device restart | |
$.ajax({ | |
url : agenturl + '/light', | |
type: 'POST', | |
data: JSON.stringify({ 'light' : 'toggle' }), | |
success : function(response) { | |
power = !power | |
$('.lamp-status span').text('The Lamp is ' + (power ? 'On' : 'Off')); | |
}, | |
cache: false | |
}); | |
} | |
function doIdentify() { | |
// Trigger a lamp identify | |
$.ajax({ | |
url : agenturl + '/actions', | |
type: 'POST', | |
data: JSON.stringify({ 'action' : 'identify' }), | |
cache: false | |
}); | |
} | |
function setPowerOn() { | |
// Tell the device to light up | |
$.ajax({ | |
url : agenturl + '/light', | |
type: 'POST', | |
data: JSON.stringify({ 'light' : 'on' }), | |
success : function(response) { | |
power = true; | |
$('.lamp-status span').text('The Lamp is On'); | |
}, | |
cache: false | |
}); | |
} | |
function setPowerOff() { | |
// Tell the device to light up | |
$.ajax({ | |
url : agenturl + '/light', | |
type: 'POST', | |
data: JSON.stringify({ 'light' : 'off' }), | |
success : function(response) { | |
power = false; | |
$('.lamp-status span').text('The Lamp is Off'); | |
}, | |
cache: false | |
}); | |
} | |
function setDebug() { | |
// Tell the device to enter or leave debug mode | |
$.ajax({ | |
url : agenturl + '/actions', | |
type: 'POST', | |
data: JSON.stringify({ 'action' : 'debug', 'debug' : document.getElementById('debug').checked }), | |
cache: false | |
}); | |
} | |
</script> | |
</body> | |
</html> | |
"; | |
// GLOBALS | |
local lampState = null; | |
local lampAPI = null; | |
// FUNCTIONS | |
function setDefaultState() { | |
// Set the lamp's initial state - this will be updated shortly | |
lampState = {}; | |
lampState.debug <- true; | |
lampState.on <- true; | |
} | |
// RUNTIME START | |
// Initalise the state of the lamp | |
setDefaultState(); | |
// Set up device message handlers | |
device.on("send.state", function(devState) { | |
lampState = devState; | |
server.log("Lamp state: " + (lampState.on ? "on" : "off")); | |
}); | |
// Set up the lamp's Internet API | |
lampAPI = Rocky(); | |
lampAPI.get("/", function(context) { | |
// Send the UI, eg. to a web browser | |
context.send(200, format(HTML_STRING, http.agenturl())); | |
}); | |
lampAPI.get("/state", function(context) { | |
// Handle a GET request made to /state: return a state structure | |
local data = {}; | |
data.on <- lampState.on; | |
data.debug <- lampState.debug; | |
local isOnline = device.isconnected(); | |
data.connected <- isOnline ? "1" : "0"; | |
context.send(200, http.jsonencode(data, {"compact":true})); | |
}); | |
lampAPI.post("/light", function(context) { | |
// Handle a POST request made to /light - light control | |
local data; | |
try { | |
// Attempt to decode the received data as JSON | |
data = http.jsondecode(context.req.rawbody); | |
} catch (err) { | |
server.error(err); | |
context.send(400, "Bad data posted"); | |
return; | |
} | |
if ("light" in data) { | |
// Does the 'light' value indicate a known command? | |
// If so, send an appropriate message to the device to issue a command | |
// to the lamp via Zigbee | |
if (data.light == "on") device.send("light.on", true); | |
if (data.light == "off") device.send("light.on", false); | |
if (data.light == "toggle") device.send("light.toggle", true); | |
// Reply to the source, ie. the web UI | |
context.send(200, "OK"); | |
} | |
}); | |
lampAPI.post("/actions", function(context) { | |
// Handle a POST request made to /actions - non-light lamp actions | |
local data; | |
try { | |
// Attempt to decode the received data as JSON | |
data = http.jsondecode(context.req.rawbody); | |
} catch (err) { | |
server.error(err); | |
context.send(400, "Bad data posted"); | |
return; | |
} | |
if ("action" in data) { | |
// Does the 'action' value indicate toggling the debug state? | |
if (data.action == "debug") { | |
// In this case the data will also contain a 'debug' key with | |
// the chosen debug state as its value | |
lampState.debug = data.debug; | |
server.log("Debugging " + (lampState.debug ? "enabled" : "disabled")); | |
// Relay the choice to the device | |
device.send("set.debug", lampState.debug); | |
} | |
// Does the 'action' value indicate toggling the debug state | |
if (data.action == "identify") device.send("lamp.identify", true); | |
// Reply to the source, ie. the web UI | |
context.send(200, "OK"); | |
} | |
}); |
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
// Zigbee Lamp Simulator - Gateway Device Code | |
// Copyright Electric Imp, Inc. 2018 | |
// IMPORTS | |
#require "xbee.class.nut:1.0.0" | |
// CONSTANTS | |
// Zigbee Endpoints | |
const LAMP_ENDPOINT = 0x40; | |
const GATEWAY_ENDPOINT = 0x00; | |
// Zigbee Lamp Profile ID | |
const LAMP_PROFILE_ID = 0x0104; | |
// Lamp Cluster IDs | |
const LAMP_CLUSTER_BASIC = 0x0000; | |
const LAMP_CLUSTER_IDENTIFY = 0x0003; | |
const LAMP_CLUSTER_ONOFF = 0x0006; | |
// Commands | |
const LAMP_LIGHT_CMD_GETSTATE = 0x00; | |
const LAMP_LIGHT_CMD_ON = 0x00; | |
const LAMP_LIGHT_CMD_OFF = 0x01; | |
const LAMP_LIGHT_CMD_TOGGLE = 0x02; | |
const LAMP_COMMAND_IDENTIFY = 0x00; | |
const CLUSTER_BASIC_CMD_READ_ATTR = 0x00; | |
const CLUSTER_BASIC_CMD_READ_RSPD = 0x01; | |
const CLUSTER_BASIC_CMD_WRITE_ATTR = 0x02; | |
const CLUSTER_BASIC_CMD_WRITE_RSPD = 0x04; | |
const CLUSTER_IDENT_CMD_READ_ATTR = 0x00; | |
const CLUSTER_IDENT_CMD_READ_RSPD = 0x01; | |
const CLUSTER_ONOFF_CMD_READ_RSPD = 0x01; | |
// XBee Transmission Constants (Partial) | |
const SUCCESS = 0x00; | |
const FAILURE = 0x01; | |
const UNSUPPORTED_ATTRIBUTE = 0x86; | |
// ZCL Frame Structure Constants | |
const ZCL_FRAME_CONTROL_BYTE = 0x00; | |
const ZCL_TRANSACTION_ID_BYTE = 0x01; | |
const ZCL_COMMAND_ID_BYTE = 0x02; | |
// ZCL Data Type Constants | |
const UINT8 = 0x20; | |
const UINT16 = 0x21; | |
const ENUM8 = 0x30; | |
const CHAR_STRING = 0x42; | |
const BOOL = 0x10; | |
// GLOBALS | |
// The lamp's Zigbee addresses | |
local lampAddress64bit = "0013A20040DD310C"; | |
local lampAddress16bit = 0xFFFE; | |
local coordinatorUART = null; | |
local coordinator = null; | |
local debug = true; | |
local trans = 1; | |
// Zigbee Cluster Library (ZCL) Attributes use by the application | |
local BasicAttributes = ["ZCL Version", "Application Version", "Stack Version", "Hardware Version", | |
"Manufacturer Name", "Model Identifier", "Production Date", "Power Source"]; | |
local IdentAttributes = ["Identify Time"]; | |
local LightAttributes = ["On/Off"]; | |
// FUNCTIONS | |
// XBee Response Callback | |
// It is through this callback that all data received by the XBee | |
// is relayed to the host app | |
function xBeeResponse(error, response) { | |
if (error) { | |
server.error(error); | |
return; | |
} | |
// Does the frame contain an XBee-specific command? | |
if ("command" in response) { | |
if (response.command == "AI") { | |
// 'AI' is a frame of network status information | |
if ("data" in response) { | |
if (debug) server.log("Network Status: " + decodeAI(response.data[0])); | |
if (response.data[0] == 0x00) { | |
// Have successfully formed a network (signalled by a value == 0) | |
// so take no further action here | |
return; | |
} | |
} | |
// Network not yet up, so check again in 5 seconds | |
imp.wakeup(5, function() { | |
coordinator.sendLocalATCommand("AI"); | |
}); | |
} | |
if (response.command == "VR") { | |
local fv = (response.data[0] * 256) + response.data[1]; | |
if (debug) server.log("XBee Firmware Version: " + getHex(fv, 4)); | |
} | |
} | |
// 'cmdid' contains the XBee API packet ID that has been passed back to the host app | |
if ("cmdid" in response) { | |
// Is the packet an AT Command Response? | |
if (response.cmdid == 0x88) { | |
if (debug) server.log("Command " + response.command + " status: " + response.status.message); | |
} | |
// Is the packet a Modem Status? | |
if (response.cmdid == 0x8A) { | |
if (debug) server.log("Modem status: " + response.status.message); | |
} | |
// Is the packet an auto-generated Zigbee TX Response? | |
if (response.cmdid == 0x8B) { | |
if (debug) { | |
server.log("Delivery status : " + response.deliveryStatus.message); | |
server.log("Discovery status: " + response.discoveryStatus.message); | |
server.log("Dest. Addr: " + format("0x%04X", response.address16bit)); | |
} | |
if (response.address16bit == 0xFFFD) { | |
// If the packet couldn't readch the target, 0xFFFD is returned as the | |
// the target 16-bit address, and we should set the recorded address to | |
// 'unknown' (0xFFFE) | |
lampAddress16bit = 0xFFFE; | |
if (debug) server.log("Destination Address not found - resetting stored address"); | |
} else { | |
if (debug) server.log("Destination Address: " + getHex(response.address16bit, 4)); | |
} | |
} | |
// Does the packet contain an explicit Zigbee RX packet? | |
if (response.cmdid == 0x91) { | |
// Is it a lamp-specific packet | |
if (response.profileID == LAMP_PROFILE_ID) { | |
// Update the lamp's 16-bit address (this can change) | |
lampAddress16bit = response.address16bit; | |
// Decode the ZCL frame | |
local zclFrame = response.data; | |
local frameControl = zclFrame[0]; | |
local transaction = zclFrame[1]; | |
local commandID = zclFrame[2]; | |
if ((frameControl & 0x08) == 0) { | |
// Data flow direction is client (gateway) -> server (lamp), so ignore this packet | |
if (debug) server.log("Wrong direction: Client to Server"); | |
return; | |
} | |
// Does the ZCL Frame target the Basic Cluster? | |
if (response.clusterID == LAMP_CLUSTER_BASIC) { | |
// Assume this is a global Cluster command (ie. the frame control Frame Type bits are 0x00) | |
// because these are the only commands we send (others are supported by the lamp) | |
if (commandID == CLUSTER_BASIC_CMD_READ_RSPD) { | |
// This is a Cluser-specific command: a response to a read request we have | |
// issued for lamp information | |
decodeAttributes(zclFrame, BasicAttributes); | |
} | |
return; | |
} | |
// Does the ZCL Frame target the Identify Cluster? | |
if (response.clusterID == LAMP_CLUSTER_IDENTIFY) { | |
// Assume this is a global Cluster command (ie. the frame control Frame Type bits are 0x00) | |
// because these are the only commands we send (others are supported by the lamp) | |
if (commandID == CLUSTER_IDENT_CMD_READ_RSPD) { | |
// Print out the requested attribute values | |
decodeAttributes(zclFrame, IdentAttributes); | |
} | |
} | |
// Does the ZCL Frame target the OnOFF Cluster? | |
if (response.clusterID == LAMP_CLUSTER_ONOFF) { | |
// Assume frame control Frame Type bits are 0x00, ie. a general command | |
if (commandID == CLUSTER_ONOFF_CMD_READ_RSPD) { | |
// Print out the requested attribute values | |
decodeAttributes(zclFrame, LightAttributes); | |
// Get lamp state attribute | |
// NOTE This is the only attribute we can read from the ONOFF cluster | |
// so we know it will be a frame bytes 6 (data type) and 7 (value) | |
// NOTE Value is 0x01 for true; 0x00 for false (Zigbe standard) | |
local dataType = zclFrame[6]; | |
if (dataType == BOOL) { | |
local state = {}; | |
state.on <- (zclFrame[7] == 0x01 ? true : false); | |
state.debug <- debug; | |
// Send the lamp state to the agent for use by the UI | |
agent.send("send.state", state); | |
} | |
} | |
} | |
} else if (response.profileID = 0x0000) { | |
// Update the lamp's 16-bit address (this can change) | |
lampAddress16bit = response.address16bit; | |
} | |
} | |
} | |
} | |
function getHex(value, digits) { | |
// Display 'value' as a hex number of 'digits' digits in length | |
local fs = digits == 4 ? "%04X" : "%02X"; | |
return ("0x" + format(fs, value)); | |
} | |
function decodeAttributes(frame, attList) { | |
// Extract the list of attributes and their records, decode and then display them | |
// NOTE These Attributes and their values are defined by the ZCL Specification | |
if (debug) { | |
server.log("Decoding ZCL Packet Payload..."); | |
// Set i to the first data byte in the received ZCL frame | |
local i = 3; | |
// Iterate through the frame bytes to find Attribute Records: | |
// Attribute ID (16-bit, little endian) | |
// Read Operation Status (8-bit) | |
// Attribute Data Type (8-bit) | |
// Attribute Value (8 or more bits, according to type) | |
// NOTE For strings, the first byte of the value is the string length | |
do { | |
local attribute = frame[i] + (frame[i + 1] * 256); | |
local status = frame[i + 2]; | |
if (status != 0x00) { | |
server.error("Bad attribute"); | |
return; | |
} else { | |
local type = frame[i + 3]; | |
// Handle out-of-sequence attributes | |
if (attribute == 0x4000) { | |
local length = frame[i + 4]; | |
local s = ""; | |
for (local j = 0 ; j < length ; j++) { | |
s = s + (frame[i + 5 + j]).tochar(); | |
} | |
server.log("SW Build ID: " + s); | |
i = i + 5 + length; | |
} else { | |
if (type == BOOL) { | |
server.log(attList[attribute] + ": " + (frame[i + 4] == 0x01 ? "TRUE" : "FALSE")); | |
i = i + 5; | |
} | |
if (type == UINT8) { | |
server.log(attList[attribute] + ": " + getHex(frame[i + 4], 2)); | |
i = i + 5; | |
} | |
if (type == UINT16) { | |
local a = frame[i + 4] + (frame[i + 5] * 256); | |
server.log(attList[attribute] + ": " + a + " seconds"); | |
i = i + 6; | |
} | |
if (type == CHAR_STRING) { | |
local length = frame[i + 4]; | |
local s = ""; | |
for (local j = 0 ; j < length ; j++) { | |
s = s + (frame[i + 5 + j]).tochar(); | |
} | |
server.log(attList[attribute] + ": " + s); | |
i = i + 5 + length; | |
} | |
} | |
} | |
} while (i < frame.len()); | |
server.log("ZCL Packet Payload Decoded"); | |
} | |
} | |
function decodeAI(code) { | |
// Decode the response from an "AI" command (network status) send to the XBee | |
// while the gateway is forming a Zigbee networl | |
if (code == 0x00) return "Successfully formed or joined a network"; | |
local states = {}; | |
states[0x21] <- "Scan found no PANs"; | |
states[0x22] <- "Scan found no valid PANs based on current SC and ID settings"; | |
states[0x23] <- "Valid Coordinator or Routers found, but they are not allowing joining"; | |
states[0x24] <- "No joinable beacons found"; | |
states[0x25] <- "Unexpected state, node should not be attempting to join at this time"; | |
states[0x27] <- "Node Joining attempt failed (typically due to incompatible security settings)"; | |
states[0x2A] <- "Coordinator Start attempt failed"; | |
states[0x2B] <- "Checking for an existing coordinator"; | |
states[0x2C] <- "Attempt to leave the network failed"; | |
states[0xAB] <- "Attempted to join a device that did not respond"; | |
states[0xAC] <- "Secure join error - network security key received unsecured"; | |
states[0xAD] <- "Secure join error - network security key not received"; | |
states[0xAF] <- "Secure join error - joining device does not have the right preconfigured link key"; | |
states[0xFF] <- "Scanning for a Zigbee network (routers and end devices"; | |
if (code in states) return states[code]; | |
return "Unknown state"; | |
} | |
function getBasicInfo() { | |
// This should cause the lamp to return some basic info about itself | |
local zFrame = blob(11); | |
// Make and add the ZCL header to the payload | |
zFrame.writeblob(makeZCLframeHeader()); | |
// Add two Attribute IDs to the request | |
zFrame[3] = 0x04; // Attribute identifier LSB - Manufacturer's Name | |
zFrame[4] = 0x00; // Attribute identifier MSB | |
zFrame[5] = 0x05; // Attribute identifier LSB - Model ID | |
zFrame[6] = 0x00; // Attribute identifier MSB | |
zFrame[7] = 0x00; // Attribute identifier LSB - SW Version | |
zFrame[8] = 0x40; // Attribute identifier MSB | |
zFrame[9] = 0x06; // Attribute identifier LSB - Production Date | |
zFrame[10] = 0x00; // Attribute identifier MSB | |
// Send the request | |
send(LAMP_CLUSTER_BASIC, zFrame); | |
} | |
function getState() { | |
// Assemble a new ZCL packet that will be transmitted to the lamp | |
local zFrame = blob(5); | |
zFrame.writeblob(makeZCLframeHeader(true, false, true, trans, LAMP_LIGHT_CMD_GETSTATE)); | |
zFrame[3] = 0x00; // An Attribute ID: in this case, the lamp state Attribute | |
zFrame[4] = 0x00; | |
// Send the ZCL packet to the ONOFF Cluster via Zigbee | |
send(LAMP_CLUSTER_ONOFF, zFrame); | |
} | |
function makeZCLframeHeader(isCmdGlobal = true, isCmdManufacturerSpecific = false, isDirectionToServer = true, transactionID = -1, cmdID = 0x00) { | |
// Paramters: | |
// 1. isCmdGlobal - Boolean: is the command global (true), or specific to the cluster (false)? | |
// 2. isCmdManufacturerSpecific - Boolean: is the command manufacturer specific (true), or not (false)? | |
// 3. isDirectionToServer - Boolean: is the command being sent to the cluster server (true)? | |
// NOTE In this application, the cluster server is the lamp | |
// 4. transactionID - An 8-bit value to identify the transaction. | |
// NOTE This can be used to match a response to a request, though we do not do so here | |
// 5. cmdID - An 8-bit value indicating the command being sent | |
// Assemble a ZCL frame header of three bytes | |
local header = blob(3); | |
// Assemble the Frame Control byte (non-set bits must be clear) | |
local fc = 0; | |
if (!isCmdGlobal) fc = fc + 1; | |
if (isCmdManufacturerSpecific) fc = fc | 4; | |
if (!isDirectionToServer) fc = fc | 8; | |
header[0] = fc; | |
// Add the transaction ID | |
if (transactionID == -1) { | |
header[1] = trans; | |
trans++; | |
if (trans > 255) trans = 0; | |
} else { | |
header[1] = transactionID; | |
} | |
// Add the command ID | |
header[2] = cmdID; | |
return header; | |
} | |
function send(clusterID, payload, frameID = -1) { | |
// Send the supplied ZCL packet out to the lamp via the XBee API | |
local r = coordinator.sendZCL(lampAddress64bit, | |
lampAddress16bit, | |
GATEWAY_ENDPOINT, | |
LAMP_ENDPOINT, | |
clusterID, | |
LAMP_PROFILE_ID, | |
payload, | |
0, | |
frameID); | |
} | |
function init() { | |
// Put coordinator into ZDO mode - this is necessary to | |
// receive ZDO commands and responses (ie. enables the explicit | |
// receive API frame (API ID 0x91) which indicates the source and | |
// destination endpoints, cluster ID, and profile ID) | |
coordinator.enterZDMode(); | |
// Check the network status | |
coordinator.sendLocalATCommand("AI"); | |
} | |
// RUNTIME START | |
// Instantiate Zigbee coordinator | |
// NOTE You will need to change this line depending on what imp dev board you are using | |
// and over which UART its XBee is connected. This code was written for the | |
// imp005 Breakout Board | |
coordinatorUART = hardware.uart1; | |
coordinator = XBee(coordinatorUART, xBeeResponse, true, true, false); | |
// Prep the coordinator | |
init(); | |
// Request some basic data from the lamp | |
imp.wakeup(20, getBasicInfo); | |
// Set up agent message handlers | |
// These will be triggered by the agent as you operate the web-based UI | |
agent.on("light.on", function(state) { | |
// Switch the lamp on or off, as required, indicated by 'state', a boolean value | |
// First, assemble the ZCL packet that will be transmitted to the lamp | |
local command = state ? LAMP_LIGHT_CMD_ON : LAMP_LIGHT_CMD_OFF; | |
local zFrame = makeZCLframeHeader(false, false, true, trans, command); | |
// NOTE No payload required here (see 'lamp.identify', below) | |
// Send the ZCL packet to the ONOFF Cluster via Zigbee | |
send(LAMP_CLUSTER_ONOFF, zFrame); | |
}); | |
agent.on("light.toggle", function(ignored) { | |
// Toggle the lamp on or off | |
// First, assemble the ZCL packet that will be transmitted to the lamp | |
local zFrame = makeZCLframeHeader(false, false, true, trans, LAMP_LIGHT_CMD_TOGGLE); | |
// NOTE No payload required here (see 'lamp.identify', below) | |
// Send the ZCL packet to the ONOFF Cluster via Zigbee | |
send(LAMP_CLUSTER_ONOFF, zFrame); | |
// Make sure we update the state information for the UI | |
// (because we don't store lamp state here) | |
getState(); | |
}); | |
agent.on("lamp.identify", function(state) { | |
// Make the lamp idenfify itself by flashing its light | |
// First, assemble the ZCL packet that will be transmitted to the lamp | |
local zFrame = blob(5); | |
zFrame.writeblob(makeZCLframeHeader(false, false, true, trans, LAMP_COMMAND_IDENTIFY)); | |
// Here we need to pass a (little endian) 16-bit value to the lamp: how long (in seconds) | |
// that it will flash to identify itself (see ZCL spec 3.5.2.3) | |
zFrame[3] = 0x78; // Identity time - 120 seconds | |
zFrame[4] = 0x00; | |
// Send the ZCL packet to the IDENTIFY Cluster via Zigbee | |
send(LAMP_CLUSTER_IDENTIFY, zFrame); | |
}); | |
agent.on("set.debug", function(state) { | |
// Record the debugging state if updated by the UI | |
debug = state; | |
}); | |
agent.on("get.state", function(ignored) { | |
// This message is sent by the agent to get the lamp's actual state (rather | |
// than the infered one; see setDefaults() in the agent code) from the lamp | |
getState(); | |
}); |
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
// Zigbee Lamp Simulator - Lamp Device Code | |
// Copyright Electric Imp, Inc. 2018 | |
// IMPORTS | |
//#require "xbee.class.nut:1.0.0" | |
#import "~/documents/github/xbee/xbee.device.lib.nut" | |
// CONSTANTS | |
// Lamp Endpoints | |
const LAMP_ENDPOINT = 0x40; | |
const GATEWAY_ENDPOINT = 0x20; | |
// Smart Lamp Profile IDs | |
const LAMP_PROFILE_ID = 0x0104; | |
// Lamp Cluster IDs | |
const LAMP_CLUSTER_BASIC = 0x0000; | |
const LAMP_CLUSTER_IDENTIFY = 0x0003; | |
const LAMP_CLUSTER_ONOFF = 0x0006; | |
const LAMP_CLUSTER_LEVEL = 0x0008; | |
// Commands - Cluster Specific | |
const LAMP_BASIC_CMD_RESET = 0x00; | |
const LAMP_IDENT_CMD_IDENTIFY = 0x00; | |
const LAMP_IDENT_CMD_IDENTQRY = 0x01; | |
const LAMP_IDENT_CMD_IQRY_RSPNSE = 0x00; | |
const LAMP_ONOFF_CMD_ON = 0x00; | |
const LAMP_ONOFF_CMD_OFF = 0x01; | |
const LAMP_ONOFF_CMD_TOGGLE = 0x02; | |
const LAMP_LEVEL_MOVE_TO_LEVEL = 0x00; | |
const LAMP_LEVEL_MOVE = 0x01; | |
// Commands - General | |
const GLOBAL_READ_REQ = 0x00; | |
const GLOBAL_READ_RSP = 0x01; | |
const GLOBAL_WRITE_REQ = 0x02; | |
const GLOBAL_WRITE_RSP = 0x04; | |
// XBee Transmission Constants (Partial) | |
const SUCCESS = 0x00; | |
const FAILURE = 0x01; | |
const UNSUPPORTED_ATTRIBUTE = 0x86; | |
// XBee Frame Types | |
const XBEE_AT_CMD_STATUS = 0x88; | |
const XBEE_FRAME_MODEM_STATUS = 0x8A; | |
const XBEE_FRAME_ZIGBEE_TX_STATUS = 0x8B; | |
const XBEE_FRAME_ZIGBEE_EXP_RX = 0x91; | |
// ZCL Frame Structure Constants | |
const ZCL_FRAME_CONTROL_BYTE = 0x00; | |
const ZCL_TRANSACTION_ID_BYTE = 0x01; | |
const ZCL_COMMAND_ID_BYTE = 0x02; | |
// ZCL Data Type Constants | |
const UINT8 = 0x20; | |
const UINT16 = 0x21; | |
const ENUM8 = 0x30; | |
const CHAR_STRING = 0x42; | |
const BOOL = 0x10; | |
// GLOBALS | |
local gatewayAddress64bit = "0000000000000000"; // The coordinator | |
local gatewayAddress16bit = 0xFFFE; | |
local zigbee = null; | |
local lampState = null; | |
local lampLightPin = null; | |
local lampUART = null; | |
local identifyTimer = null; | |
local debug = true; | |
local trans = 1; | |
// FUNCTIONS | |
// XBee Response Callback | |
// It is through this callback that all data received by the XBee | |
// is relayed to the host app | |
function xBeeResponse(error, response) { | |
if (error) { | |
// Report any error encountered | |
server.error(error); | |
return; | |
} | |
if ("command" in response) { | |
if (response.command == "MY") { | |
local a = (response.data[0] << 8) + response.data[1]; | |
if (debug) server.log("Local 16-bit address: " + getHex(a, 4)); | |
// Relay address back to requester | |
send16BitAddress(a); | |
} | |
} | |
// 'cmdid' contains the XBee API packet ID that has been passed back to the host app | |
if ("cmdid" in response) { | |
// Is the packet an AT Command Response? | |
if (response.cmdid == XBEE_AT_CMD_STATUS) { | |
if (debug) server.log("Command (" + response.command + ") status: " + response.status.message); | |
} | |
// Is the packet a Modem Status? | |
if (response.cmdid == XBEE_FRAME_MODEM_STATUS) { | |
if (debug) server.log("Modem status: " + response.status.message); | |
} | |
// Is the packet an auto-generated Zigbee TX Response? | |
if (response.cmdid == XBEE_FRAME_ZIGBEE_TX_STATUS) { | |
local s = "Zigbee TX Report: Delivery " + response.deliveryStatus.message + ", " + response.discoveryStatus.message; | |
if (response.address16bit == 0xFFFD) { | |
// If the packet couldn't readch the target, 0xFFFD is returned as the | |
// the target 16-bit address, and we should set the recorded address to | |
// 'unknown' (0xFFFE) | |
gatewayAddress16bit = 0xFFFE; | |
if (debug) server.log(s + ", Destination Address not found"); | |
} else { | |
if (debug) server.log(s + ", Destination Address " + getHex(response.address16bit, 4)); | |
gatewayAddress16bit = response.address16bit; | |
} | |
} | |
// Does the packet contain an explicit Zigbee RX packet? | |
if (response.cmdid == XBEE_FRAME_ZIGBEE_EXP_RX) { | |
// Is the Zigbee Profile ID and Endpoint that of the demo lamp? | |
if (response.profileID == LAMP_PROFILE_ID) { | |
// Store the current 16-bit lamp address | |
gatewayAddress16bit = response.address16bit; | |
// Decode the Zigbee Cluster Library (ZCL) frame | |
local zFrame = response.data; | |
local frameControl = zFrame[ZCL_FRAME_CONTROL_BYTE]; | |
local transaction = zFrame[ZCL_TRANSACTION_ID_BYTE]; | |
local commandID = zFrame[ZCL_COMMAND_ID_BYTE]; | |
if ((frameControl & 0x08) != 0) { | |
// Data flow direction is server (lamp) -> client (gateway), so ignore this packet | |
if (debug) server.log("Wrong direction: Lamp to Gateway"); | |
return; | |
} | |
// Does the ZCL Frame target the Basic Cluster? | |
if (response.clusterID == LAMP_CLUSTER_BASIC) { | |
if (frameControl == 0x01) { | |
// This is a Cluser-specific command | |
if (commandID == LAMP_BASIC_CMD_RESET) { | |
// Factory Reset | |
// NOTE This is not implemented in this simulation | |
} else { | |
if (debug) server.log("Unsupported Cluster-specific command received"); | |
} | |
} else { | |
// This is a Global Cluser command | |
if (commandID == GLOBAL_READ_REQ) { | |
// Read Attributes Command - ie. payload contains a list of attributes to read | |
// each of which is a 16-bit value placed in a simple list | |
// Get all the supplied attributes | |
local attributes = getAttributes(zFrame); | |
if (attributes.len() > 0) { | |
// Create the Read Attributes Response | |
local responseData = blob(); | |
// Iterate through the list of attributes and prepare return data payloads for each | |
foreach (att in attributes) { | |
switch (att) { | |
case 0x0000: // ZCL Version | |
responseData.writen(0x00, 'b'); // Attribute ID LSB | |
responseData.writen(0x00, 'b'); // Attribute ID MSB | |
responseData.writen(SUCCESS, 'b'); // Status - Success | |
responseData.writen(UINT8, 'b'); // Data type | |
responseData.writen(0x02, 'b'); // The data | |
break; | |
case 0x0001: // Application Version | |
responseData.writen(0x01, 'b'); | |
responseData.writen(0x00, 'b'); | |
responseData.writen(SUCCESS, 'b'); | |
responseData.writen(UINT8, 'b'); | |
responseData.writen(0x00, 'b'); | |
break; | |
case 0x0002: // Stack Version | |
responseData.writen(0x02, 'b'); | |
responseData.writen(0x00, 'b'); | |
responseData.writen(SUCCESS, 'b'); | |
responseData.writen(UINT8, 'b'); | |
responseData.writen(0x00, 'b'); | |
break; | |
case 0x0003: // Hardware Version | |
responseData.writen(0x03, 'b'); | |
responseData.writen(0x00, 'b'); | |
responseData.writen(SUCCESS, 'b'); | |
responseData.writen(UINT8, 'b'); | |
responseData.writen(0x00, 'b'); | |
break; | |
case 0x0004: // Manufacturer's name | |
responseData.writen(0x04, 'b'); | |
responseData.writen(0x00, 'b'); | |
responseData.writen(SUCCESS, 'b'); | |
responseData.writen(CHAR_STRING, 'b'); | |
local s = "The Electric Imp Lighting Company"; | |
responseData.writen(s.len(), 'b'); // String length | |
responseData.writestring(s) | |
break; | |
case 0x0005: // Model ID | |
responseData.writen(0x05, 'b'); | |
responseData.writen(0x00, 'b'); | |
responseData.writen(SUCCESS, 'b'); | |
responseData.writen(CHAR_STRING, 'b'); | |
local s = "Zigbee Lamp Simulator"; | |
responseData.writen(s.len(), 'b'); | |
responseData.writestring(s) | |
break; | |
case 0x0006: // Date Code | |
responseData.writen(0x06, 'b'); | |
responseData.writen(0x00, 'b'); | |
responseData.writen(SUCCESS, 'b'); | |
responseData.writen(CHAR_STRING, 'b'); | |
local s = "2018/09/30"; | |
responseData.writen(s.len(), 'b'); | |
responseData.writestring(s) | |
break; | |
case 0x0007: // Power Source | |
responseData.writen(0x07, 'b'); | |
responseData.writen(0x00, 'b'); | |
responseData.writen(SUCCESS, 'b'); | |
responseData.writen(ENUM8, 'b'); | |
responseData.writen(0x01, 'b'); // Assume we're on mains power | |
break; | |
case 0x4000: // SW Build ID | |
responseData.writen(0x00, 'b'); | |
responseData.writen(0x40, 'b'); | |
responseData.writen(SUCCESS, 'b'); | |
responseData.writen(CHAR_STRING, 'b'); | |
local s = imp.getsoftwareversion(); | |
responseData.writen(s.len(), 'b'); | |
responseData.writestring(s) | |
break; | |
case 0x4003: // Product 10NC | |
responseData.writen(0x03, 'b'); | |
responseData.writen(0x40, 'b'); | |
responseData.writen(SUCCESS, 'b'); | |
responseData.writen(CHAR_STRING, 'b'); | |
local s = imp.getbootromversion(); | |
responseData.writen(s.len(), 'b'); | |
responseData.writestring(s) | |
break; | |
default: | |
// Don't recognise the attribute so send back a FAIL | |
responseData.writen((att & 0xFF), 'b'); | |
responseData.writen(((att & 0xFF00) >> 8), 'b'); | |
responseData.writen(UNSUPPORTED_ATTRIBUTE, 'b'); | |
break; | |
} | |
} | |
if (responseData.len() > 0) { | |
local frame = makeZCLFrame(0x08, transaction, GLOBAL_READ_RSP, responseData); | |
sendZCLFrame(response, frame); | |
} | |
} else { | |
if (debug) server.log("No attributes recogized"); | |
} | |
} else { | |
if (debug) server.log("Unsupported global Cluster command received"); | |
} | |
} | |
} | |
// Does the ZCL Frame target the Identify Cluster? | |
if (response.clusterID == LAMP_CLUSTER_IDENTIFY) { | |
if (frameControl == 0x01) { | |
// Process Cluster-specific commands | |
if (commandID == LAMP_IDENT_CMD_IDENTIFY) { | |
// Identify Command | |
// This takes a time and flashes the lamp so an installer | |
// can distinguish it from other lamps of its kind | |
local newTime = 60; | |
try { | |
newTime = zFrame[3] + (zFrame[4] * 256); | |
} catch (e) { | |
if (debug) server.log("Bad Idenifty Time supplied - Defaulting to 60s"); | |
} | |
if (newTime > 0) lampState.identifyTime = newTime; | |
if (debug) server.log("Lamp Identify Time Set To: " + lampState.identifyTime + " seconds"); | |
// Flash the imp's LED for 'lampIdentifyTime' seconds | |
if (!lampState.isIdentifying) { | |
if (debug) server.log("Lamp Identification Cycle On"); | |
lampState.isIdentifying = true; | |
lampState.flash = lampState.isOn; | |
identifyTimer = imp.wakeup(1.0, identifyLoop); | |
} else { | |
identifyComplete(); | |
} | |
} else if (commandID == LAMP_IDENT_CMD_IDENTQRY) { | |
// Identify Query Command | |
// This is used to check if the lamp is currently identifying itself | |
if (lampState.isIdentifying) { | |
// ZCL return payload is the current indentification time as a uint16 | |
local responseData = blob(); | |
responseData.writen((lampState.identifyTime & 0xFF), 'b'); | |
responseData.writen((lampState.identifyTime & 0xFF00) >> 8, 'b'); | |
local frame = makeZCLFrame(0x08, transaction, LAMP_IDENT_CMD_IQRY_RSPNSE, responseData); | |
sendZCLFrame(response, frame); | |
} | |
// NOTE Spec says 'take no further action' if the lamp is NOT identifying itself | |
} else { | |
if (debug) server.log("Unsupported Cluster-specific command received"); | |
} | |
} else { | |
// Process global Cluster commands | |
if (commandID == GLOBAL_READ_REQ) { | |
// Get all of the requested attributes by their 16-bit IDs - should be only one, 0x0000 | |
local attributes = getAttributes(zFrame); | |
if (attributes.len() > 0) { | |
// Create the Read Attributes Response | |
local responseData = blob(); | |
foreach (att in attributes) { | |
switch (att) { | |
case 0x0000: // IdentifyTime | |
responseData.writen(0x00, 'b'); | |
responseData.writen(0x00, 'b'); | |
responseData.writen(SUCCESS, 'b'); | |
responseData.writen(UINT16, 'b'); | |
responseData.writen((lampState.identifyTime & 0xFF), 'b'); | |
responseData.writen((lampState.identifyTime & 0xFF00) >> 8, 'b'); | |
break; | |
default: | |
// Don't recognise the attribute so send back a FAIL | |
responseData.writen((att & 0xFF), 'b'); | |
responseData.writen(((att & 0xFF00) >> 8), 'b'); | |
responseData.writen(UNSUPPORTED_ATTRIBUTE, 'b'); | |
break; | |
} | |
} | |
if (responseData.len() > 0) { | |
local frame = makeZCLFrame(0x08, transaction, GLOBAL_READ_RSP, responseData); | |
sendZCLFrame(response, frame); | |
} | |
} else { | |
if (debug) server.log("No attributes supplied"); | |
} | |
} else if (commandID == GLOBAL_WRITE_REQ) { | |
// Get all of the written attributes by their 16-bit IDs - should be only one, 0x0000 | |
local attributes = getAttributes(zFrame); | |
if (attributes.len() > 0) { | |
// Create the Written Attributes Response | |
local responseData = blob(); | |
foreach (att in attributes) { | |
switch (att) { | |
case 0x0000: // IdentifyTime | |
// Write the attribute value to app storage | |
lampState.identifyTime = zFrame[i + 3] + (zFrame[i + 4] * 256); | |
if (lampState.identifyTime == 0 && lampState.isIdentifying) identifyComplete(); | |
if (debug) server.log("IdentityTime set to " + getHex(lampState.IdentifyTime, 4)); | |
// And build the response | |
responseData.writen(0x00, 'b'); | |
responseData.writen(0x00, 'b'); | |
responseData.writen(SUCCESS, 'b'); | |
responseData.writen(UINT16, 'b'); | |
responseData.writen((lampState.identifyTime & 0xFF), 'b'); | |
responseData.writen((lampState.identifyTime & 0xFF00) >> 8, 'b'); | |
break; | |
default: | |
// Don't recognise the attribute so send back a FAIL | |
responseData.writen((att & 0xFF), 'b'); | |
responseData.writen(((att & 0xFF00) >> 8), 'b'); | |
responseData.writen(UNSUPPORTED_ATTRIBUTE, 'b'); | |
break; | |
} | |
} | |
if (responseData.len() > 0) { | |
local frame = makeZCLFrame(0x08, transaction, GLOBAL_WRITE_RSP, responseData); | |
sendZCLFrame(response, frame); | |
} | |
} else { | |
if (debug) server.log("No attributes supplied"); | |
} | |
} else { | |
if (debug) server.log("Unsupported global Cluster command received"); | |
} | |
} | |
} | |
// Does the ZCL Frame target the OnOFF Cluster? | |
if (response.clusterID == LAMP_CLUSTER_ONOFF) { | |
if (frameControl == 0x01) { | |
// Process Cluster-specific commands | |
if (commandID == LAMP_ONOFF_CMD_ON) { | |
// Turn lamp on | |
lampState.isOn = true; | |
setLampLevel(); | |
} else if (commandID == LAMP_ONOFF_CMD_OFF) { | |
// Turn lamp off | |
lampState.isOn = false; | |
lampLightPin.write(0.0); | |
} else if (commandID == LAMP_ONOFF_CMD_TOGGLE) { | |
// Toggle lamp | |
lampState.isOn = !lampState.isOn; | |
setLampLevel(); | |
} else { | |
if (debug) server.log("Unsupported Cluster-specific command received"); | |
} | |
} else { | |
// Process global Cluster commands | |
if (commandID == GLOBAL_READ_REQ) { | |
// Get all of the requested attributes by their 16-bit IDs - should be only one, 0x0000 | |
local attributes = getAttributes(zFrame); | |
if (attributes.len() > 0) { | |
// Create the Read Attributes Response | |
local responseData = blob(); | |
foreach (att in attributes) { | |
switch (att) { | |
case 0x0000: // On/Off State | |
responseData.writen(0x00, 'b'); | |
responseData.writen(0x00, 'b'); | |
responseData.writen(SUCCESS, 'b'); | |
responseData.writen(BOOL, 'b'); | |
responseData.writen((lampState.isOn ? 0x01 : 0x00), 'b'); // Set 1 for on, 0 for off | |
break; | |
default: | |
// Don't recognise the attribute so send back a FAIL | |
responseData.writen((att & 0xFF), 'b'); | |
responseData.writen(((att & 0xFF00) >> 8), 'b'); | |
responseData.writen(UNSUPPORTED_ATTRIBUTE, 'b'); | |
break; | |
} | |
} | |
if (responseData.len() > 0) { | |
local frame = makeZCLFrame(0x08, transaction, GLOBAL_READ_RSP, responseData); | |
sendZCLFrame(response, frame); | |
} | |
} else { | |
if (debug) server.log("No attributes supplied"); | |
} | |
} else { | |
if (debug) server.log("Unsupported global Cluster command received"); | |
} | |
} | |
} | |
if (response.clusterID == LAMP_CLUSTER_LEVEL) { | |
if (frameControl == 0x01) { | |
// Cluster-specific commands | |
if (commandID == LAMP_LEVEL_MOVE_TO_LEVEL) { | |
local newLevel = zFrame[3]; | |
local transTime = zFrame[4] + (zFrame[5] << 8); | |
server.log("Level move: " + lampState.level + " -> " + newLevel); | |
if (lampState.level != newLevel) { | |
lampState.onLevel = newLevel; | |
lampState.transTime = transTime == 0xFFFF ? lampstate.onOffTransTime : transTime; | |
lampState.deltaFunction = function() { | |
local delta = lampState.level - lampState.onLevel; | |
lampState.level = lampState.level - (math.abs(delta) == delta ? 1 : -1); | |
setLampLevel(); | |
if (lampState.level != lampState.onLevel) { | |
imp.wakeup((10.0 / transTime.tofloat()), lampState.deltaFunction); | |
} else { | |
server.log("Done!"); | |
} | |
}.bindenv(this); | |
lampState.deltaFunction(); | |
} | |
} | |
if (commandID == LAMP_LEVEL_MOVE) { | |
local mode = zFrame[3]; | |
local rate = zFrame[4]; | |
local delta = mode == 0x00 ? 1 : -1; | |
server.log("Move: " + (mode == 0x00 ? "up" : "down") + " in " + rate + " steps/second"); | |
lampState.transTime = rate * delta; | |
lampState.deltaFunction = function() { | |
lampState.level = lampState.level + lampState.transTime; | |
if (lampState.level > 0xFF) lampState.level = 0xFF; | |
if (lampState.level <= 0x00) { | |
lampState.level = 0x00; | |
lampState.isOn = false; | |
} | |
setLampLevel(); | |
if (lampState.level > 0x00 && lampState.level < 0xFF) { | |
lampState.isOn = true; | |
imp.wakeup(1.0, lampState.deltaFunction); | |
} else { | |
server.log("Done!"); | |
} | |
}.bindenv(this); | |
lampState.deltaFunction(); | |
} | |
} else { | |
// Global commands | |
if (commandID == GLOBAL_READ_REQ) { | |
// Get all of the requested attributes by their 16-bit IDs - should be only one, 0x0000 | |
local attributes = getAttributes(zFrame); | |
if (attributes.len() > 0) { | |
// Create the Read Attributes Response | |
local responseData = blob(); | |
foreach (att in attributes) { | |
switch (att) { | |
case 0x0000: // Current Level | |
responseData.writen(0x00, 'b'); | |
responseData.writen(0x00, 'b'); | |
responseData.writen(SUCCESS, 'b'); | |
responseData.writen(UINT8, 'b'); | |
responseData.writen(lampState.level, 'b'); | |
break; | |
case 0x0010: // OnOffTransitionTime | |
responseData.writen(0x00, 'b'); | |
responseData.writen(0x00, 'b'); | |
responseData.writen(SUCCESS, 'b'); | |
responseData.writen(UINT16, 'b'); | |
responseData.writen(lampState.onOffTransTime & 0xFF, 'b'); | |
responseData.writen((lampState.onOffTransTime & 0xFF00) >> 8, 'b'); | |
break; | |
case 0x0011: // OnLevel | |
responseData.writen(0x00, 'b'); | |
responseData.writen(0x00, 'b'); | |
responseData.writen(SUCCESS, 'b'); | |
responseData.writen(UINT8, 'b'); | |
responseData.writen(lampState.onLevel, 'b'); | |
break; | |
case 0x0012: // OnTransitionTime | |
responseData.writen(0x00, 'b'); | |
responseData.writen(0x00, 'b'); | |
responseData.writen(SUCCESS, 'b'); | |
responseData.writen(UINT16, 'b'); | |
responseData.writen(lampState.onTransTime & 0xFF, 'b'); | |
responseData.writen((lampState.onTransTime & 0xFF00) >> 8, 'b'); | |
break; | |
case 0x00113: // OffTransitionTime | |
responseData.writen(0x00, 'b'); | |
responseData.writen(0x00, 'b'); | |
responseData.writen(SUCCESS, 'b'); | |
responseData.writen(UINT16, 'b'); | |
responseData.writen(lampState.offTransTime & 0xFF, 'b'); | |
responseData.writen((lampState.offTransTime & 0xFF00) >> 8, 'b'); | |
break; | |
default: | |
// Don't recognise the attribute so send back a FAIL | |
responseData.writen((att & 0xFF), 'b'); | |
responseData.writen(((att & 0xFF00) >> 8), 'b'); | |
responseData.writen(UNSUPPORTED_ATTRIBUTE, 'b'); | |
break; | |
} | |
} | |
if (responseData.len() > 0) { | |
local frame = makeZCLFrame(0x08, transaction, GLOBAL_READ_RSP, responseData); | |
sendZCLFrame(response, frame); | |
} | |
} else { | |
if (debug) server.log("No attributes supplied"); | |
} | |
} else { | |
if (debug) server.log("Unsupported global Cluster command received"); | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
function getHex(value, digits) { | |
// Display 'value' as a hex number of 'digits' digits in length | |
local fs = digits == 4 ? "%04X" : "%02X"; | |
return ("0x" + format(fs, value)); | |
} | |
function getAttributes(frame) { | |
// Extract the sequence of attribute IDs from the ZCL frame and return them as an array | |
local atts = []; | |
for (local i = 3 ; i < frame.len() ; i = i + 2) { | |
try { | |
local att = frame[i + 1] * 256 + frame[i]; | |
atts.append(att); | |
} catch (err) { | |
// NOP | |
} | |
} | |
return atts; | |
} | |
function makeZCLFrame(frameControl, transaction, command, response) { | |
// Build a ZCL response frame | |
local frame = blob(3 + response.len()); | |
// Write in the header | |
frame[ZCL_FRAME_CONTROL_BYTE] = frameControl; | |
frame[ZCL_TRANSACTION_ID_BYTE] = transaction; | |
frame[ZCL_COMMAND_ID_BYTE] = command; | |
// Write in the data payload | |
frame.seek(3, 'b'); | |
frame.writeblob(response); | |
return frame; | |
} | |
function sendZCLFrame(response, frame) { | |
// Send the ZCL frame back out on the Zigbee radio | |
local fid = "frameid" in response ? response.frameid : -1; | |
local r = zigbee.sendZCL(response.address64bit, | |
response.address16bit, | |
LAMP_ENDPOINT, | |
response.sourceEndpoint, | |
response.clusterID, | |
response.profileID, | |
frame, | |
fid); | |
} | |
function identifyLoop() { | |
// As per the ZCL spec, decrement the value of 'lampIdentifyTime' every second | |
// until we get to zero, then stop the identification signal | |
lampState.identifyTime--; | |
if (lampState.identifyTime <= 0) { | |
identifyComplete(); | |
return; | |
} | |
// Turn the lamp on or off, alternately | |
lampState.flash = !lampState.flash; | |
lampLightPin.write(lampState.flash ? 1.0 : 0); | |
// Come back in one second's time | |
identifyTimer = imp.wakeup(1.0, identifyLoop); | |
} | |
function identifyComplete() { | |
// The lamp has stopped identifying itself, so do the clean-up here | |
imp.cancelwakeup(identifyTimer); | |
lampState.isIdentifying = false; | |
lampState.identifyTime = 0; | |
if (debug) server.log("Lamp Identification Cycle Off"); | |
// Swith the lamp on or off as per its pre-identification mode state | |
setLampLevel(); | |
} | |
function setLampLevel() { | |
local dutyCyle = lampState.level.tofloat() / 255.0; | |
lampLightPin.write(lampState.isOn ? dutyCyle : 0.0); | |
} | |
function setLampDefaults() { | |
lampState = {}; | |
lampState.identifyTime <- 0x0000; // 60 seconds | |
lampState.isIdentifying <- false; | |
lampState.isOn <- true; | |
lampState.flash <- true; | |
lampState.level <- 0xFE; | |
lampState.onLevel <- 0xFE; | |
lampState.onOffTransTime <- 0x0001; // 10ms | |
lampState.onTransTime <- 0x0001; // 10ms | |
lampState.offTransTime <- 0x0001; // 10ms | |
lampState.transTime <- 0x0001; // 10ms | |
lampState.deltaFunction <- function() {}; | |
} | |
// RUNTIME START | |
// Set up the lamp's initial state | |
setLampDefaults(); | |
// Configure the hardware | |
// NOTE You will need to change these lines if you are not working with an imp003-based | |
// board. This code was written for the imp003 Breakout Board | |
lampLightPin = hardware.pinL; | |
local dutyCyle = lampState.level.tofloat() / 255.0; | |
lampLightPin.configure(PWM_OUT, 0.01, (lampState.isOn ? dutyCyle : 0.0)); | |
lampUART = hardware.uartDM; | |
// Configure the radio object | |
// NOTE You will need to change this line if you are not working with the imp005-based | |
// breakout board | |
zigbee = XBee(lampUART, xBeeResponse, true, true, true); | |
// Put lamp into ZDO mode - this is necessary to | |
// receive ZDO and ZCL commands and responses (ie. enables the explicit | |
// receive API frame (API ID 0x91) which indicates the source and | |
// destination endpoints, cluster ID, and profile ID) | |
zigbee.enterZDMode(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment