diff --git a/BUILDS.md b/BUILDS.md index 01290816c..6eb2e134c 100644 --- a/BUILDS.md +++ b/BUILDS.md @@ -20,6 +20,7 @@ Note: the `minimal` variant is not listed as it shouldn't be used outside of the | USE_TELEGRAM | - | - / - | - | - | - | - | | USE_KNX | - | - / x | x | - | - | - | | USE_TELNET | - | - / - | - | - | - | - | +| USE_XYZMODEM | - | - / - | - | - | - | - | | USE_WEBSERVER | x | x / x | x | x | x | x | | USE_WEBSEND_RESPONSE | - | - / - | - | - | - | - | | USE_EMULATION_HUE | x | x / x | - | x | - | - | diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eee02a04..002b208ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. - Berry `bytes.add()` now accepts 3-bytes values (#23200) - Berry expose `esp_http_server` for websockets (#23206) - AlpineJS 2.8.2 - optional for now (#23259) +- Support for XMODEM over serial and telnet if enabled with `#define USE_XYZMODEM` ### Breaking Changed - HASPmota added `y2_min` and `y2_max` to control the second series of `chart` (#23287) @@ -28,7 +29,6 @@ All notable changes to this project will be documented in this file. ### Removed - ## [14.5.0.2] 20250325 ### Added - Berry load `.tapp` files in `/.extensions/` then in `/` (#23113) @@ -36,7 +36,7 @@ All notable changes to this project will be documented in this file. - Support for HLK-LD2402 24GHz smart wave motion sensor (#23133) - Matter prepare for ICD cluster (#23158) - Berry `re.dump()` (#23162) -- Telnet server using command `Telnet <0|1|port>[,]` +- Support for Telnet server using command `Telnet <0|1|port>[,]` if enabled with `#define USE_TELNET` - HASPmota autostart when `pages.jsonl` exists (#23181) ### Breaking Changed diff --git a/CODE_OWNERS.md b/CODE_OWNERS.md index f75deeeeb..0732e5f25 100644 --- a/CODE_OWNERS.md +++ b/CODE_OWNERS.md @@ -106,6 +106,7 @@ In addition to @arendst the following code is mainly owned by: | xdrv_93_ | | xdrv_94_ | | | +| xdrv_120_xyzmodem | @arendst | xdrv_121_gpioviewer | @arendst | xdrv_122_file_settings_demo | @arendst | xdrv_122_file_json_settings_demo | @arendst diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 885ecf44d..a3d8edd7f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -119,7 +119,8 @@ The latter links can be used for OTA upgrades too like ``OtaUrl https://ota.tasm - Extend command `GPIO` with different display options and allowing updating of module GPIO's in one go - Support Vango Technologies V924x ultralow power, single-phase, power measurement [#23127](https://github.com/arendst/Tasmota/issues/23127) - Support for HLK-LD2402 24GHz smart wave motion sensor [#23133](https://github.com/arendst/Tasmota/issues/23133) -- Telnet server using command `Telnet <0|1|port>[,]` +- Support for Telnet server using command `Telnet <0|1|port>[,]` if enabled with `#define USE_TELNET` +- Support for XMODEM over serial and telnet if enabled with `#define USE_XYZMODEM` - Allow acl in mqtt when client certificate is in use with `#define USE_MQTT_CLIENT_CERT` [#22998](https://github.com/arendst/Tasmota/issues/22998) - AlpineJS 2.8.2 - optional for now [#23259](https://github.com/arendst/Tasmota/issues/23259) - Berry experimental driver for AXP2101 for M5Core2v1.1 [#23039](https://github.com/arendst/Tasmota/issues/23039) diff --git a/tasmota/my_user_config.h b/tasmota/my_user_config.h index 9b7e733b8..c822571be 100644 --- a/tasmota/my_user_config.h +++ b/tasmota/my_user_config.h @@ -495,6 +495,8 @@ // #define TELNET_COL_RESPONSE 32 // [TelnetColor] ANSI color escape code (default 32 - Green) // #define TELNET_COL_LOGGING 36 // [TelnetColor] ANSI color escape code (default 36 - Cyan) +//#define USE_XYZMODEM // Add support for XModem over serial and telnet (+5k code) + // -- HTTP ---------------------------------------- #define USE_WEBSERVER // Enable web server and Wi-Fi Manager (+66k code, +8k mem) #define WEB_PORT 80 // Web server Port for User and Admin mode diff --git a/tasmota/tasmota_support/support_features.ino b/tasmota/tasmota_support/support_features.ino index 06398a9ad..b92e78451 100644 --- a/tasmota/tasmota_support/support_features.ino +++ b/tasmota/tasmota_support/support_features.ino @@ -949,7 +949,9 @@ constexpr uint32_t feature[] = { #ifdef USE_TELNET 0x00008000 | // xdrv_80_telnet.ino #endif -// 0x00010000 | // +#ifdef USE_XYZMODEM + 0x00010000 | // xdrv_120_xyzmodem.ino +#endif // 0x00020000 | // // 0x00040000 | // // 0x00080000 | // diff --git a/tasmota/tasmota_xdrv_driver/xdrv_120_xyzmodem.ino b/tasmota/tasmota_xdrv_driver/xdrv_120_xyzmodem.ino new file mode 100644 index 000000000..33a636721 --- /dev/null +++ b/tasmota/tasmota_xdrv_driver/xdrv_120_xyzmodem.ino @@ -0,0 +1,1051 @@ +/* + xdrv_120_xyzmodem.ino - XModem support for Tasmota + + SPDX-FileCopyrightText: 2025 Theo Arends + + SPDX-License-Identifier: GPL-3.0-only +*/ + +#ifdef USE_XYZMODEM +/*********************************************************************************************\ + * XModem, XModem/CRC, Xmodem-1k file transfer protocol + * + * Usage: + * Use a tool able supporting XModem file transfer and to connect either using serial or telnet with Tasmota. + * TeraTerm + * SyncTerm + * To start XModem file transfer first execute Tasmota command XSend or XReceive, + * then start tool XModem receive or send. + * + * Commands: + * TeraTerm - Show current selection + * TeraTerm 0 - Disable adding zero after carriage return (0x0D) + * TeraTerm 1 - Enable adding zero after carriage return (0x0D) + * XSend - Send filename + * XSend Settings - Send active settings + * XSend autoexec.bat - Send file autoexec.bat + * XReceive[] [,] - Receive filename with checksum (default CRC) and trim CP/M SUB from end of file (default trim) + * XReceive Settings - Receive and activate settings using CRC (No need for trim1a as it's size is on 128/1024 bytes boundary) + * XReceive autoexec.bat - Receive file autoexec.bat using CRC and trim CP/M SUB from end of file + * XReceive autoexec.bat,0 - Receive file autoexec.bat using CRC and do not trim CP/M SUB from end of file + * XReceive autoexec.bat,1 - Receive file autoexec.bat using CRC and trim CP/M SUB from end of file (default) + * XReceive autoexec.bat,2 - Receive file autoexec.bat using CRC and auto trim CP/M SUB from end of text file + * XReceive2 autoexec.bat - Receive file autoexec.bat using checksum (less secure) + * + * Resource: https://gallium.inria.fr/~doligez/zmodem/ymodem.txt +\*********************************************************************************************/ + +#define XDRV_120 120 + +#define XYZM_SOH 0x01 // Start of 128 byte data +#define XYZM_STX 0x02 // Start of 1024 byte data +#define XYZM_EOT 0x04 +#define XYZM_ACK 0x06 +#define XYZM_BS 0x08 +#define XYZM_LF 0x0a +#define XYZM_CR 0x0d +#define XYZM_NAK 0x15 +#define XYZM_CAN 0x18 +#define XYZM_SUB 0x1a + +// Number of seconds until giving up hope of receiving sync packets from host. +const uint8_t XYZMODEM_SYNC_TIMEOUT = 30; +const uint8_t XYZMODEM_RECV_TIMEOUT_SHORT = 1; // Protocol = 10 seconds +const uint8_t XYZMODEM_RECV_TIMEOUT_LONG = 20; // Protocol = 60 seconds + +// Number of times we try to send a packet to the host until we give up sending.. +const uint8_t XYZMODEM_MAX_RETRY = 4; // Protocol = 10 for total packets to be send. Here retry per packet +// Packet size +const uint8_t XYZMODEM_SOH_PACKET_SIZE = 128; +const uint16_t XYZMODEM_STX_PACKET_SIZE = 1024; +const uint16_t XYZMODEM_FILE_SECTOR_SIZE = 2048; + +enum XTrim1aModes { XYZT_NONE, XYZT_TRIM, XYZT_AUTO }; + +enum XReceiveModes { XYZD_NONE, XYZD_SOH, XYZD_BLK1, XYZD_BLK2, XYZD_DATA }; + +enum XYZFileSteps { XYZM_IDLE, + XYZM_SEND, XYZM_SEOT, XYZM_ACKT, XYZM_COMPLETE, XYZM_ERROR, XYZM_DONE, + XYZM_RECEIVE, XYZM_RCV_START, XYZM_RCV_EOT }; + +enum XReceiveStates { XYZS_OK, XYZS_TIMEOUT, XYZS_EOT, XYZS_CAN, XYZS_OTHER, XYZS_CHECKSUM, XYZS_PACKET, XYZS_FILE }; + +#include + +struct { + TasmotaSerial *serial; + WiFiClient *client; + uint32_t timeout; + uint32_t filepos; + uint32_t packet_no; + uint16_t packet_size; + uint16_t crcBuf; + uint8_t receive_timeout; + uint8_t can_count; + uint8_t nak_count; + uint8_t checksumBuf; + uint8_t protocol; + uint8_t enabled; + uint8_t mode; + uint8_t retry; + bool oldChecksum; + bool teraterm; +} XYZModem; + +struct { + uint32_t size; + int32_t sector_counter; + uint32_t byte_counter; + uint8_t *buffer; + char file_name[48]; + uint8_t step; + uint8_t state; + uint8_t trim1a; + bool file; +} XYZFile; + +/*********************************************************************************************\ + * Low level read and write +\*********************************************************************************************/ + +int XYZModemAvailable(void) { + switch (XYZModem.protocol) { + case TXMP_SERIAL: + return XYZModem.serial->available(); // TasmotaSerial + case TXMP_TASCONSOLE: + return TasConsole.available(); // Serial or TASCONSOLE + case TXMP_TELNET: + return XYZModem.client->available(); // WiFiClient + } + return -1; +} + +int XYZModemRead(void) { + switch (XYZModem.protocol) { + case TXMP_SERIAL: + return XYZModem.serial->read(); + case TXMP_TASCONSOLE: + return TasConsole.read(); + case TXMP_TELNET: + return XYZModem.client->read(); + } + return -1; +} + +size_t XYZModemWrite(uint8_t data) { + switch (XYZModem.protocol) { + case TXMP_SERIAL: + return XYZModem.serial->write(data); + case TXMP_TASCONSOLE: + return TasConsole.write(data); + case TXMP_TELNET: + return XYZModem.client->write(data); + } + return 0; +} + +size_t XYZModemWriteBuf(const uint8_t *buf, size_t size) { + switch (XYZModem.protocol) { + case TXMP_SERIAL: + return XYZModem.serial->write(buf, size); + case TXMP_TASCONSOLE: + return TasConsole.write(buf, size); + case TXMP_TELNET: + return XYZModem.client->write(buf, size); + } + return 0; +} + +/*********************************************************************************************\ + * File or Settings read and write +\*********************************************************************************************/ + +bool XYZModemBufferAlloc(void) { + if (nullptr == XYZFile.buffer) { + // Tradeoff between memory usage and speed (file access is slow) + if (!(XYZFile.buffer = (uint8_t *)malloc(XYZMODEM_FILE_SECTOR_SIZE))) { + return false; // Not enough (memory) space + } + } + return true; +} + +void XYZModemBufferFree(void) { + if (XYZFile.buffer) { + free(XYZFile.buffer); + XYZFile.buffer = nullptr; + } +} + +uint32_t XYZModemFileAvailable(void) { + int available = XYZFile.size - XYZFile.byte_counter; + if (available < 0) { available = 0; } + return available; +} + +int XYZModemFileRead(void) { + // When the source device reaches the last XModem data block, it should be padded to 128 bytes + // of data using SUB (ASCII 0x1A) characters. + int data = XYZM_SUB; + + if (XYZModemFileAvailable()) { + if (XYZFile.file) { +#ifdef USE_UFILESYS + if (!XYZModemBufferAlloc()) { + return -1; // Not enough (memory) space + } + + uint32_t index = XYZFile.byte_counter % XYZMODEM_FILE_SECTOR_SIZE; + int32_t sector = XYZFile.byte_counter / XYZMODEM_FILE_SECTOR_SIZE; + if (sector != XYZFile.sector_counter) { + XYZFile.sector_counter = sector; + File file = ffsp->open(XYZFile.file_name, "r"); + if (file && file.seek(XYZFile.byte_counter)) { + file.read(XYZFile.buffer, XYZMODEM_FILE_SECTOR_SIZE); + file.close(); + } + } + if (XYZFile.byte_counter <= XYZFile.size) { + data = XYZFile.buffer[index]; + } + if (XYZFile.byte_counter == XYZFile.size) { + XYZModemBufferFree(); + } +#endif // USE_UFILESYS + } else { + if (0 == XYZFile.byte_counter) { + XYZFile.size = SettingsConfigBackup(); + if (0 == XYZFile.size) { + return -1; // Not enough (memory) space + } + } + if (XYZFile.byte_counter <= XYZFile.size) { + data = settings_buffer[XYZFile.byte_counter]; + } + if (XYZFile.byte_counter == XYZFile.size) { + SettingsBufferFree(); + } + } + } + XYZFile.byte_counter++; + return data; +} + +/*********************************************************************************************/ + +bool XYZModemFileWrite(const uint8_t *buffer) { + if (XYZFile.file) { // File +#ifdef USE_UFILESYS + if (!XYZModemBufferAlloc()) { + return false; // Not enough (memory) space + } + + // Keep space for mixed 128 and 1k packages + uint32_t max_size = XYZMODEM_FILE_SECTOR_SIZE - XYZMODEM_STX_PACKET_SIZE; + if (nullptr == buffer) { // At EOT + if (XYZFile.trim1a) { + uint32_t eot = XYZFile.sector_counter; // (Auto) trim SUB from text files + while ((eot > 0) && (XYZM_SUB == XYZFile.buffer[--eot])) {} + if (XYZT_AUTO == XYZFile.trim1a) { + if ((XYZM_LF == XYZFile.buffer[eot]) || (XYZM_CR == XYZFile.buffer[eot])) { + eot++; + } + } else { + eot++; + } + XYZFile.byte_counter -= (XYZFile.sector_counter - eot); // Final file size + XYZFile.sector_counter = eot; + } + max_size = 0; // Force flush + } + + if (XYZFile.sector_counter > max_size) { +// AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Save file size %d,%d"), XYZFile.sector_counter, max_size); + + File file; + if ((XYZFile.byte_counter - XYZFile.sector_counter) <= XYZFile.sector_counter) { + ffsp->remove(XYZFile.file_name); + file = ffsp->open(XYZFile.file_name, "w"); + } else { + file = ffsp->open(XYZFile.file_name, "a"); + } + if (file) { + size_t size = 0; + if (UfsFree() > 7) { // Fix ESP32 file.write() not detecting filesystem full + size = file.write(XYZFile.buffer, XYZFile.sector_counter); + } + file.close(); + if (size != XYZFile.sector_counter) { + return false; // File write error - disk full + } + } else { + return false; // Unable to open file + } + XYZFile.sector_counter = 0; + } + + if (buffer) { // Add receive buffer to file save buffer + memcpy(XYZFile.buffer + XYZFile.sector_counter, buffer, XYZModem.packet_size); + XYZFile.sector_counter += XYZModem.packet_size; + } +#endif // USE_UFILESYS + } else { // Settings + uint32_t index = XYZFile.byte_counter - XYZModem.packet_size; + if (0 == index) { // First package + uint32_t set_size = sizeof(TSettings); +#ifdef USE_UFILESYS + if (('s' == buffer[2]) && ('e' == buffer[3])) { // /.settings + set_size = buffer[14] + (buffer[15] << 8); + } +#endif // USE_UFILESYS + if (!SettingsBufferAlloc(set_size)) { + return false; // Not enough (memory) space + } + } + + if (nullptr == buffer) { // Flush receive buffer + return true; // Already flushed + } + + uint32_t package_size = XYZModem.packet_size; + if (XYZFile.byte_counter > settings_size) { + package_size -= (XYZFile.byte_counter - settings_size); + } + memcpy(settings_buffer + index, buffer, package_size); + } + return true; +} + +void XYZModemFileWriteEot(bool state) { + if (XYZFile.file) { +#ifdef USE_UFILESYS + XYZModemBufferFree(); // Release buffer + if (0 == state) { + ffsp->remove(XYZFile.file_name); // Remove corrupted file + } +#endif // USE_UFILESYS + } else { + if (1 == state) { + if (SettingsConfigRestore()) { // Process new settings and release buffer + TasmotaGlobal.restart_flag = 2; // Restart on valid settings + } + } else { + SettingsBufferFree(); // Release buffer + } + } +} + +/*********************************************************************************************\ + * Xmodem read +\*********************************************************************************************/ + +bool XYZModemReadAvailable(uint32_t timeout) { + int i = 0; + while (!XYZModemAvailable()) { + delayMicroseconds(100); + i++; + if (i > timeout * 10000) { // 10000 * 100 uS = 1 Sec (Protocol Initial 40 Sec, then 20 Sec) + return false; + } + } + return true; +} + +int XYZModemReadByte(void) { + if (!XYZModemReadAvailable(XYZModem.receive_timeout)) { + return -1; + } + int in_char = XYZModemRead(); + if (in_char >= 0) { + if (TXMP_TELNET == XYZModem.protocol) { + if (0xFF == in_char) { // Fix XModem over Telnet escape + XYZModemRead(); + } + if (XYZModem.teraterm && (0x0D == in_char)) { // Fix TeraTerm + XYZModemRead(); + } + } + } +// AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Rcvd %d"), in_char); + + return in_char; +} + +/*********************************************************************************************\ + * Xmodem Send +\*********************************************************************************************/ + +// 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. +int XYZModemWaitACK(void) { + int in_char; + do { + in_char = XYZModemReadByte(); + if (in_char < 0) { return -1; } // Timeout + if (XYZM_CAN == in_char) { return XYZM_CAN; } // Cancel + } while ((in_char != XYZM_NAK) && (in_char != XYZM_ACK) && (in_char != 'C')); + return in_char; +} + +// Calculate checksum +void XYZModemChecksum(uint8_t out_char) { + XYZModem.checksumBuf += out_char; + + XYZModem.crcBuf = XYZModem.crcBuf ^ (uint16_t)out_char << 8; + for (uint32_t i = 0; i < 8; i++) { + if (XYZModem.crcBuf & 0x8000) { + XYZModem.crcBuf = XYZModem.crcBuf << 1 ^ 0x1021; + } else { + XYZModem.crcBuf = XYZModem.crcBuf << 1; + } + } +} + +void XYZModemSendEscaped(uint8_t *xmodem_buffer, uint32_t *xmodem_buffer_ptr, uint8_t in_char) { + uint32_t buffer_ptr = *xmodem_buffer_ptr; + xmodem_buffer[buffer_ptr++] = in_char; + if (TXMP_TELNET == XYZModem.protocol) { + if (0xFF == in_char) { // Fix XModem over Telnet escape + xmodem_buffer[buffer_ptr++] = in_char; + } + if (XYZModem.teraterm && (0x0D == in_char)) { // Fix TeraTerm + xmodem_buffer[buffer_ptr++] = 0x00; + } + } + *xmodem_buffer_ptr = buffer_ptr; +} + +bool XYZModemSend(uint32_t packet_no) { + XYZModem.filepos = XYZFile.byte_counter; + + // Try to send packet, so header first. Use a buffer to reduce TCP/IP header overhead + uint8_t xmodem_buffer[2 * (3 + XYZMODEM_SOH_PACKET_SIZE + 2)]; // Need double packet size to fix Telnet escape + + // Sending a packet will be retried + uint32_t retries = 0; + int in_char; + do { + // Seek to start of current data block, + // will advance through the file as block will be acked.. + XYZFile.byte_counter = XYZModem.filepos; + + // Reset checksum stuff + XYZModem.checksumBuf = 0x00; + XYZModem.crcBuf = 0x00; + + uint8_t packet_num = packet_no; + +// AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Packet %d, retries %d, counter %d"), packet_no, retries, XYZFile.byte_counter); + + uint32_t xmodem_buffer_ptr = 0; + XYZModemSendEscaped(xmodem_buffer, &xmodem_buffer_ptr, XYZM_SOH); + XYZModemSendEscaped(xmodem_buffer, &xmodem_buffer_ptr, packet_num); + XYZModemSendEscaped(xmodem_buffer, &xmodem_buffer_ptr, ~packet_num); + for (uint32_t i = 0; i < XYZMODEM_SOH_PACKET_SIZE; i++) { + in_char = XYZModemFileRead(); + if (in_char < 0) { + return false; // No input + } + XYZModemChecksum(in_char); + XYZModemSendEscaped(xmodem_buffer, &xmodem_buffer_ptr, in_char); + } + // Send out checksum, either CRC-16 CCITT or classical inverse of sum of bytes. + // Depending on how the receiver introduced itself + if (XYZModem.oldChecksum) { + XYZModemSendEscaped(xmodem_buffer, &xmodem_buffer_ptr, XYZModem.checksumBuf); + } else { + XYZModemSendEscaped(xmodem_buffer, &xmodem_buffer_ptr, XYZModem.crcBuf >> 8); + XYZModemSendEscaped(xmodem_buffer, &xmodem_buffer_ptr, XYZModem.crcBuf & 0xFF); + } + XYZModemWriteBuf(xmodem_buffer, xmodem_buffer_ptr); + + in_char = XYZModemWaitACK(); + +// AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Ack %02X, Send %*_H"), in_char, xmodem_buffer_ptr, xmodem_buffer); +// if (in_char != XYZM_ACK) { +// AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("XMD: Send packet %d, Size %d, Ack %d"), packet_no, xmodem_buffer_ptr, in_char); +// } + + if (XYZM_CAN == in_char) { + return false; // CAN + } + retries++; + XYZModem.teraterm = 1; // Fix teraterm TELNET issue + if (retries > XYZMODEM_MAX_RETRY) { + return false; // Max retries + } + } while (in_char != XYZM_ACK); + return true; +} + +/*********************************************************************************************\ + * Xmodem Receive +\*********************************************************************************************/ + +void XYZModemCancel(void) { + // Five cancels & five backspaces per spec + const uint8_t cancel[] = { XYZM_CAN, XYZM_CAN, XYZM_CAN, XYZM_CAN, XYZM_CAN, XYZM_BS, XYZM_BS, XYZM_BS, XYZM_BS, XYZM_BS }; + XYZModemWriteBuf(cancel, sizeof(cancel)); +} + +void XYZModemSendNakOrC(void) { + while (XYZModemAvailable()) { XYZModemRead(); } // Flush input + + char out_char = 'C'; + if (XYZModem.oldChecksum) { + out_char = XYZM_NAK; + } + XYZModemWrite(out_char); +} + +void XYZModemSendNak(void) { + // When the receiver wishes to , it should call a "PURGE" subroutine, to wait + // for the line to clear. Recall the sender tosses any characters in its buffer + // immediately upon completing sending a block, to ensure no glitches were mis-interpreted. + // The most common technique is for "PURGE" to call the character receive subroutine, + // specifying a 1-second timeout,[1] and looping back to PURGE until a timeout occurs. + // The is then sent, ensuring the other end will see it. + uint32_t timeout = 1 * XYZMODEM_RECV_TIMEOUT_SHORT; + while (timeout--) { + while (XYZModemAvailable()) { XYZModemRead(); } // Flush input + delay(1); + } + + if (XYZModem.nak_count) { + XYZModem.nak_count--; + if (0 == XYZModem.nak_count) { + XYZModemCancel(); // Cancel xfer + return; + } + } + + XYZModemWrite(XYZM_NAK); + + XYZModem.mode = XYZD_SOH; +} + +bool XYZModemCheckPacket(uint8_t *buffer) { + XYZModem.checksumBuf = 0x00; + XYZModem.crcBuf = 0x00; + + for (uint32_t i = 0; i < XYZModem.packet_size; i++) { + XYZModemChecksum(buffer[i + 3]); + } + +// AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: CheckPacket %d, checksum %d,%d, crc %04X,%04X"), +// XYZModem.oldChecksum, buffer[3 + XYZModem.packet_size], XYZModem.checksumBuf, +// buffer[3 + XYZModem.packet_size] << 8 | buffer[3 + XYZModem.packet_size +1], XYZModem.crcBuf); + + if (XYZModem.oldChecksum) { + return (buffer[3 + XYZModem.packet_size] == XYZModem.checksumBuf); + } else { + return ((buffer[3 + XYZModem.packet_size] == (XYZModem.crcBuf >> 8)) && + (buffer[3 + XYZModem.packet_size +1] == (XYZModem.crcBuf & 0xFF))); + } +} + +int XYZModemReceive(uint32_t packet_no) { + // Try to send packet, so header first. Use a buffer to reduce TCP/IP header overhead + uint8_t xmodem_buffer[3 + XYZMODEM_STX_PACKET_SIZE + 2]; + uint32_t xmodem_buffer_ptr = 0; + uint32_t packet_size; + + XYZModem.mode = XYZD_SOH; + + // The character-receive subroutine should be called with a parameter specifying the + // number of seconds to wait. The receiver should first call it with a time of 10, + // then and try again, 10 times. + XYZModem.receive_timeout = XYZMODEM_RECV_TIMEOUT_LONG; + + int in_char; + bool packet_ready = false; + do { + in_char = XYZModemReadByte(); + if (in_char < 0) { + return XYZS_TIMEOUT; // No receive after timeout + } + switch (XYZModem.mode) { + case XYZD_SOH: { +// AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: XYZD_SOH")); + + switch (in_char) { + case XYZM_SOH: { // Start XModem 128 byte data transfer +// AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: SOH")); + + xmodem_buffer[0] = in_char; + // After receiving the , the receiver should call the character + // receive subroutine with a 1-second timeout, for the remainder of the + // message and the . Since they are sent as a continuous stream, + // timing out of this implies a serious like glitch that caused, say, + // 127 characters to be seen instead of 128. + XYZModem.receive_timeout = XYZMODEM_RECV_TIMEOUT_SHORT; + XYZModem.packet_size = XYZMODEM_SOH_PACKET_SIZE; + XYZModem.mode = XYZD_BLK1; + break; + } + case XYZM_STX: { // Start XModem 1024 byte data transfer +// AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: STX")); + + xmodem_buffer[0] = in_char; + // After receiving the , the receiver should call the character + // receive subroutine with a 1-second timeout, for the remainder of the + // message and the . Since they are sent as a continuous stream, + // timing out of this implies a serious like glitch that caused, say, + // 1023 characters to be seen instead of 1024. + XYZModem.receive_timeout = XYZMODEM_RECV_TIMEOUT_SHORT; + XYZModem.packet_size = XYZMODEM_STX_PACKET_SIZE; + XYZModem.mode = XYZD_BLK1; + break; + } + case XYZM_EOT: { +// AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: EOT")); + + if (packet_no) { + XYZModemFileWrite(nullptr); + } else { + // Handle YModem filename package + } + XYZModemWrite(XYZM_ACK); + return XYZS_EOT; // Success as EOT received + break; + } + case XYZM_CAN: { +// AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: CAN")); + + XYZModem.can_count++; + if (5 == XYZModem.can_count) { + return XYZS_CAN; // Cancelled by sender + } + break; + } + default: { +// AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Default")); + + XYZModemCancel(); + return XYZS_OTHER; // Unknown error + } + } + break; + } + case XYZD_BLK1: { +// AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: XYZD_BLK1")); + + xmodem_buffer[1] = in_char; + XYZModem.mode = XYZD_BLK2; + break; + } + case XYZD_BLK2: { +// AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: XYZD_BLK2 %02X, exor %02X"), in_char, (in_char ^ xmodem_buffer[1])); + + xmodem_buffer[2] = in_char; + if (0xFF == (in_char ^ xmodem_buffer[1])) { + xmodem_buffer_ptr = 3; + packet_size = 3 + XYZModem.packet_size + ((XYZModem.oldChecksum) ? 1 : 2); + XYZModem.mode = XYZD_DATA; + } else { + XYZModemSendNak(); + } + break; + } + case XYZD_DATA: { // Read data and checksum/crc +// AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: XYZD_DATA %d of %d"), xmodem_buffer_ptr, packet_size); + + xmodem_buffer[xmodem_buffer_ptr++] = in_char; + if (xmodem_buffer_ptr >= packet_size) { +// XYZFile.byte_counter += XYZModem.packet_size; + XYZFile.byte_counter = packet_no * XYZModem.packet_size; + XYZModem.mode = XYZD_SOH; + packet_ready = true; + } else { + + } + break; + } + } + } while (!packet_ready); +// AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Packet ready")); + + if (!XYZModemCheckPacket(xmodem_buffer)) { +// AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Checksum failure")); + + XYZFile.byte_counter -= XYZModem.packet_size; // Restore for retry + XYZModemSendNak(); + return XYZS_CHECKSUM; // Checksum failure + } + + uint8_t packet = xmodem_buffer[1] - packet_no; + +// AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Packet %d,%d,%d"), packet, xmodem_buffer[1], packet_no); + + if (packet > 1) { +// AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Bad packet number")); + + XYZModemCancel(); + return XYZS_PACKET; // Bad packet number + } + + XYZModemWrite(XYZM_ACK); + XYZModem.nak_count = 10; + +// if (0 == packet) { +// return true; +// } + + if (!XYZModemFileWrite(xmodem_buffer +3)) { + return XYZS_FILE; // Unable to claim file buffer + } + + return XYZS_OK; +} + +/********************************************************************************************/ + +void XModemSendStart(void) { + // *** Handle file send using XModem - sync + // Wait for either C or NACK as a sync packet. Determines protocol details, checksum algorithm. + XYZModem.enabled = XYZM_SEND; + XYZModem.timeout = millis() + (XYZMODEM_SYNC_TIMEOUT * 1000); + XYZModem.packet_no = 1; + XYZFile.byte_counter = 0; + XYZFile.sector_counter = -1; + if (XYZFile.size) { + XYZFile.step = XYZM_SEND; + AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Send started")); + } else { + XYZFile.step = XYZM_SEOT; + AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Send aborted")); + } +} + +void XModemReceiveStart(void) { + // *** Handle receive file using XModem - sync + XYZModem.enabled = XYZM_RECEIVE; + XYZModem.timeout = millis() + 1000; + XYZModem.nak_count = 30; // Allow 30 * 2 seconds for Xmodem Send to start + XYZModem.can_count = 0; + XYZModem.packet_no = 1; + XYZModem.retry = 4; + XYZFile.byte_counter = 0; + XYZFile.sector_counter = 0; + XYZFile.step = XYZM_RECEIVE; +} + +bool XYZModemLoop(void) { + switch (XYZFile.step) { + case XYZM_IDLE: { // *** Send/Receive disabled + return false; + } + // *** Send + case XYZM_SEND: { // *** Handle file send using XModem - upload + if (XYZModemFileAvailable()) { + if (XYZFile.byte_counter && !(XYZFile.byte_counter % 10240)) { // Show progress every 10kB + AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Progress %d kB"), XYZFile.byte_counter / 1024); + } + if (!XYZModemSend(XYZModem.packet_no)) { + AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Packet %d send failed"), XYZModem.packet_no); + XYZFile.step = XYZM_ERROR; + return true; + } + XYZModem.packet_no++; + } else { + XYZFile.step = XYZM_SEOT; + AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Send %d bytes"), XYZFile.size); + } + break; + } + case XYZM_SEOT: { // *** Send EOT and wait for ACK + // Once the last block is ACKed by the target, the transfer should be finalized by an + // EOT (ASCII 0x04) packet from the source. This packet is confirmed via XModem ACK + // from the target. + XYZModemWrite(XYZM_EOT); + XYZModem.timeout = millis() + (30 * 1000); // Allow 30 seconds to receive EOT ACK + XYZFile.step = XYZM_ACKT; + break; + } + case XYZM_ACKT: { // *** 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. + if (millis() > XYZModem.timeout) { + AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: EOT ACK timeout")); + XYZFile.step = XYZM_ERROR; + return true; + } + if (XYZModemAvailable()) { + int xmodem_ack = XYZModemWaitACK(); + if (XYZM_CAN == xmodem_ack) { + AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Transfer invalid")); + XYZFile.step = XYZM_ERROR; + return true; + } + else if (XYZM_ACK == xmodem_ack) { + AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Successful")); + XYZModem.timeout = millis() + (30 * 1000); // Allow 30 seconds + XYZFile.byte_counter = 0; + XYZFile.step = XYZM_DONE; + } + } + break; + } + // *** Receive + case XYZM_RECEIVE: { // *** Handle receive file using XModem - download + if (millis() > XYZModem.timeout) { + XYZModem.timeout = millis() + (2 * 1000); // Protocol 10 second receive timeout - here 2 seconds + XYZModemSendNakOrC(); + if (XYZModemReadAvailable(1)) { // Timeout after 1 second + XYZModem.nak_count = (XYZModem.oldChecksum) ? 10 : 3; + AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Receive started")); + XYZFile.step = XYZM_RCV_START; + return true; + } + else if (0 == XYZModem.nak_count) { // Timeout after 30 * 2 seconds + AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Receive timeout")); + XYZFile.step = XYZM_ERROR; + return true; + } + } + break; + } + case XYZM_RCV_START: { + if (XYZFile.byte_counter && !(XYZFile.byte_counter % 10240)) { // Show progress every 10kB + AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Progress %d kB"), XYZFile.byte_counter / 1024); + } + int result = XYZModemReceive(XYZModem.packet_no); + if (result) { + switch (result) { + case XYZS_EOT: { + AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Received %d bytes"), XYZFile.byte_counter); + AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Successful")); + XYZModemFileWriteEot(1); + XYZFile.step = XYZM_DONE; + break; + } + case XYZS_CHECKSUM: { + // Checksum error or teraterm special ops on 0x0D disabled - will retry + AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Checksum failure - retry")); + XYZModem.teraterm = 1; // Fix teraterm TELNET issue + XYZModem.retry--; + break; + } + case XYZS_TIMEOUT: { + // Receive character timeout - will retry + AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Timeout - retry")); + XYZModem.retry--; + break; + } + case XYZS_OTHER: { + // Received unknown command + AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Unknown error")); + XYZModem.retry = 0; + break; + } + case XYZS_CAN: { + // Receive cancelled by user + AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Cancelled")); + XYZModem.retry = 0; + break; + } + case XYZS_PACKET: { + // Packet number mismatch + AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Packet number error")); + XYZModem.retry = 0; + break; + } + case XYZS_FILE: { + // Unable to allocate memory, file write error or filesystem full + AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: File write error")); + XYZModem.retry = 0; + break; + } + default: + AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Receive error %d"), result); + XYZModem.retry = 0; + } + + if (0 == XYZModem.retry) { + AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Packet %d receive failed"), XYZModem.packet_no); + XYZModemFileWriteEot(0); + XYZModemCancel(); + XYZFile.step = XYZM_ERROR; + return true; + } + } else { + XYZModem.packet_no++; + } + break; + } + // *** Finish + case XYZM_COMPLETE: { // *** Wait for send complete + if (millis() > XYZModem.timeout) { + AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Timeout")); + XYZFile.step = XYZM_ERROR; + return true; + } else { + XYZFile.state = XYZM_COMPLETE; + XYZFile.step = XYZM_DONE; + } + break; + } + case XYZM_ERROR: + XYZFile.state = XYZM_ERROR; + AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Failed")); + case XYZM_DONE: { // *** Clean up + XYZModemBufferFree(); + XYZModemInit(); + break; + } + } + return true; +} + +void XYZModemInit(void) { + XYZModem.receive_timeout = XYZMODEM_RECV_TIMEOUT_SHORT; + XYZModem.enabled = XYZM_IDLE; + XYZModem.protocol = TXMP_NONE; + XYZFile.size = 0; + XYZFile.step = XYZM_IDLE; +} + +/*********************************************************************************************\ + * Commands +\*********************************************************************************************/ + +const char kXYZmodemCommands[] PROGMEM = "|" // No prefix + "TeraTerm|XSend|XReceive"; + +void (* const XYZmodemCommands[])(void) PROGMEM = { + &CmndTeraTerm, &CmndXSend, &CmndXReceive }; + +void CmndTeraTerm(void) { + // TeraTerm - Show current selection + // TeraTerm 0 - Disable adding zero after carriage return (0x0D) + // TeraTerm 1 - Enable adding zero after carriage return (0x0D) + if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 1)) { + XYZModem.teraterm = XdrvMailbox.payload; + } + ResponseCmndStateText(XYZModem.teraterm); +} + +void CmndXSend(void) { + // XSend + // XSend Settings - Send active settings + // XSend autoexec.bat - Send file autoexec.bat + if (XdrvMailbox.data_len > 0) { + if (!strcasecmp_P(XdrvMailbox.data, PSTR("Settings"))) { + XYZFile.size = sizeof(TSettings); + XYZFile.file = false; +// XYZModem.enabled = XYZM_SEND; + ResponseCmndChar("Ready to start receive Settings"); +#ifdef USE_UFILESYS + } else { + UfsFilename(XYZFile.file_name, XdrvMailbox.data); + if (TfsFileExists(XYZFile.file_name)) { + XYZFile.size = TfsFileSize(XYZFile.file_name); + if (XYZFile.size) { + XYZFile.file = true; +// XYZModem.enabled = XYZM_SEND; + ResponseCmndChar("Ready to start receive file"); + } else { + ResponseCmndChar("File is empty"); + } + } else { + ResponseCmndChar("File not found"); + } +#endif // USE_UFILESYS + } + } +} + +void CmndXReceive(void) { + // XReceive[] [,] - Receive filename with optional crc (default CRC) and trim CP/M SUB from end of file (default trim) + // XReceive Settings - Receive and activate settings using CRC (No need for trim1a as it's size is on 128/1024 bytes boundary) + // XReceive autoexec.bat - Receive file autoexec.bat using CRC leaving CP/M SUB at end of file + // XReceive autoexec.bat,0 - Receive file autoexec.bat using CRC and do not trim CP/M SUB from end of file + // XReceive autoexec.bat,1 - Receive file autoexec.bat using CRC and trim CP/M SUB from end of file (default) + // XReceive autoexec.bat,2 - Receive file autoexec.bat using CRC and auto trim CP/M SUB from end of file + // XReceive2 autoexec.bat - Receive file autoexec.bat using checksum (less secure) + if (XdrvMailbox.data_len > 0) { + XYZModem.oldChecksum = 0; // CRC (Start with C) - Default + if (2 == XdrvMailbox.index) { + XYZModem.oldChecksum = 1; // Checksum (Start with NAK) + } + XYZFile.trim1a = XYZT_TRIM; // Trim CP/M SUB from end of file - Default + size_t option = strchrspn(XdrvMailbox.data, ','); + if (option) { + char option_crc[XdrvMailbox.data_len]; + ArgV(option_crc, 2); + XYZFile.trim1a = atoi(option_crc) &0x03; + XdrvMailbox.data[option] = '\0'; // Force input end of string + } + if (!strcasecmp_P(XdrvMailbox.data, PSTR("Settings"))) { + XYZFile.file = false; + XModemReceiveStart(); + ResponseCmndChar("Ready to start sending Settings"); +#ifdef USE_UFILESYS + } else { + UfsFilename(XYZFile.file_name, XdrvMailbox.data); + XYZFile.file = true; + XModemReceiveStart(); + ResponseCmndChar("Ready to start sending file"); +#endif // USE_UFILESYS + } + } +} + +/*********************************************************************************************\ + * External access +\*********************************************************************************************/ + +bool XYZModemStart(uint32_t protocol, uint8_t in_byte) { + XYZModem.protocol = protocol; + +// AddLog(LOG_LEVEL_DEBUG, PSTR("XMD: Protocol %d, Received %02X(%d)"), protocol, in_byte, in_byte); + + if (XYZFile.size) { + if ((in_byte == XYZM_NAK) || // Xmodem NAK - Checksum + (in_byte == 'C')) { // Xmodem NAK - CRC + // Determine which checksum algorithm to use + XYZModem.oldChecksum = (in_byte == XYZM_NAK); + XModemSendStart(); + return true; + } + } + return false; +} + +bool XYZModemWifiClientStart(WiFiClient *client, uint8_t in_byte) { + XYZModem.client = client; + return XYZModemStart(TXMP_TELNET, in_byte); +} + +bool XYZModemActive(uint32_t protocol) { + return (XYZModem.enabled && (XYZModem.protocol == protocol)); +} + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ + +bool Xdrv120(uint32_t function) { + bool result = false; + + switch (function) { + case FUNC_SLEEP_LOOP: + case FUNC_LOOP: + if (XYZModem.enabled) { + XYZModemLoop(); + } + break; + case FUNC_INIT: + XYZModemInit(); + break; + case FUNC_COMMAND: + result = DecodeCommand(kXYZmodemCommands, XYZmodemCommands); + break; + case FUNC_ACTIVE: + result = true; + break; + } + return result; +} + +#endif // USE_XYZMODEM diff --git a/tools/decode-status.py b/tools/decode-status.py index 5395d784b..acd9ba636 100755 --- a/tools/decode-status.py +++ b/tools/decode-status.py @@ -312,7 +312,7 @@ a_features = [[ "USE_SPI_LORA","USE_SPL06_007","USE_QMP6988","USE_WOOLIIS", "USE_HX711_M5SCALES","USE_RX8010","USE_PCF85063","USE_ESP32_TWAI", "USE_C8_CO2_5K","USE_WIZMOTE","USE_V9240","USE_TELNET", - "","","","", + "USE_XYZMODEM","","","", "","","","", "","","","", "","","","" @@ -343,7 +343,7 @@ else: obj = json.load(fp) def StartDecode(): - print ("\n*** decode-status.py v14.5.0.2 by Theo Arends and Jacek Ziolkowski ***") + print ("\n*** decode-status.py v14.5.0.3 by Theo Arends and Jacek Ziolkowski ***") # print("Decoding\n{}".format(obj))