Add Zigbee better support for Xiaomi Double Switch and Xiaomi Vibration sensor

This commit is contained in:
Hadinger 2019-12-23 16:53:54 +01:00
parent a10b199808
commit 749960533f
7 changed files with 182 additions and 57 deletions

View File

@ -3,6 +3,7 @@
### 8.0.0.2 20191223
- Changed Settings variable namings
- Add Zigbee better support for Xiaomi Double Switch and Xiaomi Vibration sensor
### 8.0.0.1 20191221

View File

@ -468,6 +468,7 @@
#define D_CMND_ZIGBEE_STATUS "ZigbeeStatus"
#define D_CMND_ZIGBEE_RESET "ZigbeeReset"
#define D_JSON_ZIGBEE_CC2530 "CC2530"
#define D_CMND_ZIGBEEZNPRECEIVE "ZigbeeZNPReceive" // only for debug
#define D_CMND_ZIGBEEZNPSEND "ZigbeeZNPSend"
#define D_JSON_ZIGBEE_STATE "ZigbeeState"
#define D_JSON_ZIGBEEZNPRECEIVED "ZigbeeZNPReceived"

View File

@ -91,6 +91,8 @@ public:
void jsonClear(uint16_t shortaddr);
void jsonAppend(uint16_t shortaddr, JsonObject &values);
const JsonObject *jsonGet(uint16_t shortaddr);
const void jsonPublish(uint16_t shortaddr); // publish the json message and clear buffer
bool jsonIsConflict(uint16_t shortaddr, const JsonObject &values);
private:
std::vector<Z_Device> _devices = {};
@ -419,6 +421,67 @@ void Z_Devices::jsonClear(uint16_t shortaddr) {
device.json_buffer->clear();
}
void CopyJsonVariant(JsonObject &to, const String &key, const JsonVariant &val) {
to.remove(key); // force remove to have metadata like LinkQuality at the end
if (val.is<char*>()) {
String sval = val.as<String>(); // force a copy of the String value
to.set(key, sval);
} else if (val.is<JsonArray>()) {
JsonArray &nested_arr = to.createNestedArray(key);
CopyJsonArray(nested_arr, val.as<JsonArray>());
} else if (val.is<JsonObject>()) {
JsonObject &nested_obj = to.createNestedObject(key);
CopyJsonObject(nested_obj, val.as<JsonObject>());
} else {
to.set(key, val);
}
}
void CopyJsonArray(JsonArray &to, const JsonArray &arr) {
for (auto v : arr) {
if (v.is<char*>()) {
String sval = v.as<String>(); // force a copy of the String value
to.add(sval);
} else if (v.is<JsonArray>()) {
} else if (v.is<JsonObject>()) {
} else {
to.add(v);
}
}
}
void CopyJsonObject(JsonObject &to, const JsonObject &from) {
for (auto kv : from) {
String key_string = kv.key;
JsonVariant &val = kv.value;
CopyJsonVariant(to, key_string, val);
}
}
// does the new payload conflicts with the existing payload, i.e. values would be overwritten
bool Z_Devices::jsonIsConflict(uint16_t shortaddr, const JsonObject &values) {
Z_Device & device = getShortAddr(shortaddr);
if (&device == nullptr) { return false; } // don't crash if not found
if (&values == nullptr) { return false; }
if (nullptr == device.json) {
return false; // if no previous value, no conflict
}
for (auto kv : values) {
String key_string = kv.key;
if (strcasecmp_P(kv.key, PSTR(D_CMND_ZIGBEE_LINKQUALITY))) { // exception = ignore duplicates for LinkQuality
if (device.json->containsKey(kv.key)) {
return true; // conflict!
}
}
}
return false;
}
void Z_Devices::jsonAppend(uint16_t shortaddr, JsonObject &values) {
Z_Device & device = getShortAddr(shortaddr);
if (&device == nullptr) { return; } // don't crash if not found
@ -428,24 +491,7 @@ void Z_Devices::jsonAppend(uint16_t shortaddr, JsonObject &values) {
device.json = &(device.json_buffer->createObject());
}
// copy all values from 'values' to 'json'
for (auto kv : values) {
String key_string = kv.key;
const char * key = key_string.c_str();
JsonVariant &val = kv.value;
device.json->remove(key_string); // force remove to have metadata like LinkQuality at the end
if (val.is<char*>()) {
String sval = val.as<String>(); // force a copy of the String value
device.json->set(key_string, sval);
} else if (val.is<JsonArray>()) {
// todo
} else if (val.is<JsonObject>()) {
// todo
} else {
device.json->set(key_string, kv.value);
}
}
CopyJsonObject(*device.json, values);
}
const JsonObject *Z_Devices::jsonGet(uint16_t shortaddr) {
@ -454,6 +500,19 @@ const JsonObject *Z_Devices::jsonGet(uint16_t shortaddr) {
return device.json;
}
const void Z_Devices::jsonPublish(uint16_t shortaddr) {
const JsonObject *json = zigbee_devices.jsonGet(shortaddr);
if (json == nullptr) { return; } // don't crash if not found
String msg = "";
json->printTo(msg);
zigbee_devices.jsonClear(shortaddr);
Response_P(PSTR("{\"" D_CMND_ZIGBEE_RECEIVED "\":{\"0x%04X\":%s}}"), shortaddr, msg.c_str());
MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR));
XdrvRulesProcess();
}
// Dump the internal memory of Zigbee devices
// Mode = 1: simple dump of devices addresses and names
// Mode = 2: Mode 1 + also dump the endpoints, profiles and clusters

