/* xdrv_23_zigbee_9a_upload.ino - zigbee: serial xmodem upload to MCU Copyright (C) 2021 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 #ifdef USE_ZIGBEE_EZSP /*********************************************************************************************\ * MCU EFR32 firmware upload using xmodem * * Step 1 - Upload MCU firmware in ESP8266 flash free space (current size is about 200k) * Step 2 - Upload MCU firmware from ESP8266 flash to MCU EFR32 using XMODEM protocol * Step 3 - Restart \*********************************************************************************************/ //#define ZIGBEE_BOOTLOADER_SOFTWARE_RESET_FIRST #define XM_SOH 0x01 #define XM_EOT 0x04 #define XM_ACK 0x06 #define XM_CR 0x0d #define XM_NAK 0x15 #define XM_CAN 0x18 #define XM_SUB 0x1a enum ZbUploadSteps { ZBU_IDLE, ZBU_INIT, ZBU_SOFTWARE_RESET, ZBU_SOFTWARE_SEND, ZBU_HARDWARE_RESET, ZBU_PROMPT, ZBU_SYNC, ZBU_UPLOAD, ZBU_EOT, ZBU_COMPLETE, ZBU_DONE, ZBU_ERROR, ZBU_FINISH }; const uint8_t PIN_ZIGBEE_DEFAULT_BOOTLOADER = 5; // default pin for Sonoff ZBBridge struct ZBUPLOAD { uint32_t ota_size = 0; int32_t sector_base = -1; int32_t sector_counter = -1; uint32_t byte_counter = 0; char *buffer = nullptr; uint8_t ota_step = ZBU_IDLE; uint8_t bootloader = 0; uint8_t state = ZBU_IDLE; } ZbUpload; /*********************************************************************************************\ * Flash from ESP8266 to EZSP \*********************************************************************************************/ uint32_t ZigbeeUploadAvailable(void) { int available = ZbUpload.ota_size - ZbUpload.byte_counter; if (available < 0) { available = 0; } return available; } char ZigbeeUploadFlashRead(void) { if (nullptr == ZbUpload.buffer) { if (!(ZbUpload.buffer = (char *)malloc(SPI_FLASH_SEC_SIZE))) { return -1; // Not enough (memory) space } } uint32_t index = ZbUpload.byte_counter % SPI_FLASH_SEC_SIZE; int32_t sector = ZbUpload.sector_base + ZbUpload.byte_counter / SPI_FLASH_SEC_SIZE; if (sector != ZbUpload.sector_counter) { ZbUpload.sector_counter = sector; ESP.flashRead(ZbUpload.sector_counter * SPI_FLASH_SEC_SIZE, (uint32_t*)ZbUpload.buffer, SPI_FLASH_SEC_SIZE); // AddLog(LOG_LEVEL_DEBUG, "= sector %d %*_H", ZbUpload.sector_counter, 256, ZbUpload.buffer); } char data = ZbUpload.buffer[index]; ZbUpload.byte_counter++; if (ZbUpload.byte_counter > ZbUpload.ota_size) { // static bool padding = true; // if (padding) { // AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Start padding from %d"), ZbUpload.byte_counter); // padding = false; // } // When the source device reaches the last XModem data block, it should be padded to 128 bytes // of data using SUB (ASCII 0x1A) characters. data = XM_SUB; } return data; } /*********************************************************************************************\ * XModem protocol \*********************************************************************************************/ // Number of milliseconds to wait before prompt is received const uint32_t XMODEM_FLUSH_DELAY = 1000; // Number of seconds until giving up hope of receiving sync packets from host. const uint8_t XMODEM_SYNC_TIMEOUT = 30; // Number of times we try to send a packet to the host until we give up sending.. const uint8_t XMODEM_MAX_RETRY = 30; // Packet size const uint8_t XMODEM_PACKET_SIZE = 128; struct XMODEM { uint32_t timeout = 0; uint32_t delay = 0; uint32_t flush_delay = 0xFFFFFFFF; uint32_t filepos = 0; uint32_t packet_no = 1; int crcBuf = 0; uint8_t checksumBuf = 0; bool oldChecksum; } XModem; // Send out a byte of payload data, includes checksumming void XModemOutputByte(uint8_t out_char) { XModem.checksumBuf += out_char; XModem.crcBuf = XModem.crcBuf ^ (int) out_char << 8; for (uint32_t i = 0; i < 8; i++) { if (XModem.crcBuf & 0x8000) { XModem.crcBuf = XModem.crcBuf << 1 ^ 0x1021; } else { XModem.crcBuf = XModem.crcBuf << 1; } } ZigbeeSerial->write(out_char); } // Wait for the remote to acknowledge or cancel. // Returns the received char if no timeout occured or a CAN was received. In this cases, it returns -1. char XModemWaitACK(void) { char in_char; do { uint8_t i = 0; while (!ZigbeeSerial->available()) { delayMicroseconds(100); i++; if (i > 4000) { return -1; } } in_char = ZigbeeSerial->read(); // if (in_char != XM_ACK) { // AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("XMD: Rcvd3 0x%02X"), in_char); // } if (XM_CAN == in_char) { return XM_CAN; } } while ((in_char != XM_NAK) && (in_char != XM_ACK) && (in_char != 'C')); return in_char; } bool XModemSendPacket(uint32_t packet_no) { XModem.filepos = ZbUpload.byte_counter; // Sending a packet will be retried uint32_t retries = 0; char in_char; do { // Seek to start of current data block, // will advance through the file as block will be acked.. ZbUpload.byte_counter = XModem.filepos; // Reset checksum stuff XModem.checksumBuf = 0x00; XModem.crcBuf = 0x00; uint8_t packet_num = packet_no; // AddLog(LOG_LEVEL_DEBUG, "++ Packet %d, retries %d, counter %d", packet_no, retries, ZbUpload.byte_counter); // Try to send packet, so header first ZigbeeSerial->write(XM_SOH); ZigbeeSerial->write(packet_num); ZigbeeSerial->write(~packet_num); for (uint32_t i = 0; i < XMODEM_PACKET_SIZE; i++) { in_char = ZigbeeUploadFlashRead(); XModemOutputByte(in_char); } // Send out checksum, either CRC-16 CCITT or classical inverse of sum of bytes. // Depending on how the received introduced himself if (XModem.oldChecksum) { ZigbeeSerial->write((char)XModem.checksumBuf); } else { ZigbeeSerial->write((char)(XModem.crcBuf >> 8)); ZigbeeSerial->write((char)(XModem.crcBuf & 0xFF)); } in_char = XModemWaitACK(); if (XM_CAN == in_char) { return false; } retries++; if (retries > XMODEM_MAX_RETRY) { return false; } } while (in_char != XM_ACK); return true; } /*********************************************************************************************\ * Step 2 - Upload MCU firmware from ESP8266 flash to MCU EFR32 using XMODEM protocol * * https://www.silabs.com/documents/public/application-notes/an760-using-legacy-standalone-bootloader.pdf \*********************************************************************************************/ void ZigbeeUploadSetSoftwareBootloader() { // https://github.com/arendst/Tasmota/issues/8583#issuecomment-663967883 SBuffer buf(4); buf.add16(EZSP_launchStandaloneBootloader); buf.add8(0x01); ZigbeeEZSPSendCmd(buf.getBuffer(), buf.len()); // Send software bootloader init } void ZigbeeUploadSetBootloader(uint8_t state) { int32_t pin_bootloader = Pin(GPIO_ZIGBEE_RST, 1); if (pin_bootloader < 0) { pin_bootloader = PIN_ZIGBEE_DEFAULT_BOOTLOADER; } AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_ZIGBEE "Bootloader pin %d"), pin_bootloader); pinMode(pin_bootloader, OUTPUT); digitalWrite(pin_bootloader, state); // Toggle Gecko bootloader digitalWrite(Pin(GPIO_ZIGBEE_RST), 0); delay(100); // Need to experiment to find a value as low as possible digitalWrite(Pin(GPIO_ZIGBEE_RST), 1); // Reboot MCU EFR32 } bool ZigbeeUploadBootloaderPrompt(void) { // Scripts that interact with the bootloader should use only the “BL >” prompt to determine // when the bootloader is ready for input. While current menu options should remain functionally // unchanged, the menu title and options text is liable to change, and new options might be added. uint8_t serial_buffer[255]; uint32_t buf_len = 0; while (ZigbeeSerial->available()) { yield(); char bootloader_byte = ZigbeeSerial->read(); // [cr][lf] // Gecko Bootloader v1.A.3 or Gecko Bootloader v1.9.1.04[cr][lf] // 1. upload gbl[cr][lf] // 2. run[cr][lf] // 3. ebl info[cr][lf] // BL > if (((uint8_t)bootloader_byte >=0) && (buf_len < sizeof(serial_buffer) -2)) { serial_buffer[buf_len++] = bootloader_byte; } if (ZbUpload.byte_counter != 4) { switch (ZbUpload.byte_counter) { case 0: if ('B' == bootloader_byte) { ZbUpload.byte_counter++; } break; case 1: if ('L' == bootloader_byte) { ZbUpload.byte_counter++; } break; case 2: if (' ' == bootloader_byte) { ZbUpload.byte_counter++; } break; case 3: if ('>' == bootloader_byte) { ZbUpload.byte_counter++; XModem.flush_delay = millis() + XMODEM_FLUSH_DELAY; XModem.delay = XModem.flush_delay + XMODEM_FLUSH_DELAY; } } } } if (buf_len) { AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("XMD: Rcvd %*_H"), buf_len, serial_buffer); } return ((4 == ZbUpload.byte_counter) && (millis() > XModem.flush_delay)); } bool ZigbeeUploadXmodem(void) { switch (ZbUpload.ota_step) { case ZBU_IDLE: { // *** Upload disabled return false; } #ifdef ZIGBEE_BOOTLOADER_SOFTWARE_RESET_FIRST case ZBU_INIT: { // *** Init ESF32 bootloader AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Init bootloader")); ZbUpload.ota_step = ZBU_SOFTWARE_RESET; return false; // Keep Zigbee serial active } case ZBU_SOFTWARE_RESET: { SBuffer buf(4); buf.add16(EZSP_launchStandaloneBootloader); buf.add8(0x01); ZigbeeEZSPSendCmd(buf.getBuffer(), buf.len()); // Send software bootloader init XModem.timeout = millis() + (10 * 1000); // Allow 10 seconds to send Zigbee command ZbUpload.ota_step = ZBU_SOFTWARE_SEND; return false; // Keep Zigbee serial active } case ZBU_SOFTWARE_SEND: { if (millis() > XModem.timeout) { AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Bootloader software reset send timeout")); ZbUpload.ota_step = ZBU_HARDWARE_RESET; return true; } if (EZSP_Serial.to_send == EZSP_Serial.to_end) { ZbUpload.bootloader = ZBU_SOFTWARE_RESET; XModem.timeout = millis() + (10 * 1000); // Allow 10 seconds to receive EBL prompt XModem.delay = millis() + (2 * XMODEM_FLUSH_DELAY); ZbUpload.byte_counter = 0; ZbUpload.sector_counter = -1; ZbUpload.ota_step = ZBU_PROMPT; } break; } case ZBU_HARDWARE_RESET: { ZbUpload.bootloader = ZBU_HARDWARE_RESET; ZigbeeUploadSetBootloader(0); // Reboot MCU EFR32 which returns below text XModem.timeout = millis() + (30 * 1000); // Allow 30 seconds to receive EBL prompt XModem.delay = millis() + (2 * XMODEM_FLUSH_DELAY); ZbUpload.byte_counter = 0; ZbUpload.sector_counter = -1; ZbUpload.ota_step = ZBU_PROMPT; break; } case ZBU_PROMPT: { // *** Wait for prompt and select option upload ebl if (millis() > XModem.timeout) { if (ZBU_SOFTWARE_RESET == ZbUpload.bootloader) { AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Bootloader software reset timeout")); ZbUpload.ota_step = ZBU_HARDWARE_RESET; } else { AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Bootloader hardware reset timeout")); ZbUpload.ota_step = ZBU_ERROR; } return true; } #else // No ZIGBEE_BOOTLOADER_SOFTWARE_RESET_FIRST case ZBU_INIT: { // *** Init ESF32 bootloader AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Init bootloader")); ZigbeeUploadSetBootloader(0); // Reboot MCU EFR32 which returns below text XModem.timeout = millis() + (30 * 1000); // Allow 30 seconds to receive EBL prompt XModem.delay = millis() + (2 * XMODEM_FLUSH_DELAY); ZbUpload.byte_counter = 0; ZbUpload.sector_counter = -1; ZbUpload.ota_step = ZBU_PROMPT; break; } case ZBU_PROMPT: { // *** Wait for prompt and select option upload ebl if (millis() > XModem.timeout) { AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Bootloader timeout")); ZbUpload.ota_step = ZBU_ERROR; return true; } #endif // ZIGBEE_BOOTLOADER_SOFTWARE_RESET_FIRST else if (!ZigbeeSerial->available() && (millis() < XModem.flush_delay)) { // The target device’s bootloader sends output over its serial port after it receives a // carriage return from the source device if (millis() > XModem.delay) { ZigbeeSerial->write(XM_CR); XModem.delay = millis() + (2 * XMODEM_FLUSH_DELAY); } } else { // After the bootloader receives a carriage return from the target device, it displays a menu // [cr][lf] // Gecko Bootloader v1.A.3 or Gecko Bootloader v1.9.1.04[cr][lf] // 1. upload gbl[cr][lf] // 2. run[cr][lf] // 3. ebl info[cr][lf] // BL > if (ZigbeeUploadBootloaderPrompt()) { AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Init sync")); ZigbeeSerial->flush(); ZigbeeSerial->write('1'); // upload ebl if (TasmotaGlobal.sleep > 0) { TasmotaGlobal.sleep = 1; // Speed up loop used for xmodem upload } XModem.timeout = millis() + (XMODEM_SYNC_TIMEOUT * 1000); ZbUpload.ota_step = ZBU_SYNC; } } break; } case ZBU_SYNC: { // *** Handle file upload using XModem - sync if (millis() > XModem.timeout) { AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: SYNC timeout")); ZbUpload.ota_step = ZBU_ERROR; return true; } // Wait for either C or NACK as a sync packet. Determines protocol details, checksum algorithm. if (ZigbeeSerial->available()) { // [cr][lf] // begin upload[cr][lf] // C char xmodem_sync = ZigbeeSerial->read(); // AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("XMD: Rcvd2 0x%02X"), xmodem_sync); if (('C' == xmodem_sync) || (XM_NAK == xmodem_sync)) { // Determine which checksum algorithm to use XModem.oldChecksum = (xmodem_sync == XM_NAK); XModem.packet_no = 1; ZbUpload.byte_counter = 0; ZbUpload.sector_counter = -1; ZbUpload.ota_step = ZBU_UPLOAD; AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Init packet send")); delay(100); } } break; } case ZBU_UPLOAD: { // *** Handle file upload using XModem - upload if (ZigbeeUploadAvailable()) { if (ZbUpload.byte_counter && !(ZbUpload.byte_counter % 10240)) { // Show progress every 10kB AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Progress %d kB"), ZbUpload.byte_counter / 1024); } if (!XModemSendPacket(XModem.packet_no)) { AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Packet %d send failed"), XModem.packet_no); ZbUpload.ota_step = ZBU_ERROR; return true; } XModem.packet_no++; } else { // Once the last block is ACKed by the target, the transfer should be finalized by an // EOT (ASCII 0x04) packet from the source. Once this packet is confirmed via XModem ACK // from the target, the device will reboot, causing the new firmware to be launched. ZigbeeSerial->write(XM_EOT); XModem.timeout = millis() + (30 * 1000); // Allow 30 seconds to receive EOT ACK ZbUpload.ota_step = ZBU_EOT; AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Transferred %d bytes"), ZbUpload.ota_size); } break; } case ZBU_EOT: { // *** Send EOT and wait for ACK // The ACK for the last XModem data packet may take much longer (1-3 seconds) than prior // data packets to be received. This is due to the CRC32 checksum being performed across // the received EBL file data prior to sending the ACK. The source device must ensure that // its XModem state machine waits a sufficient amount of time to allow this checksum process // to occur without timing out on the response just before the EOT is sent. if (millis() > XModem.timeout) { AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: EOT ACK timeout")); ZbUpload.ota_step = ZBU_ERROR; return true; } if (ZigbeeSerial->available()) { char xmodem_ack = XModemWaitACK(); if (XM_CAN == xmodem_ack) { AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Transfer invalid")); ZbUpload.ota_step = ZBU_ERROR; return true; } else if (XM_ACK == xmodem_ack) { AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: " D_SUCCESSFUL)); XModem.timeout = millis() + (30 * 1000); // Allow 30 seconds to receive EBL prompt ZbUpload.byte_counter = 0; ZbUpload.sector_counter = -1; ZbUpload.ota_step = ZBU_COMPLETE; } } break; } case ZBU_COMPLETE: { // *** Wait for Serial upload complete EBL prompt if (millis() > XModem.timeout) { AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Bootloader timeout")); ZbUpload.ota_step = ZBU_ERROR; return true; } else { // After an image successfully uploads, the XModem transaction completes and the bootloader displays // ‘Serial upload complete’ before redisplaying the menu // // [cr][lf] // Serial upload complete[cr][lf] // [cr][lf] // Gecko Bootloader v1.A.3 or Gecko Bootloader v1.9.1.04[cr][lf] // 1. upload gbl[cr][lf] // 2. run[cr][lf] // 3. ebl info[cr][lf] // BL > if (ZigbeeUploadBootloaderPrompt()) { ZbUpload.state = ZBU_COMPLETE; ZbUpload.ota_step = ZBU_DONE; AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: " D_RESTARTING)); } } break; } case ZBU_ERROR: ZbUpload.state = ZBU_ERROR; AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: " D_FAILED)); case ZBU_DONE: { // *** Clean up and restart to disable bootloader and use new firmware ZigbeeUploadSetBootloader(1); // Disable bootloader and reset MCU - should happen at restart if (1 == TasmotaGlobal.sleep) { TasmotaGlobal.sleep = Settings->sleep; // Restore loop sleep } // TasmotaGlobal.restart_flag = 2; // Restart to disable bootloader and use new firmware if (ZbUpload.buffer) { free(ZbUpload.buffer); ZbUpload.buffer = nullptr; } ZbUpload.ota_step = ZBU_FINISH; // Never return to zero without a restart to get a sane Zigbee environment break; } case ZBU_FINISH: { // *** Wait for restart making sure not to start Zigbee serial again // Wait for restart break; } } return true; } /*********************************************************************************************\ * Step 1 - Upload MCU firmware in ESP8266 flash free space (current size is about 200k) \*********************************************************************************************/ #ifdef USE_WEBSERVER uint8_t ZigbeeUploadStep1Init(void) { if (!PinUsed(GPIO_ZIGBEE_RST) && (ZigbeeSerial == nullptr)) { return 1; } // Wrong pin configuration - No file selected ZbUpload.ota_step = ZBU_IDLE; ZbUpload.state = ZBU_IDLE; return 0; } void ZigbeeUploadStep1Done(uint32_t data, size_t size) { ZbUpload.sector_base = data; ZbUpload.sector_counter = -1; ZbUpload.ota_size = size; ZbUpload.ota_step = ZBU_INIT; ZbUpload.state = ZBU_UPLOAD; // Signal upload done and ready for delayed upload to MCU EFR32 } #define WEB_HANDLE_ZIGBEE_XFER "zx" const char HTTP_SCRIPT_XFER_STATE[] PROGMEM = "function z9(){" "if(x!=null){x.abort();}" // Abort if no response within 2 seconds (happens on restart 1) "x=new XMLHttpRequest();" "x.onreadystatechange=()=>{" "if(x.readyState==4&&x.status==200){" "var s=x.responseText;" "if(s!=7){" // ZBU_UPLOAD "location.href='/u3';" // Load page HandleUploadDone() "}" "}" "};" "x.open('GET','" WEB_HANDLE_ZIGBEE_XFER "?z=1',true);" // ?z related to Webserver->hasArg("z") "x.send();" "lt=setTimeout(z9,950);" // Poll every 0.95 second "}" "wl(z9);"; // Execute z9() on page load void HandleZigbeeXfer(void) { if (!HttpCheckPriviledgedAccess()) { return; } if (Webserver->hasArg("z")) { // Status refresh requested if (ZBU_ERROR == ZbUpload.state) { Web.upload_error = 7; // Upload aborted (xmodem transfer failed) } WSContentBegin(200, CT_PLAIN); WSContentSend_P(PSTR("%d"), ZbUpload.state); WSContentEnd(); return; } AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_HTTP D_UPLOAD_TRANSFER)); WSContentStart_P(PSTR(D_INFORMATION)); WSContentSend_P(HTTP_SCRIPT_XFER_STATE); WSContentSendStyle(); WSContentSend_P(PSTR("
" D_UPLOAD_TRANSFER " ...
")); WSContentSpaceButton(BUTTON_MAIN); WSContentStop(); } #endif // USE_WEBSERVER #endif // USE_ZIGBEE_EZSP #endif // USE_ZIGBEE