/*
  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 <http://www.gnu.org/licenses/>.
*/

#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("<div style='text-align:center;'><b>" D_UPLOAD_TRANSFER " ...</b></div>"));
  WSContentSpaceButton(BUTTON_MAIN);
  WSContentStop();
}

#endif  // USE_WEBSERVER

#endif  // USE_ZIGBEE_EZSP

#endif  // USE_ZIGBEE
