/* xdrv_23_zigbee.ino - zigbee 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; //#define Z_USE_SOFTWARE_SERIAL #ifdef Z_USE_SOFTWARE_SERIAL #include SoftwareSerial *ZigbeeSerial = nullptr; #else #include TasmotaSerial *ZigbeeSerial = nullptr; #endif const char kZigbeeCommands[] PROGMEM = "|" D_CMND_ZIGBEEZNPSEND "|" D_CMND_ZIGBEE_PERMITJOIN "|" D_CMND_ZIGBEE_STATUS; void (* const ZigbeeCommand[])(void) PROGMEM = { &CmndZigbeeZNPSend, &CmndZigbeePermitJoin, &CmndZigbeeStatus }; int32_t ZigbeeProcessInput(class SBuffer &buf) { if (!zigbee.state_machine) { return -1; } // if state machine is stopped, send 'ignore' message // apply the receive filter, acts as 'startsWith()' bool recv_filter_match = true; bool recv_prefix_match = false; // do the first 2 bytes match the response if ((zigbee.recv_filter) && (zigbee.recv_filter_len > 0)) { if (zigbee.recv_filter_len >= 2) { recv_prefix_match = false; if ( (pgm_read_byte(&zigbee.recv_filter[0]) == buf.get8(0)) && (pgm_read_byte(&zigbee.recv_filter[1]) == buf.get8(1)) ) { recv_prefix_match = true; } } for (uint32_t i = 0; i < zigbee.recv_filter_len; i++) { if (pgm_read_byte(&zigbee.recv_filter[i]) != buf.get8(i)) { recv_filter_match = false; break; } } AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZIG: ZigbeeProcessInput: recv_prefix_match = %d, recv_filter_match = %d"), recv_prefix_match, recv_filter_match); } // if there is a recv_callback, call it now int32_t res = -1; // default to ok // res = 0 - proceed to next state // res > 0 - proceed to the specified state // res = -1 - silently ignore the message // res <= -2 - move to error state // pre-compute the suggested value if ((zigbee.recv_filter) && (zigbee.recv_filter_len > 0)) { if (!recv_prefix_match) { res = -1; // ignore } else { // recv_prefix_match if (recv_filter_match) { res = 0; // ok } else { if (zigbee.recv_until) { res = -1; // ignore until full match } else { res = -2; // error, because message is expected but wrong value } } } } else { // we don't have any filter, ignore message by default res = -1; } if (recv_prefix_match) { if (zigbee.recv_func) { res = (*zigbee.recv_func)(res, buf); } } if (-1 == res) { // if frame was ignored up to now if (zigbee.recv_unexpected) { res = (*zigbee.recv_unexpected)(res, buf); } } AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZIG: ZigbeeProcessInput: res = %d"), res); // change state accordingly if (0 == res) { // if ok, continue execution zigbee.state_waiting = false; } else if (res > 0) { ZigbeeGotoLabel(res); // if >0 then go to specified label } else if (-1 == res) { // -1 means ignore message // just do nothing } else { // any other negative value means error ZigbeeGotoLabel(zigbee.on_error_goto); } } void ZigbeeInput(void) { static uint32_t zigbee_polling_window = 0; static uint8_t fcs = ZIGBEE_SOF; static uint32_t zigbee_frame_len = 5; // minimal zigbee frame lenght, will be updated when buf[1] is read // 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(); AddLog_P2(LOG_LEVEL_DEBUG_MORE, PSTR("ZigbeeInput byte=%d len=%d"), zigbee_in_byte, zigbee_buffer->len()); if (0 == zigbee_buffer->len()) { // make sure all variables are correctly initialized zigbee_frame_len = 5; fcs = ZIGBEE_SOF; } if ((0 == zigbee_buffer->len()) && (ZIGBEE_SOF != zigbee_in_byte)) { // waiting for SOF (Start Of Frame) byte, discard anything else AddLog_P2(LOG_LEVEL_INFO, PSTR("ZigbeeInput discarding byte %02X"), zigbee_in_byte); continue; // discard } if (zigbee_buffer->len() < zigbee_frame_len) { zigbee_buffer->add8(zigbee_in_byte); zigbee_polling_window = millis(); // Wait for more data fcs ^= zigbee_in_byte; } if (zigbee_buffer->len() >= zigbee_frame_len) { zigbee_polling_window = 0; // Publish now break; } // recalculate frame length if (02 == zigbee_buffer->len()) { // We just received the Lenght byte uint8_t len_byte = zigbee_buffer->get8(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_buffer->len() && (millis() > (zigbee_polling_window + ZIGBEE_POLLING))) { char hex_char[(zigbee_buffer->len() * 2) + 2]; ToHex_P((unsigned char*)zigbee_buffer->getBuffer(), zigbee_buffer->len(), hex_char, sizeof(hex_char)); #ifndef Z_USE_SOFTWARE_SERIAL AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZIG: Bytes follor_read_metric = %0d"), ZigbeeSerial->getLoopReadMetric()); #endif // buffer received, now check integrity if (zigbee_buffer->len() != zigbee_frame_len) { // Len is not correct, log and reject frame AddLog_P2(LOG_LEVEL_INFO, PSTR(D_JSON_ZIGBEEZNPRECEIVED ": received frame of wrong size %s, len %d, expected %d"), hex_char, zigbee_buffer->len(), zigbee_frame_len); } else if (0x00 != fcs) { // FCS is wrong, packet is corrupt, log and reject frame AddLog_P2(LOG_LEVEL_INFO, PSTR(D_JSON_ZIGBEEZNPRECEIVED ": received bad FCS frame %s, %d"), hex_char, fcs); } else { // frame is correct AddLog_P2(LOG_LEVEL_DEBUG, PSTR(D_JSON_ZIGBEEZNPRECEIVED ": received correct frame %s"), hex_char); SBuffer znp_buffer = zigbee_buffer->subBuffer(2, zigbee_frame_len - 3); // remove SOF, LEN and FCS ToHex_P((unsigned char*)znp_buffer.getBuffer(), znp_buffer.len(), hex_char, sizeof(hex_char)); Response_P(PSTR("{\"" D_JSON_ZIGBEEZNPRECEIVED "\":\"%s\"}"), hex_char); MqttPublishPrefixTopic_P(RESULT_OR_TELE, PSTR(D_JSON_ZIGBEEZNPRECEIVED)); XdrvRulesProcess(); // now process the message ZigbeeProcessInput(znp_buffer); } zigbee_buffer->setLen(0); // empty buffer } } /********************************************************************************************/ void ZigbeeInit(void) { zigbee.active = false; if ((pin[GPIO_ZIGBEE_RX] < 99) && (pin[GPIO_ZIGBEE_TX] < 99)) { AddLog_P2(LOG_LEVEL_DEBUG_MORE, PSTR("Zigbee: GPIOs Rx:%d Tx:%d"), pin[GPIO_ZIGBEE_RX], pin[GPIO_ZIGBEE_TX]); #ifdef Z_USE_SOFTWARE_SERIAL ZigbeeSerial = new SoftwareSerial(); ZigbeeSerial->begin(115200, pin[GPIO_ZIGBEE_RX], pin[GPIO_ZIGBEE_TX], SWSERIAL_8N1, false, 256); // ZNP is 115200, RTS/CTS (ignored), 8N1 ZigbeeSerial->enableIntTx(false); zigbee_buffer = new SBuffer(ZIGBEE_BUFFER_SIZE); #else ZigbeeSerial = new TasmotaSerial(pin[GPIO_ZIGBEE_RX], pin[GPIO_ZIGBEE_TX], 0, 0, 256); // set a receive buffer of 256 bytes ZigbeeSerial->begin(115200); if (ZigbeeSerial->hardwareSerial()) { ClaimSerial(); zigbee_buffer = new PreAllocatedSBuffer(sizeof(serial_in_buffer), serial_in_buffer); } else { zigbee_buffer = new SBuffer(ZIGBEE_BUFFER_SIZE); } #endif zigbee.active = true; zigbee.init_phase = true; // start the state machine zigbee.state_machine = true; // start the state machine ZigbeeSerial->flush(); } } /*********************************************************************************************\ * Commands \*********************************************************************************************/ void CmndZigbeeStatus(void) { if (ZigbeeSerial) { String dump = zigbee_devices.dump(XdrvMailbox.payload); Response_P(PSTR("{\"%s%d\":%s}"), XdrvMailbox.command, XdrvMailbox.payload, dump.c_str()); } } void CmndZigbeeZNPSend(void) { if (ZigbeeSerial && (XdrvMailbox.data_len > 0)) { uint8_t code; char *codes = RemoveSpace(XdrvMailbox.data); int32_t size = strlen(XdrvMailbox.data); SBuffer buf((size+1)/2); while (size > 0) { char stemp[3]; strlcpy(stemp, codes, sizeof(stemp)); code = strtol(stemp, nullptr, 16); buf.add8(code); size -= 2; codes += 2; } ZigbeeZNPSend(buf.getBuffer(), buf.len()); } ResponseCmndDone(); } 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 AddLog_P2(LOG_LEVEL_DEBUG, PSTR(D_JSON_ZIGBEEZNPSENT ": bad message len %d"), len); return; } uint8_t data_len = len - 2; // removing CMD1 and CMD2 if (ZigbeeSerial) { uint8_t fcs = data_len; ZigbeeSerial->write(ZIGBEE_SOF); // 0xFE AddLog_P2(LOG_LEVEL_DEBUG_MORE, PSTR("ZNPSend SOF %02X"), ZIGBEE_SOF); ZigbeeSerial->write(data_len); AddLog_P2(LOG_LEVEL_DEBUG_MORE, PSTR("ZNPSend LEN %02X"), data_len); for (uint32_t i = 0; i < len; i++) { uint8_t b = pgm_read_byte(msg + i); ZigbeeSerial->write(b); fcs ^= b; AddLog_P2(LOG_LEVEL_DEBUG_MORE, PSTR("ZNPSend byt %02X"), b); } ZigbeeSerial->write(fcs); // finally send fcs checksum byte AddLog_P2(LOG_LEVEL_DEBUG_MORE, PSTR("ZNPSend FCS %02X"), fcs); } // Now send a MQTT message to report the sent message char hex_char[(len * 2) + 2]; Response_P(PSTR("{\"" D_JSON_ZIGBEEZNPSENT "\":\"%s\"}"), ToHex_P(msg, len, hex_char, sizeof(hex_char))); MqttPublishPrefixTopic_P(RESULT_OR_TELE, PSTR(D_JSON_ZIGBEEZNPSENT)); XdrvRulesProcess(); } // Allow or Deny pairing of new Zigbee devices void CmndZigbeePermitJoin(void) { uint32_t payload = XdrvMailbox.payload; if (payload < 0) { payload = 0; } if ((99 != payload) && (payload > 1)) { payload = 1; } if (1 == payload) { ZigbeeGotoLabel(ZIGBEE_LABEL_PERMIT_JOIN_OPEN_60); } else if (99 == payload){ ZigbeeGotoLabel(ZIGBEE_LABEL_PERMIT_JOIN_OPEN_XX); } else { ZigbeeGotoLabel(ZIGBEE_LABEL_PERMIT_JOIN_CLOSE); } ResponseCmndDone(); } /*********************************************************************************************\ * Interface \*********************************************************************************************/ bool Xdrv23(uint8_t function) { bool result = false; if (zigbee.active) { switch (function) { case FUNC_LOOP: if (ZigbeeSerial) { ZigbeeInput(); } if (zigbee.state_machine) { //ZigbeeStateMachine(); ZigbeeStateMachine_Run(); } break; case FUNC_PRE_INIT: ZigbeeInit(); break; case FUNC_COMMAND: result = DecodeCommand(kZigbeeCommands, ZigbeeCommand); break; } } return result; } #endif // USE_ZIGBEE