/* xdrv_23_zigbee.ino - zigbee serial support for Sonoff-Tasmota Copyright (C) 2019 Theo Arends and Stephan Hadinger This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifdef USE_ZIGBEE #define XDRV_23 23 const uint32_t ZIGBEE_BUFFER_SIZE = 256; // Max ZNP frame is SOF+LEN+CMD1+CMD2+250+FCS = 255 const uint8_t ZIGBEE_SOF = 0xFE; // State machine states enum class ZnpStates { S_START = 0, S_READY }; // ZNP Constants taken from https://github.com/Frans-Willem/AqaraHub/blob/master/src/znp/znp.h enum class ZnpCommandType { POLL = 0, SREQ = 2, AREQ = 4, SRSP = 6 }; enum class ZnpSubsystem { RPC_Error = 0, SYS = 1, MAC = 2, NWK = 3, AF = 4, ZDO = 5, SAPI = 6, UTIL = 7, DEBUG = 8, APP = 9 }; enum class ZnpStatus : uint8_t { Success = 0x00, Failure = 0x01, InvalidParameter = 0x02, MemError = 0x03, BufferFull = 0x11 }; typedef uint64_t IEEEAddress; typedef uint16_t ShortAddress; enum class AddrMode : uint8_t { NotPresent = 0, Group = 1, ShortAddress = 2, IEEEAddress = 3, Broadcast = 0xFF }; // Commands in the SYS subsystem enum class SysCommand : uint8_t { RESET = 0x00, PING = 0x01, VERSION = 0x02, SET_EXTADDR = 0x03, GET_EXTADDR = 0x04, RAM_READ = 0x05, RAM_WRITE = 0x06, OSAL_NV_ITEM_INIT = 0x07, OSAL_NV_READ = 0x08, OSAL_NV_WRITE = 0x09, OSAL_START_TIMER = 0x0A, OSAL_STOP_TIMER = 0x0B, RANDOM = 0x0C, ADC_READ = 0x0D, GPIO = 0x0E, STACK_TUNE = 0x0F, SET_TIME = 0x10, GET_TIME = 0x11, OSAL_NV_DELETE = 0x12, OSAL_NV_LENGTH = 0x13, TEST_RF = 0x40, TEST_LOOPBACK = 0x41, RESET_IND = 0x80, OSAL_TIMER_EXPIRED = 0x81, }; // Commands in the AF subsystem enum class AfCommand : uint8_t { REGISTER = 0x00, DATA_REQUEST = 0x01, DATA_REQUEST_EXT = 0x02, DATA_REQUEST_SRC_RTG = 0x03, INTER_PAN_CTL = 0x10, DATA_STORE = 0x11, DATA_RETRIEVE = 0x12, APSF_CONFIG_SET = 0x13, DATA_CONFIRM = 0x80, REFLECT_ERROR = 0x83, INCOMING_MSG = 0x81, INCOMING_MSG_EXT = 0x82 }; // Commands in the ZDO subsystem enum class ZdoCommand : uint8_t { NWK_ADDR_REQ = 0x00, IEEE_ADDR_REQ = 0x01, NODE_DESC_REQ = 0x02, POWER_DESC_REQ = 0x03, SIMPLE_DESC_REQ = 0x04, ACTIVE_EP_REQ = 0x05, MATCH_DESC_REQ = 0x06, COMPLEX_DESC_REQ = 0x07, USER_DESC_REQ = 0x08, DEVICE_ANNCE = 0x0A, USER_DESC_SET = 0x0B, SERVER_DISC_REQ = 0x0C, END_DEVICE_BIND_REQ = 0x20, BIND_REQ = 0x21, UNBIND_REQ = 0x22, SET_LINK_KEY = 0x23, REMOVE_LINK_KEY = 0x24, GET_LINK_KEY = 0x25, MGMT_NWK_DISC_REQ = 0x30, MGMT_LQI_REQ = 0x31, MGMT_RTQ_REQ = 0x32, MGMT_BIND_REQ = 0x33, MGMT_LEAVE_REQ = 0x34, MGMT_DIRECT_JOIN_REQ = 0x35, MGMT_PERMIT_JOIN_REQ = 0x36, MGMT_NWK_UPDATE_REQ = 0x37, MSG_CB_REGISTER = 0x3E, MGS_CB_REMOVE = 0x3F, STARTUP_FROM_APP = 0x40, AUTO_FIND_DESTINATION = 0x41, EXT_REMOVE_GROUP = 0x47, EXT_REMOVE_ALL_GROUP = 0x48, EXT_FIND_ALL_GROUPS_ENDPOINT = 0x49, EXT_FIND_GROUP = 0x4A, EXT_ADD_GROUP = 0x4B, EXT_COUNT_ALL_GROUPS = 0x4C, NWK_ADDR_RSP = 0x80, IEEE_ADDR_RSP = 0x81, NODE_DESC_RSP = 0x82, POWER_DESC_RSP = 0x83, SIMPLE_DESC_RSP = 0x84, ACTIVE_EP_RSP = 0x85, MATCH_DESC_RSP = 0x86, COMPLEX_DESC_RSP = 0x87, USER_DESC_RSP = 0x88, USER_DESC_CONF = 0x89, SERVER_DISC_RSP = 0x8A, END_DEVICE_BIND_RSP = 0xA0, BIND_RSP = 0xA1, UNBIND_RSP = 0xA2, MGMT_NWK_DISC_RSP = 0xB0, MGMT_LQI_RSP = 0xB1, MGMT_RTG_RSP = 0xB2, MGMT_BIND_RSP = 0xB3, MGMT_LEAVE_RSP = 0xB4, MGMT_DIRECT_JOIN_RSP = 0xB5, MGMT_PERMIT_JOIN_RSP = 0xB6, STATE_CHANGE_IND = 0xC0, END_DEVICE_ANNCE_IND = 0xC1, MATCH_DESC_RSP_SENT = 0xC2, STATUS_ERROR_RSP = 0xC3, SRC_RTG_IND = 0xC4, LEAVE_IND = 0xC9, TC_DEV_IND = 0xCA, PERMIT_JOIN_IND = 0xCB, MSG_CB_INCOMING = 0xFF }; // Commands in the SAPI subsystem enum class SapiCommand : uint8_t { START_REQUEST = 0x00, BIND_DEVICE = 0x01, ALLOW_BIND = 0x02, SEND_DATA_REQUEST = 0x03, READ_CONFIGURATION = 0x04, WRITE_CONFIGURATION = 0x05, GET_DEVICE_INFO = 0x06, FIND_DEVICE_REQUEST = 0x07, PERMIT_JOINING_REQUEST = 0x08, SYSTEM_RESET = 0x09, START_CONFIRM = 0x80, BIND_CONFIRM = 0x81, ALLOW_BIND_CONFIRM = 0x82, SEND_DATA_CONFIRM = 0x83, FIND_DEVICE_CONFIRM = 0x85, RECEIVE_DATA_INDICATION = 0x87, }; // Commands in the UTIL subsystem enum class UtilCommand : uint8_t { GET_DEVICE_INFO = 0x00, GET_NV_INFO = 0x01, SET_PANID = 0x02, SET_CHANNELS = 0x03, SET_SECLEVEL = 0x04, SET_PRECFGKEY = 0x05, CALLBACK_SUB_CMD = 0x06, KEY_EVENT = 0x07, TIME_ALIVE = 0x09, LED_CONTROL = 0x0A, TEST_LOOPBACK = 0x10, DATA_REQ = 0x11, SRC_MATCH_ENABLE = 0x20, SRC_MATCH_ADD_ENTRY = 0x21, SRC_MATCH_DEL_ENTRY = 0x22, SRC_MATCH_CHECK_SRC_ADDR = 0x23, SRC_MATCH_ACK_ALL_PENDING = 0x24, SRC_MATCH_CHECK_ALL_PENDING = 0x25, ADDRMGR_EXT_ADDR_LOOKUP = 0x40, ADDRMGR_NWK_ADDR_LOOKUP = 0x41, APSME_LINK_KEY_DATA_GET = 0x44, APSME_LINK_KEY_NV_ID_GET = 0x45, ASSOC_COUNT = 0x48, ASSOC_FIND_DEVICE = 0x49, ASSOC_GET_WITH_ADDRESS = 0x4A, APSME_REQUEST_KEY_CMD = 0x4B, ZCL_KEY_EST_INIT_EST = 0x80, ZCL_KEY_EST_SIGN = 0x81, UTIL_SYNC_REQ = 0xE0, ZCL_KEY_ESTABLISH_IND = 0xE1 }; enum class Capability : uint16_t { SYS = 0x0001, MAC = 0x0002, NWK = 0x0004, AF = 0x0008, ZDO = 0x0010, SAPI = 0x0020, UTIL = 0x0040, DEBUG = 0x0080, APP = 0x0100, ZOAD = 0x1000 }; enum class ConfigurationOption : uint8_t { STARTUP_OPTION = 0x03, POLL_RATE = 0x24, QUEUED_POLL_RATE = 0x25, RESPONSE_POLL_RATE = 0x26, POLL_FAILURE_RETRIES = 0x29, INDIRECT_MSG_TIMEOUT = 0x2B, ROUTE_EXPIRY_TIME = 0x2C, EXTENDED_PAN_ID = 0x2D, BCAST_RETRIES = 0x2E, PASSIVE_ACK_TIMEOUT = 0x2F, BCAST_DELIVERY_TIME = 0x30, APS_FRAME_RETRIES = 0x43, APS_ACK_WAIT_DURATION = 0x44, BINDING_TIME = 0x46, PRECFGKEY = 0x62, PRECFGKEYS_ENABLE = 0x63, SECURITY_MODE = 0x64, USERDESC = 0x81, PANID = 0x83, CHANLIST = 0x84, LOGICAL_TYPE = 0x87, ZDO_DIRECT_CB = 0x8F }; #define D_JSON_ZIGBEEZNPRECEIVED "ZigbeeZNPReceived" #define D_PRFX_ZIGBEE "Zigbee" #define D_CMND_ZIGBEEZNPSEND "ZNPSend" const char kZigbeeCommands[] PROGMEM = D_PRFX_ZIGBEE "|" // Prefix D_CMND_ZIGBEEZNPSEND; void (* const ZigbeeCommand[])(void) PROGMEM = { &CmndZigbeeZNPSend }; #include TasmotaSerial *ZigbeeSerial = nullptr; unsigned long zigbee_polling_window = 0; uint8_t *zigbee_buffer = nullptr; uint32_t zigbee_in_byte_counter = 0; uint32_t zigbee_frame_len = 256; bool zigbee_active = true; bool zigbee_raw = false; void ZigbeeInput(void) { // Receive only valid ZNP frames: // 00 - SOF = 0xFE // 01 - Length of Data Field - 0..250 // 02 - CMD1 - first byte of command // 03 - CMD2 - second byte of command // 04..FD - Data Field // FE (or last) - FCS Checksum while (ZigbeeSerial->available()) { yield(); uint8_t zigbee_in_byte = ZigbeeSerial->read(); if ((0 == zigbee_in_byte_counter) && (ZIGBEE_SOF != zigbee_in_byte)) { // waiting for SOF (Start Of Frame) byte, discard anything else continue; // discard } if (zigbee_in_byte_counter < zigbee_frame_len) { zigbee_buffer[zigbee_in_byte_counter++] = zigbee_in_byte; zigbee_polling_window = millis(); // Wait for more data } else { zigbee_polling_window = 0; // Publish now break; } // recalculate frame length if (02 == zigbee_in_byte_counter) { // We just received the Lenght byte uint8_t len_byte = zigbee_buffer[1]; if (len_byte > 250) len_byte = 250; // ZNP spec says len is 250 max zigbee_frame_len = len_byte + 5; // SOF + LEN + CMD1 + CMD2 + FCS = 5 bytes overhead } } if (zigbee_in_byte_counter && (millis() > (zigbee_polling_window + ZIGBEE_POLLING))) { char hex_char[(zigbee_in_byte_counter * 2) + 2]; Response_P(PSTR("{\"" D_JSON_ZIGBEEZNPRECEIVED "\":\"%s\"}"), ToHex_P((unsigned char*)zigbee_buffer, zigbee_in_byte_counter, hex_char, sizeof(hex_char))); MqttPublishPrefixTopic_P(RESULT_OR_TELE, PSTR(D_JSON_ZIGBEEZNPRECEIVED)); XdrvRulesProcess(); zigbee_in_byte_counter = 0; zigbee_frame_len = 254; } } /********************************************************************************************/ void ZigbeeInit(void) { zigbee_active = false; if ((pin[GPIO_ZIGBEE_RX] < 99) && (pin[GPIO_ZIGBEE_TX] < 99)) { ZigbeeSerial = new TasmotaSerial(pin[GPIO_ZIGBEE_RX], pin[GPIO_ZIGBEE_TX]); if (ZigbeeSerial->begin(115200)) { // ZNP is 115200, RTS/CTS (ignored), 8N1 if (ZigbeeSerial->hardwareSerial()) { ClaimSerial(); zigbee_buffer = (uint8_t*) serial_in_buffer; // Use idle serial buffer to save RAM } else { zigbee_buffer = (uint8_t*) malloc(ZIGBEE_BUFFER_SIZE); } zigbee_active = true; ZigbeeSerial->flush(); } } } /*********************************************************************************************\ * Commands \*********************************************************************************************/ void CmndZigbeeZNPSend(void) { if (XdrvMailbox.data_len > 0) { uint8_t code; char *codes = RemoveSpace(XdrvMailbox.data); int32_t size = strlen(XdrvMailbox.data); while (size > 0) { char stemp[3]; strlcpy(stemp, codes, sizeof(stemp)); code = strtol(stemp, nullptr, 16); ZigbeeSerial->write(code); size -= 2; codes += 2; } } ResponseCmndDone(); } /*********************************************************************************************\ * Interface \*********************************************************************************************/ bool Xdrv23(uint8_t function) { bool result = false; if (zigbee_active) { switch (function) { case FUNC_LOOP: if (ZigbeeSerial) { ZigbeeInput(); } break; case FUNC_PRE_INIT: ZigbeeInit(); break; case FUNC_COMMAND: result = DecodeCommand(kZigbeeCommands, ZigbeeCommand); break; } } return result; } #endif // USE_ZIGBEE