View File

@ -100,6 +100,7 @@ public:
return _frame_control.b.frame_type & 1;
}
static void generateAttributeName(const JsonObject& json, uint16_t cluster, uint16_t attr, char *key, size_t key_len);
void parseRawAttributes(JsonObject& json, uint8_t offset = 0);
void parseReadAttributes(JsonObject& json, uint8_t offset = 0);
void parseClusterSpecificCommand(JsonObject& json, uint8_t offset = 0);
@ -290,17 +291,20 @@ uint32_t parseSingleAttribute(JsonObject& json, char *attrid_str, class SBuffer
bool parse_as_string = true;
uint32_t len = (attrtype <= 0x42) ? buf.get8(i) : buf.get16(i); // len is 8 or 16 bits
i += (attrtype <= 0x42) ? 1 : 2; // increment pointer
if (i + len > buf.len()) { // make sure we don't get past the buffer
len = buf.len() - i;
}
// check if we can safely use a string
if ((0x41 == attrtype) || (0x43 == attrtype)) { parse_as_string = false; }
else {
for (uint32_t j = 0; j < len; j++) {
if (0x00 == buf.get8(i+j)) {
parse_as_string = false;
break;
}
}
}
// else {
// for (uint32_t j = 0; j < len; j++) {
// if (0x00 == buf.get8(i+j)) {
// parse_as_string = false;
// break;
// }
// }
// }
if (parse_as_string) {
char str[len+1];
@ -409,19 +413,28 @@ uint32_t parseSingleAttribute(JsonObject& json, char *attrid_str, class SBuffer
return i - offset; // how much have we increased the index
}
// Generate an attribute name based on cluster number, attribute, and suffix if duplicates
void ZCLFrame::generateAttributeName(const JsonObject& json, uint16_t cluster, uint16_t attr, char *key, size_t key_len) {
uint32_t suffix = 1;
snprintf_P(key, key_len, PSTR("%04X/%04X"), cluster, attr);
while (json.containsKey(key)) {
suffix++;
snprintf_P(key, key_len, PSTR("%04X/%04X+%d"), cluster, attr, suffix); // add "0008/0001+2" suffix if duplicate
}
}
// First pass, parse all attributes in their native format
void ZCLFrame::parseRawAttributes(JsonObject& json, uint8_t offset) {
uint32_t i = offset;
uint32_t len = _payload.len();
while (len - i >= 3) {
while (len >= i + 3) {
uint16_t attrid = _payload.get16(i);
i += 2;
char key[16];
snprintf_P(key, sizeof(key), PSTR("%04X/%04X"),
_cluster_id, attrid);
generateAttributeName(json, _cluster_id, attrid, key, sizeof(key));
// exception for Xiaomi lumi.weather - specific field to be treated as octet and not char
if ((0x0000 == _cluster_id) && (0xFF01 == attrid)) {
@ -445,8 +458,7 @@ void ZCLFrame::parseReadAttributes(JsonObject& json, uint8_t offset) {
if (0 == status) {
char key[16];
snprintf_P(key, sizeof(key), PSTR("%04X/%04X"),
_cluster_id, attrid);
generateAttributeName(json, _cluster_id, attrid, key, sizeof(key));
i += parseSingleAttribute(json, key, _payload, i, len);
}
@ -472,7 +484,7 @@ void ZCLFrame::parseClusterSpecificCommand(JsonObject& json, uint8_t offset) {
// return value:
// 0 = keep initial value
// 1 = remove initial value
typedef int32_t (*Z_AttrConverter)(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const __FlashStringHelper* new_name, uint16_t cluster, uint16_t attr);
typedef int32_t (*Z_AttrConverter)(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr);
typedef struct Z_AttributeConverter {
uint16_t cluster;
uint16_t attribute;
@ -775,13 +787,13 @@ const Z_AttributeConverter Z_PostProcess[] PROGMEM = {
// ======================================================================
// Record Manuf
int32_t Z_ManufKeep(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const __FlashStringHelper *new_name, uint16_t cluster, uint16_t attr) {
int32_t Z_ManufKeep(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) {
json[new_name] = value;
zigbee_devices.setManufId(shortaddr, value.as<const char*>());
return 1;
}
//
int32_t Z_ModelKeep(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const __FlashStringHelper *new_name, uint16_t cluster, uint16_t attr) {
int32_t Z_ModelKeep(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) {
json[new_name] = value;
zigbee_devices.setModelId(shortaddr, value.as<const char*>());
return 1;
@ -789,29 +801,29 @@ int32_t Z_ModelKeep(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& j
// ======================================================================
// Remove attribute
int32_t Z_Remove(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const __FlashStringHelper *new_name, uint16_t cluster, uint16_t attr) {
int32_t Z_Remove(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) {
return 1; // remove original key
}
// Copy value as-is
int32_t Z_Copy(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const __FlashStringHelper *new_name, uint16_t cluster, uint16_t attr) {
int32_t Z_Copy(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) {
json[new_name] = value;
return 1; // remove original key
}
// Add pressure unit
int32_t Z_AddPressureUnit(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const __FlashStringHelper *new_name, uint16_t cluster, uint16_t attr) {
int32_t Z_AddPressureUnit(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) {
json[new_name] = F(D_UNIT_PRESSURE);
return 0; // keep original key
}
// Convert int to float and divide by 100
int32_t Z_FloatDiv100(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const __FlashStringHelper *new_name, uint16_t cluster, uint16_t attr) {
int32_t Z_FloatDiv100(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) {
json[new_name] = ((float)value) / 100.0f;
return 1; // remove original key
}
// Convert int to float and divide by 10
int32_t Z_FloatDiv10(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const __FlashStringHelper *new_name, uint16_t cluster, uint16_t attr) {
int32_t Z_FloatDiv10(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) {
json[new_name] = ((float)value) / 10.0f;
return 1; // remove original key
}
@ -825,7 +837,7 @@ int32_t Z_OccupancyCallback(uint16_t shortaddr, uint16_t cluster, uint16_t endpo
}
// Aqara Vibration Sensor - special proprietary attributes
int32_t Z_AqaraVibration(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const __FlashStringHelper *new_name, uint16_t cluster, uint16_t attr) {
int32_t Z_AqaraVibration(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) {
//json[new_name] = value;
switch (attr) {
case 0x0055:
@ -879,7 +891,7 @@ int32_t Z_AqaraVibration(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObje
return 1; // remove original key
}
int32_t Z_AqaraSensor(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const __FlashStringHelper *new_name, uint16_t cluster, uint16_t attr) {
int32_t Z_AqaraSensor(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) {
String hex = value;
SBuffer buf2 = SBuffer::SBufferFromHex(hex.c_str(), hex.length());
uint32_t i = 0;
@ -925,11 +937,19 @@ void ZCLFrame::postProcessAttributes(uint16_t shortaddr, JsonObject& json) {
String key_string = kv.key;
const char * key = key_string.c_str();
JsonVariant& value = kv.value;
// Check that format looks like "CCCC/AAAA"
// Check that format looks like "CCCC/AAAA" or "CCCC/AAAA+d"
char * delimiter = strchr(key, '/');
char * delimiter2 = strchr(key, '+');
if (delimiter) {
uint16_t attribute;
uint16_t suffix = 1;
uint16_t cluster = strtoul(key, &delimiter, 16);
uint16_t attribute = strtoul(delimiter+1, nullptr, 16);
if (!delimiter2) {
attribute = strtoul(delimiter+1, nullptr, 16);
} else {
attribute = strtoul(delimiter+1, &delimiter2, 16);
suffix = strtoul(delimiter2+1, nullptr, 10);
}
// Iterate on filter
for (uint32_t i = 0; i < sizeof(Z_PostProcess) / sizeof(Z_PostProcess[0]); i++) {
@ -939,7 +959,9 @@ void ZCLFrame::postProcessAttributes(uint16_t shortaddr, JsonObject& json) {
if ((conv_cluster == cluster) &&
((conv_attribute == attribute) || (conv_attribute == 0xFFFF)) ) {
int32_t drop = (*converter->func)(this, shortaddr, json, key, value, (const __FlashStringHelper*) converter->name, conv_cluster, conv_attribute);
String new_name_str = converter->name;
if (suffix > 1) { new_name_str += suffix; } // append suffix number
int32_t drop = (*converter->func)(this, shortaddr, json, key, value, new_name_str, conv_cluster, conv_attribute);
if (drop) {
json.remove(key);
}

View File

@ -32,6 +32,7 @@ const uint8_t ZIGBEE_STATUS_DEVICE_ANNOUNCE = 30; // Device announces its
const uint8_t ZIGBEE_STATUS_NODE_DESC = 31; // Node descriptor
const uint8_t ZIGBEE_STATUS_ACTIVE_EP = 32; // Endpoints descriptor
const uint8_t ZIGBEE_STATUS_SIMPLE_DESC = 33; // Simple Descriptor (clusters)
const uint8_t ZIGBEE_STATUS_DEVICE_INDICATION = 34; // Device announces its address
const uint8_t ZIGBEE_STATUS_CC_VERSION = 50; // Status: CC2530 ZNP Version
const uint8_t ZIGBEE_STATUS_CC_INFO = 51; // Status: CC2530 Device Configuration
const uint8_t ZIGBEE_STATUS_UNSUPPORTED_VERSION = 98; // Unsupported ZNP version

View File

@ -357,6 +357,28 @@ int32_t Z_ReceiveEndDeviceAnnonce(int32_t res, const class SBuffer &buf) {
return -1;
}
// 45CA
int32_t Z_ReceiveTCDevInd(int32_t res, const class SBuffer &buf) {
Z_ShortAddress srcAddr = buf.get16(2);
Z_IEEEAddress ieeeAddr = buf.get64(4);
Z_ShortAddress parentNw = buf.get16(12);
zigbee_devices.updateDevice(srcAddr, ieeeAddr);
char hex[20];
Uint64toHex(ieeeAddr, hex, 64);
Response_P(PSTR("{\"" D_JSON_ZIGBEE_STATE "\":{"
"\"Status\":%d,\"IEEEAddr\":\"%s\",\"ShortAddr\":\"0x%04X\""
",\"ParentNetwork\":\"0x%04X\"}}"),
ZIGBEE_STATUS_DEVICE_INDICATION, hex, srcAddr, parentNw
);
MqttPublishPrefixTopic_P(RESULT_OR_TELE, PSTR(D_JSON_ZIGBEEZCL_RECEIVED));
XdrvRulesProcess();
//Z_SendActiveEpReq(srcAddr);
return -1;
}
// Aqara Occupancy behavior: the Aqara device only sends Occupancy: true events every 60 seconds.
// Here we add a timer so if we don't receive a Occupancy event for 90 seconds, we send Occupancy:false
const uint32_t OCCUPANCY_TIMEOUT = 90 * 1000; // 90 s
@ -378,16 +400,11 @@ void Z_AqaraOccupancy(uint16_t shortaddr, uint16_t cluster, uint16_t endpoint, c
int32_t Z_PublishAttributes(uint16_t shortaddr, uint16_t cluster, uint16_t endpoint, uint32_t value) {
const JsonObject *json = zigbee_devices.jsonGet(shortaddr);
if (json == nullptr) { return 0; } // don't crash if not found
// Post-provess for Aqara Presence Senson
Z_AqaraOccupancy(shortaddr, cluster, endpoint, json);
String msg = "";
json->printTo(msg);
zigbee_devices.jsonClear(shortaddr);
Response_P(PSTR("{\"" D_CMND_ZIGBEE_RECEIVED "\":{\"0x%04X\":%s}}"), shortaddr, msg.c_str());
MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR));
XdrvRulesProcess();
zigbee_devices.jsonPublish(shortaddr);
return 1;
}
int32_t Z_ReceiveAfIncomingMessage(int32_t res, const class SBuffer &buf) {
@ -438,8 +455,13 @@ int32_t Z_ReceiveAfIncomingMessage(int32_t res, const class SBuffer &buf) {
if (defer_attributes) {
// Prepare for publish
zigbee_devices.jsonAppend(srcaddr, json);
zigbee_devices.setTimer(srcaddr, USE_ZIGBEE_COALESCE_ATTR_TIMER, clusterid, srcendpoint, 0, &Z_PublishAttributes);
if (zigbee_devices.jsonIsConflict(srcaddr, json)) {
// there is conflicting values, force a publish of the previous message now and don't coalesce
zigbee_devices.jsonPublish(srcaddr);
} else {
zigbee_devices.jsonAppend(srcaddr, json);
zigbee_devices.setTimer(srcaddr, USE_ZIGBEE_COALESCE_ATTR_TIMER, clusterid, srcendpoint, 0, &Z_PublishAttributes);
}
} else {
// Publish immediately
msg = "";
@ -459,6 +481,7 @@ typedef struct Z_Dispatcher {
// Filters for ZCL frames
ZBM(AREQ_AF_INCOMING_MESSAGE, Z_AREQ | Z_AF, AF_INCOMING_MSG) // 4481
ZBM(AREQ_END_DEVICE_ANNCE_IND, Z_AREQ | Z_ZDO, ZDO_END_DEVICE_ANNCE_IND) // 45C1
ZBM(AREQ_END_DEVICE_TC_DEV_IND, Z_AREQ | Z_ZDO, ZDO_TC_DEV_IND) // 45CA
ZBM(AREQ_PERMITJOIN_OPEN_XX, Z_AREQ | Z_ZDO, ZDO_PERMIT_JOIN_IND ) // 45CB
ZBM(AREQ_ZDO_ACTIVEEPRSP, Z_AREQ | Z_ZDO, ZDO_ACTIVE_EP_RSP) // 4585
ZBM(AREQ_ZDO_SIMPLEDESCRSP, Z_AREQ | Z_ZDO, ZDO_SIMPLE_DESC_RSP) // 4584
@ -466,6 +489,7 @@ ZBM(AREQ_ZDO_SIMPLEDESCRSP, Z_AREQ | Z_ZDO, ZDO_SIMPLE_DESC_RSP) // 4584
const Z_Dispatcher Z_DispatchTable[] PROGMEM = {
{ AREQ_AF_INCOMING_MESSAGE, &Z_ReceiveAfIncomingMessage },
{ AREQ_END_DEVICE_ANNCE_IND, &Z_ReceiveEndDeviceAnnonce },
{ AREQ_END_DEVICE_TC_DEV_IND, &Z_ReceiveTCDevInd },
{ AREQ_PERMITJOIN_OPEN_XX, &Z_ReceivePermitJoinStatus },
{ AREQ_ZDO_NODEDESCRSP, &Z_ReceiveNodeDesc },
{ AREQ_ZDO_ACTIVEEPRSP, &Z_ReceiveActiveEp },

View File

@ -31,12 +31,14 @@ TasmotaSerial *ZigbeeSerial = nullptr;
const char kZigbeeCommands[] PROGMEM = "|"
D_CMND_ZIGBEEZNPSEND "|" D_CMND_ZIGBEE_PERMITJOIN "|"
D_CMND_ZIGBEE_STATUS "|" D_CMND_ZIGBEE_RESET "|" D_CMND_ZIGBEE_SEND "|"
D_CMND_ZIGBEE_PROBE "|" D_CMND_ZIGBEE_READ ;
D_CMND_ZIGBEE_PROBE "|" D_CMND_ZIGBEE_READ "|" D_CMND_ZIGBEEZNPRECEIVE
;
void (* const ZigbeeCommand[])(void) PROGMEM = {
&CmndZigbeeZNPSend, &CmndZigbeePermitJoin,
&CmndZigbeeStatus, &CmndZigbeeReset, &CmndZigbeeSend,
&CmndZigbeeProbe, &CmndZigbeeRead };
&CmndZigbeeProbe, &CmndZigbeeRead, &CmndZigbeeZNPReceive
};
int32_t ZigbeeProcessInput(class SBuffer &buf) {
if (!zigbee.state_machine) { return -1; } // if state machine is stopped, send 'ignore' message
@ -268,7 +270,7 @@ void CmndZigbeeStatus(void) {
}
}
void CmndZigbeeZNPSend(void)
void CmndZigbeeZNPSendOrReceive(bool send)
{
if (ZigbeeSerial && (XdrvMailbox.data_len > 0)) {
uint8_t code;
@ -286,11 +288,26 @@ void CmndZigbeeZNPSend(void)
size -= 2;
codes += 2;
}
ZigbeeZNPSend(buf.getBuffer(), buf.len());
if (send) {
ZigbeeZNPSend(buf.getBuffer(), buf.len());
} else {
ZigbeeProcessInput(buf);
}
}
ResponseCmndDone();
}
// For debug purposes only, simulates a message received
void CmndZigbeeZNPReceive(void)
{
CmndZigbeeZNPSendOrReceive(false);
}
void CmndZigbeeZNPSend(void)
{
CmndZigbeeZNPSendOrReceive(true);
}
void ZigbeeZNPSend(const uint8_t *msg, size_t len) {
if ((len < 2) || (len > 252)) {
// abort, message cannot be less than 2 bytes for CMD1 and CMD2