diff --git a/docs/marlin_binary_file_transfer.md b/docs/marlin_binary_file_transfer.md new file mode 100644 index 00000000..b1785588 --- /dev/null +++ b/docs/marlin_binary_file_transfer.md @@ -0,0 +1,40 @@ +# Marlin Binary File Transfer + +Enable `MARLIN_BINARY_FILE_TRANSFER_FEATURE` to copy a file from ESP3D flash +or its local SD card to storage managed by Marlin. Marlin must advertise +`Cap:BINARY_FILE_TRANSFER:1` in its `M115` response. + +ESP3D negotiates the packet size and compression parameters. With +`compression=auto`, heatshrink is selected when Marlin offers it; otherwise +the file is sent uncompressed. While a transfer is active, ESP3D reserves the +printer connection and rejects other commands. + +## HTTP API + +Start a transfer with an authenticated POST request: + +```text +POST /printer-sd-transfer?action=start&source=/FS/model.gcode&destination=/model.gcode&compression=auto +``` + +Use `/FS/...` for ESP flash and `/SD/...` for the ESP's local SD card. Optional +parameters are `compression=auto|none|heatshrink` and `dummy=true|false`. + +Poll progress with: + +```text +GET /printer-sd-transfer?action=status +``` + +The JSON response includes `status`, `active`, source and wire byte counts, +`progress`, negotiated compression parameters, retries, elapsed time, and an +error message. Terminal states are `completed`, `cancelled`, and `failed`. + +Cancel an active transfer with: + +```text +POST /printer-sd-transfer?action=cancel +``` + +The WebUI should poll status until `active` becomes false, then refresh the +Marlin SD listing after a successful transfer. diff --git a/docs/printer_link_exclusive_access.md b/docs/printer_link_exclusive_access.md new file mode 100644 index 00000000..83b3c5a7 --- /dev/null +++ b/docs/printer_link_exclusive_access.md @@ -0,0 +1,36 @@ +# Exclusive Printer-Link Access + +`PrinterLinkService` lets a protocol temporarily reserve the connection to the +printer. This is useful for binary or otherwise stateful protocols that cannot +share the byte stream with terminal commands or automatic polling. + +## API + +Capture the link with an owner name and an optional receive callback: + +```cpp +printer_link_service.acquire("binary-transfer", receiveData, context); +``` + +While captured: + +- all printer RX is delivered only to `receiveData`; +- a null callback deliberately discards printer RX; +- normal commands, including WebUI polling, are rejected before serial TX; +- other clients cannot capture the link; +- the WebUI receives `printerLink:captured:binary-transfer`. + +Release it using the same owner name: + +```cpp +printer_link_service.release("binary-transfer"); +``` + +Only the current owner can release the link. Release restores normal terminal +RX and printer TX and emits `printerLink:released`. A newly connected WebUI is +sent the current state immediately, so it can disable its terminal input and +polling controls even when capture began before the page connected. + +Every successful `acquire()` must have a matching `release()` on success, +cancellation, timeout, and error paths. The service does not transmit protocol +bytes itself; the owner continues to use the selected serial service directly. diff --git a/esp3d/configuration.h b/esp3d/configuration.h index 12948a5f..43cb2184 100644 --- a/esp3d/configuration.h +++ b/esp3d/configuration.h @@ -181,6 +181,12 @@ */ #define HTTP_FEATURE +/* Copy files from the ESP flash / local SD to a Marlin-controlled SD card + * using Marlin's Binary File Transfer protocol (M28 B1). + * Marlin must be compiled with BINARY_FILE_TRANSFER. + */ +//#define MARLIN_BINARY_FILE_TRANSFER_FEATURE + /* Use telnet server * Enable telnet light (raw tcp) communications */ @@ -705,4 +711,4 @@ #undef NOTIFICATION_FEATURE #endif -#endif //_CONFIGURATION_H \ No newline at end of file +#endif //_CONFIGURATION_H diff --git a/esp3d/src/core/esp3d.cpp b/esp3d/src/core/esp3d.cpp index ff0eff28..0a6374c9 100644 --- a/esp3d/src/core/esp3d.cpp +++ b/esp3d/src/core/esp3d.cpp @@ -62,6 +62,9 @@ #if defined(USB_SERIAL_FEATURE) #include "../modules/usb-serial/usb_serial_service.h" #endif // USB_SERIAL_FEATURE +#if defined(MARLIN_BINARY_FILE_TRANSFER_FEATURE) +#include "../modules/marlin_bft/marlin_bft_service.h" +#endif bool Esp3D::restart = false; @@ -193,6 +196,9 @@ void Esp3D::handle() { esp3d_serial_service.handle(); #endif // COMMUNICATION_PROTOCOL == RAW_SERIAL || COMMUNICATION_PROTOCOL == // MKS_SERIAL +#if defined(MARLIN_BINARY_FILE_TRANSFER_FEATURE) + marlin_bft_service.handle(); +#endif #if defined(ESP_SERIAL_BRIDGE_OUTPUT) serial_bridge_service.handle(); #endif // ESP_SERIAL_BRIDGE_OUTPUT diff --git a/esp3d/src/core/esp3d_commands.cpp b/esp3d/src/core/esp3d_commands.cpp index bc6bd5a2..549867ec 100644 --- a/esp3d/src/core/esp3d_commands.cpp +++ b/esp3d/src/core/esp3d_commands.cpp @@ -89,6 +89,7 @@ const char *esp3dmsgstr[] = {"head", "core", "tail", "unique"}; #if defined(USB_SERIAL_FEATURE) #include "../modules/usb-serial/usb_serial_service.h" #endif // USB_SERIAL_FEATURE +#include "../modules/printer_link/printer_link_service.h" ESP3DCommands esp3d_commands; @@ -1329,6 +1330,13 @@ bool ESP3DCommands::dispatch(ESP3DMessage *msg) { #if COMMUNICATION_PROTOCOL == RAW_SERIAL case ESP3DClientType::serial: esp3d_log("Serial message"); + if (printer_link_service.captured()) { + esp3d_log_e("Printer link is captured by %s", + printer_link_service.owner()); + esp3d_message_manager.deleteMsg(msg); + sendOk = false; + break; + } if (!esp3d_serial_service.dispatch(msg)) { sendOk = false; esp3d_log_e("Serial dispatch failed"); @@ -1337,6 +1345,13 @@ bool ESP3DCommands::dispatch(ESP3DMessage *msg) { #if defined(USB_SERIAL_FEATURE) case ESP3DClientType::usb_serial: esp3d_log("USB Serial message"); + if (printer_link_service.captured()) { + esp3d_log_e("Printer link is captured by %s", + printer_link_service.owner()); + esp3d_message_manager.deleteMsg(msg); + sendOk = false; + break; + } if (!esp3d_usb_serial_service.dispatch(msg)) { sendOk = false; esp3d_log_e("USB Serial dispatch failed"); diff --git a/esp3d/src/include/esp3d_config.h b/esp3d/src/include/esp3d_config.h index 1e7aa770..eb170d15 100644 --- a/esp3d/src/include/esp3d_config.h +++ b/esp3d/src/include/esp3d_config.h @@ -40,6 +40,11 @@ #endif #endif +#if defined(MARLIN_BINARY_FILE_TRANSFER_FEATURE) && \ + !defined(GLOBAL_FILESYSTEM_FEATURE) +#define GLOBAL_FILESYSTEM_FEATURE +#endif + #include "../core/esp3d_hal.h" #include "../core/esp3d_log.h" #include "../include/esp3d_pins.h" diff --git a/esp3d/src/include/esp3d_sanity.h b/esp3d/src/include/esp3d_sanity.h index 11af8b56..303613a9 100644 --- a/esp3d/src/include/esp3d_sanity.h +++ b/esp3d/src/include/esp3d_sanity.h @@ -81,6 +81,18 @@ #endif // defined(ESP_SERIAL_BRIDGE_OUTPUT) #endif // COMMUNICATION_PROTOCOL == MKS_SERIAL +#if defined(MARLIN_BINARY_FILE_TRANSFER_FEATURE) +#if COMMUNICATION_PROTOCOL != RAW_SERIAL +#error MARLIN_BINARY_FILE_TRANSFER_FEATURE requires RAW_SERIAL +#endif +#if !defined(FILESYSTEM_FEATURE) && !defined(SD_DEVICE) +#error MARLIN_BINARY_FILE_TRANSFER_FEATURE requires FILESYSTEM_FEATURE or SD_DEVICE +#endif +#if !defined(HTTP_FEATURE) +#error MARLIN_BINARY_FILE_TRANSFER_FEATURE requires HTTP_FEATURE +#endif +#endif // MARLIN_BINARY_FILE_TRANSFER_FEATURE + /************************** * USB-Serial * ***********************/ diff --git a/esp3d/src/modules/http/handlers/handle-command.cpp b/esp3d/src/modules/http/handlers/handle-command.cpp index 580694b3..d919d783 100644 --- a/esp3d/src/modules/http/handlers/handle-command.cpp +++ b/esp3d/src/modules/http/handlers/handle-command.cpp @@ -31,6 +31,7 @@ #include "../../../core/esp3d_settings.h" #include "../../../core/esp3d_string.h" #include "../../authentication/authentication_service.h" +#include "../../printer_link/printer_link_service.h" // Handle web command query and send answer////////////////////////////// @@ -87,6 +88,12 @@ void HTTP_Server::handle_web_command() { } } else { HTTP_Server::set_http_headers(); + if (printer_link_service.captured()) { + String response = "Printer link is captured by "; + response += printer_link_service.owner(); + _webserver->send(423, "text/plain", response); + return; + } // the command is not ESP3D so it will be forwarded to the output client // no need to wait to answer then _webserver->send(200, "text/plain", "ESP3D says: command forwarded"); diff --git a/esp3d/src/modules/http/handlers/handle-marlin-bft.cpp b/esp3d/src/modules/http/handlers/handle-marlin-bft.cpp new file mode 100644 index 00000000..e7cf26d1 --- /dev/null +++ b/esp3d/src/modules/http/handlers/handle-marlin-bft.cpp @@ -0,0 +1,83 @@ +/* + handle-marlin-bft.cpp - Marlin Binary File Transfer HTTP API + + Copyright (c) 2026 ESP3D contributors + + This code is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. +*/ + +#include "../../../include/esp3d_config.h" + +#if defined(HTTP_FEATURE) && defined(MARLIN_BINARY_FILE_TRANSFER_FEATURE) + +#if defined(ARDUINO_ARCH_ESP32) +#include +#else +#include +#endif + +#include "../http_server.h" +#include "../../authentication/authentication_service.h" +#include "../../marlin_bft/marlin_bft_service.h" + +void HTTP_Server::handleMarlinBft() { + set_http_headers(); + if (AuthenticationService::getAuthenticatedLevel() == + ESP3DAuthenticationLevel::guest) { + _webserver->send(401, "application/json", + "{\"status\":\"error\",\"error\":\"Wrong authentication\"}"); + return; + } + + String action = _webserver->hasArg("action") + ? _webserver->arg("action") + : "status"; + action.toLowerCase(); + + if (action == "start") { + if (_webserver->method() != HTTP_POST) { + _webserver->send(405, "application/json", + "{\"status\":\"error\",\"error\":\"Use POST to start a transfer\"}"); + return; + } + if (!_webserver->hasArg("source") || + !_webserver->hasArg("destination")) { + _webserver->send(400, "application/json", + "{\"status\":\"error\",\"error\":\"source and destination are required\"}"); + return; + } + const String compression = _webserver->hasArg("compression") + ? _webserver->arg("compression") + : "auto"; + const bool dummy = _webserver->hasArg("dummy") && + _webserver->arg("dummy") == "true"; + const bool started = marlin_bft_service.start( + _webserver->arg("source").c_str(), + _webserver->arg("destination").c_str(), compression.c_str(), dummy); + _webserver->send(started ? 202 : 409, "application/json", + marlin_bft_service.statusJson()); + return; + } + if (action == "cancel") { + if (_webserver->method() != HTTP_POST) { + _webserver->send(405, "application/json", + "{\"status\":\"error\",\"error\":\"Use POST to cancel a transfer\"}"); + return; + } + marlin_bft_service.cancel(); + _webserver->send(202, "application/json", + marlin_bft_service.statusJson()); + return; + } + if (action != "status") { + _webserver->send(400, "application/json", + "{\"status\":\"error\",\"error\":\"Unknown action\"}"); + return; + } + _webserver->send(200, "application/json", marlin_bft_service.statusJson()); +} + +#endif // HTTP_FEATURE && MARLIN_BINARY_FILE_TRANSFER_FEATURE diff --git a/esp3d/src/modules/http/http_server.cpp b/esp3d/src/modules/http/http_server.cpp index 46e794e8..5bd46e6d 100644 --- a/esp3d/src/modules/http/http_server.cpp +++ b/esp3d/src/modules/http/http_server.cpp @@ -67,6 +67,9 @@ void HTTP_Server::init_handlers() { #ifdef FILESYSTEM_FEATURE _webserver->on("/files", HTTP_ANY, handleFSFileList, FSFileupload); #endif // FILESYSTEM_FEATURE +#ifdef MARLIN_BINARY_FILE_TRANSFER_FEATURE + _webserver->on("/printer-sd-transfer", HTTP_ANY, handleMarlinBft); +#endif // MARLIN_BINARY_FILE_TRANSFER_FEATURE #if COMMUNICATION_PROTOCOL == MKS_SERIAL // MKS_SERIAL _webserver->on("/upload", HTTP_ANY, handleMKSUpload, MKSFileupload); diff --git a/esp3d/src/modules/http/http_server.h b/esp3d/src/modules/http/http_server.h index c0115ccd..cd909adc 100644 --- a/esp3d/src/modules/http/http_server.h +++ b/esp3d/src/modules/http/http_server.h @@ -81,6 +81,9 @@ class HTTP_Server { static void FSFileupload(); static void handleFSFileList(); #endif // FILESYSTEM_FEATURE +#ifdef MARLIN_BINARY_FILE_TRANSFER_FEATURE + static void handleMarlinBft(); +#endif // MARLIN_BINARY_FILE_TRANSFER_FEATURE #ifdef WEB_UPDATE_FEATURE static void handleUpdate(); static void WebUpdateUpload(); diff --git a/esp3d/src/modules/marlin_bft/marlin_bft_service.cpp b/esp3d/src/modules/marlin_bft/marlin_bft_service.cpp new file mode 100644 index 00000000..058e2980 --- /dev/null +++ b/esp3d/src/modules/marlin_bft/marlin_bft_service.cpp @@ -0,0 +1,639 @@ +/* + marlin_bft_service.cpp - Marlin Binary File Transfer client + + Copyright (c) 2026 ESP3D contributors + + This code is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. +*/ + +#include "marlin_bft_service.h" + +#if defined(MARLIN_BINARY_FILE_TRANSFER_FEATURE) + +#include "../../core/esp3d_commands.h" +#include "../printer_link/printer_link_service.h" +#include "../serial/serial_service.h" +#if defined(USB_SERIAL_FEATURE) +#include "../usb-serial/usb_serial_service.h" +#endif + +MarlinBftService marlin_bft_service; + +namespace { +constexpr uint8_t PROTOCOL_CONTROL = 0; +constexpr uint8_t PROTOCOL_FILE_TRANSFER = 1; +constexpr uint8_t CONTROL_SYNC = 1; +constexpr uint8_t CONTROL_CLOSE = 2; +constexpr uint8_t FILE_QUERY = 0; +constexpr uint8_t FILE_OPEN = 1; +constexpr uint8_t FILE_CLOSE = 2; +constexpr uint8_t FILE_WRITE = 3; +constexpr uint8_t FILE_ABORT = 4; +constexpr uint16_t HEADER_TOKEN = 0xB5AD; +constexpr char PRINTER_LINK_OWNER[] = "binary-transfer"; +} + +bool MarlinBftService::ownsPrinterLink() const { return _linkCaptured; } + +bool MarlinBftService::start(const char *source, const char *destination, + const char *compression, bool dummy) { + if (ownsPrinterLink()) { + return false; + } + if (!source || !destination || !source[0] || !destination[0]) { + _error = "Source and destination are required"; + _state = State::Failed; + return false; + } + + String sourcePath(source), destinationPath(destination); + sourcePath.trim(); + destinationPath.trim(); + if (!sourcePath.length() || !destinationPath.length() || + destinationPath.indexOf("..") >= 0 || destinationPath.length() > 240 || + destinationPath.indexOf('\n') >= 0 || destinationPath.indexOf('\r') >= 0) { + _error = "Invalid destination path"; + _state = State::Failed; + return false; + } + + String mode = compression ? compression : "auto"; + mode.toLowerCase(); + if (mode == "auto") + _compressionMode = CompressionMode::Auto; + else if (mode == "none") + _compressionMode = CompressionMode::None; + else if (mode == "heatshrink") + _compressionMode = CompressionMode::Heatshrink; + else { + _error = "Invalid compression mode"; + _state = State::Failed; + return false; + } + + _sourceFs = ESP_GBFS::getFSType(sourcePath.c_str()); + if (_sourceFs != FS_FLASH && _sourceFs != FS_SD) { + _error = "Source must begin with /FS or /SD"; + _state = State::Failed; + return false; + } + if (!ESP_GBFS::accessFS(_sourceFs)) { + _error = "Source filesystem is busy or unavailable"; + _state = State::Failed; + return false; + } + _sourceFile = ESP_GBFS::open(sourcePath.c_str(), ESP_FILE_READ); + if (!_sourceFile || _sourceFile.isDirectory()) { + releaseSource(); + _error = "Cannot open source file"; + _state = State::Failed; + return false; + } + + _sourcePath = sourcePath; + _destination = destinationPath; + _sourceSize = _sourceFile.size(); + _sourceBytesRead = 0; + _wireBytesSent = 0; + _totalRetries = 0; + _inputSize = _inputOffset = 0; + _payloadSize = 0; + _sourceEof = false; + _encoderFinished = false; + _dummy = dummy; + _finalResult = FinalResult::Completed; + _binaryMode = false; + _cancelRequested = false; + _capabilitySeen = _capabilitySupported = _asciiOkReceived = false; + _enteredReceived = _syncReceived = _resendRequested = false; + _streamErrorReceived = _pftErrorReceived = false; + _ackReceived = _pftReceived = _pftSuccess = false; + _heatshrinkOffered = _compressionEnabled = false; + _windowBits = _lookaheadBits = 0; + _sync = _packetSync = _packetRetries = 0; + _serverPayloadSize = _packetSize = 0; + _lineSize = 0; + _error = ""; + _startedAt = millis(); + + if (!printer_link_service.acquire(PRINTER_LINK_OWNER, receivePrinterData, + this)) { + releaseSource(); + _error = "Printer link is already in use"; + _state = State::Failed; + return false; + } + _linkCaptured = true; + _state = State::CheckingCapabilities; + + static const uint8_t capabilityCommand[] = "M115\n"; + if (!writePrinter(capabilityCommand, sizeof(capabilityCommand) - 1)) { + fail("Cannot send M115"); + return false; + } + _deadline = millis() + RESPONSE_TIMEOUT_MS; + return true; +} + +void MarlinBftService::cancel() { + if (ownsPrinterLink()) _cancelRequested = true; +} + +void MarlinBftService::receivePrinterData(const uint8_t *data, size_t size, + void *context) { + if (context) + static_cast(context)->consumeRx(data, size); +} + +void MarlinBftService::consumeRx(const uint8_t *data, size_t size) { + if (!_linkCaptured) return; + for (size_t i = 0; i < size; ++i) { + const char c = static_cast(data[i]); + if (c == '\n' || c == '\r') { + if (_lineSize) { + _line[_lineSize] = '\0'; + processLine(_line); + _lineSize = 0; + } + } + else if (_lineSize + 1 < sizeof(_line)) { + _line[_lineSize++] = c; + } + else { + _lineSize = 0; + } + } +} + +void MarlinBftService::processLine(const char *line) { + if (!line || !line[0]) return; + + if (_state == State::CheckingCapabilities) { + const char *capability = strstr(line, "Cap:BINARY_FILE_TRANSFER:"); + if (capability) { + _capabilitySeen = true; + capability += strlen("Cap:BINARY_FILE_TRANSFER:"); + _capabilitySupported = capability[0] == '1'; + } + if (!strncmp(line, "ok", 2)) _asciiOkReceived = true; + return; + } + + if (_state == State::EnteringBinaryMode && + strstr(line, "Switching to Binary Protocol")) { + _enteredReceived = true; + return; + } + + if (_state == State::Syncing && line[0] == 's' && line[1] == 's') { + unsigned sync = 0, payload = 0; + if (sscanf(line + 2, "%u,%u", &sync, &payload) == 2 && payload > 0) { + _sync = static_cast(sync); + _serverPayloadSize = min(payload, static_cast(MAX_PAYLOAD_SIZE)); + _syncReceived = true; + } + return; + } + + if (line[0] == 'o' && line[1] == 'k') { + const unsigned received = strtoul(line + 2, nullptr, 10); + if (static_cast(received) == _packetSync) _ackReceived = true; + return; + } + if (line[0] == 'r' && line[1] == 's') { + const unsigned requested = strtoul(line + 2, nullptr, 10); + if (static_cast(requested) == _packetSync) + _resendRequested = true; + return; + } + if (!strncmp(line, "fe", 2)) { + _streamErrorReceived = true; + return; + } + if (!strncmp(line, "PFT:", 4)) { + _pftReceived = true; + _pftSuccess = strstr(line, "PFT:success") == line; + if (_state == State::Querying) { + _pftSuccess = strstr(line, "PFT:version:") == line; + const char *capability = strstr(line, ":compression:heatshrink,"); + if (capability) { + unsigned window = 0, lookahead = 0; + if (sscanf(capability, ":compression:heatshrink,%u,%u", &window, + &lookahead) == 2) { + _heatshrinkOffered = true; + _windowBits = static_cast(window); + _lookaheadBits = static_cast(lookahead); + } + } + } + else if (_state == State::Writing) { + _pftErrorReceived = true; + } + } +} + +void MarlinBftService::handle() { + if (!ownsPrinterLink()) return; + + if (_streamErrorReceived) { + _streamErrorReceived = false; + fail("Marlin reported a binary stream error"); + return; + } + if (_pftErrorReceived) { + _pftErrorReceived = false; + fail("Marlin reported a file write error"); + return; + } + + if (_state == State::CheckingCapabilities && _asciiOkReceived) { + if (!_capabilitySeen || !_capabilitySupported) { + fail("Marlin does not advertise BINARY_FILE_TRANSFER"); + return; + } + _asciiOkReceived = false; + _state = State::EnteringBinaryMode; + static const uint8_t enterCommand[] = "M28 B1\n"; + if (!writePrinter(enterCommand, sizeof(enterCommand) - 1)) { + fail("Cannot send M28 B1"); + return; + } + _deadline = millis() + RESPONSE_TIMEOUT_MS; + return; + } + if (_state == State::EnteringBinaryMode && _enteredReceived) { + _enteredReceived = false; + _binaryMode = true; + _state = State::Syncing; + sendPacket(PROTOCOL_CONTROL, CONTROL_SYNC, nullptr, 0); + return; + } + if (_state == State::Syncing && _syncReceived) { + _syncReceived = false; + _state = State::Querying; + sendPacket(PROTOCOL_FILE_TRANSFER, FILE_QUERY, nullptr, 0); + return; + } + if (_resendRequested) { + _resendRequested = false; + if (!resendPacket()) fail("Marlin requested too many retries"); + return; + } + + if (_cancelRequested && _state != State::Aborting && + _state != State::ClosingConnection) { + sendAbort(FinalResult::Cancelled, "Transfer cancelled"); + return; + } + + processResponses(); + if (!ownsPrinterLink()) return; + + if (static_cast(millis() - _deadline) >= 0) { + if (_state == State::CheckingCapabilities) { + fail("Marlin did not answer the BINARY_FILE_TRANSFER capability query"); + } + else if (_state == State::EnteringBinaryMode) { + fail("Marlin did not enter Binary File Transfer mode"); + } + else if (_ackReceived && (_state == State::Querying || + _state == State::Opening || + _state == State::ClosingFile || + _state == State::Aborting)) { + fail("Marlin operation response timeout"); + } + else if (!resendPacket()) { + fail("Printer response timeout"); + } + } +} + +void MarlinBftService::processResponses() { + if (_state == State::Syncing) return; + if (!_ackReceived) return; + + if (_state == State::Writing) { + _sync++; + _ackReceived = false; + sendNextPayload(); + return; + } + if (_state == State::ClosingConnection) { + _sync++; + finish(_finalResult); + return; + } + if (!_pftReceived) return; + + _sync++; + if (!_pftSuccess) { + fail("Marlin rejected the file-transfer request"); + return; + } + + if (_state == State::Querying) { + if (!prepareCompression()) return; + const size_t nameSize = _destination.length() + 1; + if (nameSize + 2 > _serverPayloadSize) { + fail("Destination name exceeds Marlin payload size"); + return; + } + _payload[0] = _dummy ? 1 : 0; + _payload[1] = _compressionEnabled ? 1 : 0; + memcpy(&_payload[2], _destination.c_str(), nameSize); + _state = State::Opening; + sendPacket(PROTOCOL_FILE_TRANSFER, FILE_OPEN, _payload, nameSize + 2); + } + else if (_state == State::Opening) { + sendNextPayload(); + } + else if (_state == State::ClosingFile) { + sendConnectionClose(); + } + else if (_state == State::Aborting) { + sendConnectionClose(); + } +} + +bool MarlinBftService::prepareCompression() { + _compressionEnabled = _compressionMode != CompressionMode::None && + _heatshrinkOffered; + if (_compressionMode == CompressionMode::Heatshrink && + !_heatshrinkOffered) { + fail("Marlin does not offer heatshrink compression"); + return false; + } + if (_compressionEnabled) { + if (_windowBits < 4 || _windowBits > 15 || _lookaheadBits < 3 || + _lookaheadBits >= _windowBits) { + fail("Marlin announced invalid heatshrink parameters"); + return false; + } + _encoder = heatshrink_encoder_alloc(_windowBits, _lookaheadBits); + if (!_encoder) { + fail("Cannot allocate heatshrink encoder"); + return false; + } + heatshrink_encoder_reset(_encoder); + } + return true; +} + +bool MarlinBftService::prepareNextPayload() { + _payloadSize = 0; + if (!_compressionEnabled) { + _payloadSize = _sourceFile.read(_payload, _serverPayloadSize); + _sourceBytesRead += _payloadSize; + return _payloadSize != 0; + } + + size_t guard = 0; + while (_payloadSize < _serverPayloadSize && guard++ < 4096) { + size_t produced = 0; + const HSE_poll_res pollResult = heatshrink_encoder_poll( + _encoder, &_payload[_payloadSize], _serverPayloadSize - _payloadSize, + &produced); + _payloadSize += produced; + if (pollResult == HSER_POLL_MORE || _payloadSize == _serverPayloadSize) + continue; + + if (_encoderFinished) break; + + if (_inputOffset < _inputSize) { + size_t consumed = 0; + const HSE_sink_res sinkResult = heatshrink_encoder_sink( + _encoder, &_input[_inputOffset], _inputSize - _inputOffset, + &consumed); + if (sinkResult < 0) { + fail("heatshrink rejected input"); + return false; + } + _inputOffset += consumed; + continue; + } + + if (!_sourceEof) { + _inputSize = _sourceFile.read(_input, sizeof(_input)); + _inputOffset = 0; + _sourceBytesRead += _inputSize; + _sourceEof = _inputSize == 0; + if (!_sourceEof) continue; + } + + const HSE_finish_res finishResult = heatshrink_encoder_finish(_encoder); + if (finishResult < 0) { + fail("heatshrink finalization failed"); + return false; + } + _encoderFinished = finishResult == HSER_FINISH_DONE; + if (_encoderFinished) break; + } + if (guard >= 4096) { + fail("heatshrink encoder made no progress"); + return false; + } + return _payloadSize != 0; +} + +bool MarlinBftService::sendNextPayload() { + if (!prepareNextPayload()) { + if (_state == State::Failed || _state == State::Aborting) return false; + _state = State::ClosingFile; + return sendPacket(PROTOCOL_FILE_TRANSFER, FILE_CLOSE, nullptr, 0); + } + _state = State::Writing; + return sendPacket(PROTOCOL_FILE_TRANSFER, FILE_WRITE, _payload, _payloadSize); +} + +uint16_t MarlinBftService::checksum(uint16_t value, uint8_t byte) { + const uint16_t low = ((value & 0xFF) + byte) % 255; + return ((((value >> 8) + low) % 255) << 8) | low; +} + +bool MarlinBftService::sendPacket(uint8_t protocol, uint8_t type, + const uint8_t *payload, + size_t payloadSize) { + if (payloadSize > MAX_PAYLOAD_SIZE) { + fail("Binary packet is too large"); + return false; + } + _packet[0] = HEADER_TOKEN & 0xFF; + _packet[1] = HEADER_TOKEN >> 8; + _packet[2] = _sync; + _packet[3] = (protocol << 4) | (type & 0x0F); + _packet[4] = payloadSize & 0xFF; + _packet[5] = (payloadSize >> 8) & 0xFF; + + uint16_t sum = 0; + for (size_t i = 2; i < 6; ++i) sum = checksum(sum, _packet[i]); + const uint16_t headerSum = sum; + _packet[6] = headerSum & 0xFF; + _packet[7] = headerSum >> 8; + sum = checksum(sum, _packet[6]); + sum = checksum(sum, _packet[7]); + if (payloadSize) { + memcpy(&_packet[8], payload, payloadSize); + for (size_t i = 0; i < payloadSize; ++i) + sum = checksum(sum, payload[i]); + } + if (payloadSize) { + _packet[8 + payloadSize] = sum & 0xFF; + _packet[9 + payloadSize] = sum >> 8; + } + _packetSize = payloadSize ? payloadSize + 10 : 8; + _packetSync = _sync; + _packetRetries = 0; + _ackReceived = _pftReceived = _pftSuccess = false; + if (!writePrinter(_packet, _packetSize)) { + fail("Cannot write binary packet"); + return false; + } + _wireBytesSent += _packetSize; + _deadline = millis() + RESPONSE_TIMEOUT_MS; + return true; +} + +bool MarlinBftService::resendPacket() { + if (!_packetSize || _packetRetries >= MAX_RETRIES) return false; + ++_packetRetries; + ++_totalRetries; + _ackReceived = false; + if (!writePrinter(_packet, _packetSize)) return false; + _wireBytesSent += _packetSize; + _deadline = millis() + RESPONSE_TIMEOUT_MS; + return true; +} + +void MarlinBftService::sendAbort(FinalResult result, const char *reason) { + _finalResult = result; + if (reason) _error = reason; + _cancelRequested = false; + if (!_binaryMode) { + finish(result); + return; + } + _state = State::Aborting; + sendPacket(PROTOCOL_FILE_TRANSFER, FILE_ABORT, nullptr, 0); +} + +void MarlinBftService::sendConnectionClose() { + _state = State::ClosingConnection; + sendPacket(PROTOCOL_CONTROL, CONTROL_CLOSE, nullptr, 0); +} + +void MarlinBftService::fail(const char *reason) { + if (reason) _error = reason; + if (_binaryMode && _state != State::Aborting && + _state != State::ClosingConnection) { + sendAbort(FinalResult::Failed, reason); + } + else { + finish(FinalResult::Failed); + } +} + +void MarlinBftService::finish(FinalResult result) { + _binaryMode = false; + _cancelRequested = false; + if (_encoder) { + heatshrink_encoder_free(_encoder); + _encoder = nullptr; + } + releaseSource(); + if (result == FinalResult::Completed) { + _error = ""; + _state = State::Completed; + } + else if (result == FinalResult::Cancelled) { + _state = State::Cancelled; + } + else { + _state = State::Failed; + } + if (_linkCaptured) { + printer_link_service.release(PRINTER_LINK_OWNER); + _linkCaptured = false; + } +} + +void MarlinBftService::releaseSource() { + if (_sourceFile) _sourceFile.close(); + if (_sourceFs != FS_UNKNOWN) ESP_GBFS::releaseFS(_sourceFs); + _sourceFs = FS_UNKNOWN; +} + +bool MarlinBftService::writePrinter(const uint8_t *data, size_t size) { + const ESP3DClientType output = esp3d_commands.getOutputClient(); +#if defined(USB_SERIAL_FEATURE) + if (output == ESP3DClientType::usb_serial) + return esp3d_usb_serial_service.writeBytes(data, size) == size; +#endif + if (output == ESP3DClientType::serial) + return esp3d_serial_service.writeBytes(data, size) == size; + return false; +} + +const char *MarlinBftService::stateName(State state) { + switch (state) { + case State::Idle: return "idle"; + case State::CheckingCapabilities: return "checking_capabilities"; + case State::EnteringBinaryMode: return "entering"; + case State::Syncing: return "syncing"; + case State::Querying: return "querying"; + case State::Opening: return "opening"; + case State::Writing: return "writing"; + case State::ClosingFile: return "closing_file"; + case State::Aborting: return "aborting"; + case State::ClosingConnection: return "closing_connection"; + case State::Completed: return "completed"; + case State::Cancelled: return "cancelled"; + case State::Failed: return "failed"; + } + return "unknown"; +} + +String MarlinBftService::jsonEscape(const String &value) { + String escaped; + escaped.reserve(value.length() + 8); + for (size_t i = 0; i < value.length(); ++i) { + const char c = value[i]; + if (c == '\\' || c == '"') escaped += '\\'; + if (c == '\n') escaped += "\\n"; + else if (c == '\r') escaped += "\\r"; + else escaped += c; + } + return escaped; +} + +String MarlinBftService::statusJson() const { + const bool active = ownsPrinterLink(); + const unsigned progress = _sourceSize + ? static_cast((100ULL * _sourceBytesRead) / _sourceSize) + : (active ? 0 : 100); + String json; + json.reserve(384); + json = "{\"status\":\""; + json += stateName(_state); + json += "\",\"active\":"; + json += active ? "true" : "false"; + json += ",\"source\":\"" + jsonEscape(_sourcePath); + json += "\",\"destination\":\"" + jsonEscape(_destination); + json += "\",\"source_size\":" + String(_sourceSize); + json += ",\"source_bytes_read\":" + String(_sourceBytesRead); + json += ",\"wire_bytes_sent\":" + String(_wireBytesSent); + json += ",\"progress\":" + String(progress); + json += ",\"compression\":\""; + json += _compressionEnabled ? "heatshrink" : "none"; + json += "\",\"window_bits\":" + String(_windowBits); + json += ",\"lookahead_bits\":" + String(_lookaheadBits); + json += ",\"retries\":" + String(_totalRetries); + const uint32_t elapsed = _startedAt ? millis() - _startedAt : 0; + json += ",\"elapsed_ms\":" + String(elapsed); + json += ",\"error\":\"" + jsonEscape(_error) + "\"}"; + return json; +} + +#endif // MARLIN_BINARY_FILE_TRANSFER_FEATURE diff --git a/esp3d/src/modules/marlin_bft/marlin_bft_service.h b/esp3d/src/modules/marlin_bft/marlin_bft_service.h new file mode 100644 index 00000000..a1f99aa8 --- /dev/null +++ b/esp3d/src/modules/marlin_bft/marlin_bft_service.h @@ -0,0 +1,133 @@ +/* + marlin_bft_service.h - Marlin Binary File Transfer client + + Copyright (c) 2026 ESP3D contributors + + This code is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. +*/ + +#pragma once + +#include "../../include/esp3d_config.h" + +#if defined(MARLIN_BINARY_FILE_TRANSFER_FEATURE) + +#include +extern "C" { +#include +} + +#include "../filesystem/esp_globalFS.h" + +class MarlinBftService final { + public: + enum class State : uint8_t { + Idle, + CheckingCapabilities, + EnteringBinaryMode, + Syncing, + Querying, + Opening, + Writing, + ClosingFile, + Aborting, + ClosingConnection, + Completed, + Cancelled, + Failed + }; + + bool start(const char *source, const char *destination, + const char *compression = "auto", bool dummy = false); + void cancel(); + void handle(); + bool ownsPrinterLink() const; + String statusJson() const; + + private: + enum class CompressionMode : uint8_t { Auto, None, Heatshrink }; + enum class FinalResult : uint8_t { Completed, Cancelled, Failed }; + + static constexpr size_t MAX_PAYLOAD_SIZE = 512; + static constexpr size_t MAX_PACKET_SIZE = MAX_PAYLOAD_SIZE + 10; + static constexpr uint32_t RESPONSE_TIMEOUT_MS = 2500; + static constexpr uint8_t MAX_RETRIES = 5; + + bool writePrinter(const uint8_t *data, size_t size); + static void receivePrinterData(const uint8_t *data, size_t size, + void *context); + void consumeRx(const uint8_t *data, size_t size); + bool sendPacket(uint8_t protocol, uint8_t type, const uint8_t *payload, + size_t payloadSize); + bool resendPacket(); + void processLine(const char *line); + void processResponses(); + bool prepareCompression(); + bool prepareNextPayload(); + bool sendNextPayload(); + void sendAbort(FinalResult result, const char *reason); + void sendConnectionClose(); + void finish(FinalResult result); + void fail(const char *reason); + void releaseSource(); + static uint16_t checksum(uint16_t value, uint8_t byte); + static const char *stateName(State state); + static String jsonEscape(const String &value); + + State _state = State::Idle; + FinalResult _finalResult = FinalResult::Completed; + CompressionMode _compressionMode = CompressionMode::Auto; + ESP_GBFile _sourceFile; + uint8_t _sourceFs = FS_UNKNOWN; + String _sourcePath; + String _destination; + String _error; + bool _dummy = false; + bool _linkCaptured = false; + bool _binaryMode = false; + bool _cancelRequested = false; + bool _capabilitySeen = false; + bool _capabilitySupported = false; + bool _asciiOkReceived = false; + bool _enteredReceived = false; + bool _syncReceived = false; + bool _resendRequested = false; + bool _streamErrorReceived = false; + bool _pftErrorReceived = false; + bool _ackReceived = false; + bool _pftReceived = false; + bool _pftSuccess = false; + bool _heatshrinkOffered = false; + bool _compressionEnabled = false; + bool _sourceEof = false; + bool _encoderFinished = false; + uint8_t _windowBits = 0; + uint8_t _lookaheadBits = 0; + uint8_t _sync = 0; + uint8_t _packetSync = 0; + uint8_t _packetRetries = 0; + uint32_t _totalRetries = 0; + size_t _serverPayloadSize = 0; + size_t _packetSize = 0; + size_t _payloadSize = 0; + size_t _inputSize = 0; + size_t _inputOffset = 0; + size_t _sourceSize = 0; + size_t _sourceBytesRead = 0; + size_t _wireBytesSent = 0; + uint32_t _deadline = 0; + uint32_t _startedAt = 0; + uint8_t _packet[MAX_PACKET_SIZE] = {}; + uint8_t _payload[MAX_PAYLOAD_SIZE] = {}; + uint8_t _input[512] = {}; + char _line[256] = {}; + size_t _lineSize = 0; + heatshrink_encoder *_encoder = nullptr; +}; + +extern MarlinBftService marlin_bft_service; + +#endif // MARLIN_BINARY_FILE_TRANSFER_FEATURE diff --git a/esp3d/src/modules/printer_link/printer_link_service.cpp b/esp3d/src/modules/printer_link/printer_link_service.cpp new file mode 100644 index 00000000..e180c1ab --- /dev/null +++ b/esp3d/src/modules/printer_link/printer_link_service.cpp @@ -0,0 +1,82 @@ +/* + printer_link_service.cpp - Exclusive access to the printer connection + + Copyright (c) 2026 ESP3D contributors + + This code is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. +*/ + +#include "printer_link_service.h" + +#if defined(HTTP_FEATURE) +#include "../websocket/websocket_server.h" +#endif + +PrinterLinkService printer_link_service; + +bool PrinterLinkService::acquire(const char *owner, + PrinterLinkRxHandler handler, + void *context) { + if (!owner || !owner[0] || _captured) return false; + + _ownerKey = owner; + _owner = owner; + _owner.trim(); + _owner.replace(":", "_"); + _owner.replace("\r", "_"); + _owner.replace("\n", "_"); + if (!_owner.length()) { + _ownerKey = ""; + return false; + } + if (_owner.length() > 32) _owner.remove(32); + + _handler = handler; + _context = context; + _captured = true; + notifyWebUi(); + return true; +} + +bool PrinterLinkService::release(const char *owner) { + if (!_captured || !owner || _ownerKey != owner) return false; + + // Stop diverting RX before invalidating the callback and its context. + _captured = false; + _handler = nullptr; + _context = nullptr; + _ownerKey = ""; + _owner = ""; + notifyWebUi(); + return true; +} + +bool PrinterLinkService::consumeRx(const uint8_t *data, size_t size) { + if (!_captured) return false; + if (_handler && data && size) _handler(data, size, _context); + return true; +} + +String PrinterLinkService::eventMessage() const { + if (_captured) return "printerLink:captured:" + _owner; + return "printerLink:released"; +} + +void PrinterLinkService::notifyWebUi() const { +#if defined(HTTP_FEATURE) + if (websocket_terminal_server.started()) + websocket_terminal_server.pushMSG(eventMessage().c_str()); +#endif +} + +void PrinterLinkService::notifyWebUi(uint8_t clientId) const { +#if defined(HTTP_FEATURE) + if (websocket_terminal_server.started()) + websocket_terminal_server.pushMSG(clientId, eventMessage().c_str()); +#else + (void)clientId; +#endif +} diff --git a/esp3d/src/modules/printer_link/printer_link_service.h b/esp3d/src/modules/printer_link/printer_link_service.h new file mode 100644 index 00000000..a6abf6de --- /dev/null +++ b/esp3d/src/modules/printer_link/printer_link_service.h @@ -0,0 +1,49 @@ +/* + printer_link_service.h - Exclusive access to the printer connection + + Copyright (c) 2026 ESP3D contributors + + This code is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. +*/ + +#pragma once + +#include "../../include/esp3d_config.h" + +using PrinterLinkRxHandler = + void (*)(const uint8_t *data, size_t size, void *context); + +class PrinterLinkService final { + public: + // Capture all printer RX and block normal printer TX. The owner string is + // also exposed to the WebUI. A null handler captures and discards RX. + bool acquire(const char *owner, PrinterLinkRxHandler handler = nullptr, + void *context = nullptr); + + // Release only when the caller supplies the owner used by acquire(). + bool release(const char *owner); + + bool captured() const { return _captured; } + const char *owner() const { return _owner.c_str(); } + + // Returns true when RX belongs to the current exclusive owner. + bool consumeRx(const uint8_t *data, size_t size); + + // Send current state to one newly connected WebUI client. + void notifyWebUi(uint8_t clientId) const; + + private: + void notifyWebUi() const; + String eventMessage() const; + + volatile bool _captured = false; + String _ownerKey; + String _owner; + PrinterLinkRxHandler _handler = nullptr; + void *_context = nullptr; +}; + +extern PrinterLinkService printer_link_service; diff --git a/esp3d/src/modules/serial/serial_service_esp32.cpp b/esp3d/src/modules/serial/serial_service_esp32.cpp index b341fae9..3d86c4c0 100644 --- a/esp3d/src/modules/serial/serial_service_esp32.cpp +++ b/esp3d/src/modules/serial/serial_service_esp32.cpp @@ -25,6 +25,7 @@ #include "../../core/esp3d_settings.h" #include "../../core/esp3d_string.h" #include "../authentication/authentication_service.h" +#include "../printer_link/printer_link_service.h" #include "serial_service.h" #define SERIAL_COMMUNICATION_TIMEOUT 500 @@ -102,6 +103,8 @@ void ESP3DSerialService::receiveCb() { } //take the char count--; + const uint8_t received = static_cast(data); + if (printer_link_service.consumeRx(&received, 1)) continue; _buffer[_buffer_size] = (uint8_t)data; //check what next step is if (esp3d_string::isRealTimeCommand(_buffer[_buffer_size])) { diff --git a/esp3d/src/modules/serial/serial_service_esp8266.cpp b/esp3d/src/modules/serial/serial_service_esp8266.cpp index 7b6fa0e9..2f13a1c3 100644 --- a/esp3d/src/modules/serial/serial_service_esp8266.cpp +++ b/esp3d/src/modules/serial/serial_service_esp8266.cpp @@ -29,6 +29,7 @@ #include "../mks/mks_service.h" #endif // COMMUNICATION_PROTOCOL == MKS_SERIAL #include "../authentication/authentication_service.h" +#include "../printer_link/printer_link_service.h" #define MAX_SERIAL 2 HardwareSerial *Serials[MAX_SERIAL] = {&Serial, &Serial1}; @@ -132,7 +133,9 @@ void ESP3DSerialService::handle() { size_t count = readBytes(sbuf, len); // push to buffer if (count > 0) { - push2buffer(sbuf, count); + if (!printer_link_service.consumeRx(sbuf, count)) { + push2buffer(sbuf, count); + } } // freen buffer free(sbuf); @@ -276,4 +279,4 @@ void ESP3DSerialService::swap() { #endif // COMMUNICATION_PROTOCOL == MKS_SERIAL || COMMUNICATION_PROTOCOL == // RAW_SERIAL -#endif // ARDUINO_ARCH_ESP8266 \ No newline at end of file +#endif // ARDUINO_ARCH_ESP8266 diff --git a/esp3d/src/modules/usb-serial/usb_serial_service.cpp b/esp3d/src/modules/usb-serial/usb_serial_service.cpp index 794ce411..8fa83c9f 100644 --- a/esp3d/src/modules/usb-serial/usb_serial_service.cpp +++ b/esp3d/src/modules/usb-serial/usb_serial_service.cpp @@ -24,6 +24,7 @@ #include "../../core/esp3d_settings.h" #include "../../core/esp3d_string.h" #include "../authentication/authentication_service.h" +#include "../printer_link/printer_link_service.h" #include "usb_serial_service.h" #if defined(NOTIFICATION_FEATURE) @@ -172,6 +173,7 @@ void ESP3DUsbSerialService::receiveCb(const uint8_t *data, size_t data_len, if (!started()) { return; } + if (printer_link_service.consumeRx(data, data_len)) return; if (xSemaphoreTake(_buffer_mutex, portMAX_DELAY) == pdTRUE) { for (size_t i = 0; i < data_len; i++) { if (esp3d_string::isRealTimeCommand(data[i])) { @@ -439,24 +441,27 @@ size_t ESP3DUsbSerialService::writeBytes(const uint8_t *buffer, size_t size) { esp3d_log_e("USB Serial not started or not connected"); return 0; } - esp3d_log("writeBytes %d : %s", size, (const char *)buffer); - if (_vcp_ptr && _vcp_ptr->tx_blocking((uint8_t *)buffer, size) == ESP_OK) { - if (!(_vcp_ptr && _vcp_ptr->set_control_line_state(true, true) == ESP_OK)) { - esp3d_log_e("Failed to set control line state"); - return 0; - - esp3d_log_e("Failed to send message"); - return 0; - } - return size; - } + esp3d_log("writeBytes %d", size); if (!_vcp_ptr) { esp3d_log_e("_vcp_ptr is null"); - } else { - esp3d_log_e("tx_blocking failed"); + return 0; } - esp3d_log_e("Failed to send message"); - return 0; + if (_vcp_ptr->set_control_line_state(true, true) != ESP_OK) { + esp3d_log_e("Failed to set control line state"); + return 0; + } + + size_t sent = 0; + while (sent < size) { + const size_t chunk = + min(size - sent, static_cast(ESP3D_USB_SERIAL_TX_BUFFER_SIZE)); + if (_vcp_ptr->tx_blocking((uint8_t *)&buffer[sent], chunk) != ESP_OK) { + esp3d_log_e("tx_blocking failed after %d bytes", sent); + return sent; + } + sent += chunk; + } + return sent; } size_t ESP3DUsbSerialService::readBytes(uint8_t *sbuf, size_t len) { @@ -508,4 +513,4 @@ bool ESP3DUsbSerialService::dispatch(ESP3DMessage *message) { return done; } -#endif // USB_SERIAL_FEATURE \ No newline at end of file +#endif // USB_SERIAL_FEATURE diff --git a/esp3d/src/modules/websocket/websocket_server.cpp b/esp3d/src/modules/websocket/websocket_server.cpp index f3546c2d..acb65477 100644 --- a/esp3d/src/modules/websocket/websocket_server.cpp +++ b/esp3d/src/modules/websocket/websocket_server.cpp @@ -30,6 +30,7 @@ #include "../../core/esp3d_settings.h" #include "../../core/esp3d_string.h" #include "../authentication/authentication_service.h" +#include "../printer_link/printer_link_service.h" #include "websocket_server.h" WebSocket_Server websocket_terminal_server("webui-v3", @@ -142,6 +143,7 @@ void handle_Websocket_Terminal_Event(uint8_t num, uint8_t type, websocket_terminal_server.pushMSG(num, msg.c_str()); msg = "activeID:" + String(num); websocket_terminal_server.pushMSG(msg.c_str()); + printer_link_service.notifyWebUi(num); websocket_terminal_server.enableOnly(num); esp3d_log_d("[%u] Socket connected port %d", num, websocket_terminal_server.port()); diff --git a/libraries/heatshrink-0.4.1/LICENSE b/libraries/heatshrink-0.4.1/LICENSE new file mode 100644 index 00000000..9132cb6a --- /dev/null +++ b/libraries/heatshrink-0.4.1/LICENSE @@ -0,0 +1,14 @@ +Copyright (c) 2013-2015, Scott Vokes +All rights reserved. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/libraries/heatshrink-0.4.1/README.ESP3D.md b/libraries/heatshrink-0.4.1/README.ESP3D.md new file mode 100644 index 00000000..1c12d121 --- /dev/null +++ b/libraries/heatshrink-0.4.1/README.ESP3D.md @@ -0,0 +1,12 @@ +# Vendored heatshrink encoder + +This directory contains the encoder and license from the upstream +`v0.4.1` tag (`b9ac05e`) of +. + +Only the encoder is required. Marlin provides the matching decoder and +announces its window and lookahead parameters during Binary File Transfer +capability negotiation. + +The encoder includes the finalization fix from upstream commit `3b85e98` +(atomicobject/heatshrink#87), applied separately from the pristine import. diff --git a/libraries/heatshrink-0.4.1/library.properties b/libraries/heatshrink-0.4.1/library.properties new file mode 100644 index 00000000..0fe2972d --- /dev/null +++ b/libraries/heatshrink-0.4.1/library.properties @@ -0,0 +1,10 @@ +name=heatshrink +version=0.4.1 +author=Scott Vokes +maintainer=Scott Vokes +sentence=An embedded data compression library. +paragraph=Vendored heatshrink encoder used for Marlin Binary File Transfer. +category=Data Processing +url=https://github.com/atomicobject/heatshrink +architectures=* + diff --git a/libraries/heatshrink-0.4.1/src/heatshrink_common.h b/libraries/heatshrink-0.4.1/src/heatshrink_common.h new file mode 100644 index 00000000..bc89774f --- /dev/null +++ b/libraries/heatshrink-0.4.1/src/heatshrink_common.h @@ -0,0 +1,20 @@ +#ifndef HEATSHRINK_H +#define HEATSHRINK_H + +#define HEATSHRINK_AUTHOR "Scott Vokes " +#define HEATSHRINK_URL "https://github.com/atomicobject/heatshrink" + +/* Version 0.4.1 */ +#define HEATSHRINK_VERSION_MAJOR 0 +#define HEATSHRINK_VERSION_MINOR 4 +#define HEATSHRINK_VERSION_PATCH 1 + +#define HEATSHRINK_MIN_WINDOW_BITS 4 +#define HEATSHRINK_MAX_WINDOW_BITS 15 + +#define HEATSHRINK_MIN_LOOKAHEAD_BITS 3 + +#define HEATSHRINK_LITERAL_MARKER 0x01 +#define HEATSHRINK_BACKREF_MARKER 0x00 + +#endif diff --git a/libraries/heatshrink-0.4.1/src/heatshrink_config.h b/libraries/heatshrink-0.4.1/src/heatshrink_config.h new file mode 100644 index 00000000..13135b93 --- /dev/null +++ b/libraries/heatshrink-0.4.1/src/heatshrink_config.h @@ -0,0 +1,26 @@ +#ifndef HEATSHRINK_CONFIG_H +#define HEATSHRINK_CONFIG_H + +/* Should functionality assuming dynamic allocation be used? */ +#ifndef HEATSHRINK_DYNAMIC_ALLOC +#define HEATSHRINK_DYNAMIC_ALLOC 1 +#endif + +#if HEATSHRINK_DYNAMIC_ALLOC + /* Optional replacement of malloc/free */ + #define HEATSHRINK_MALLOC(SZ) malloc(SZ) + #define HEATSHRINK_FREE(P, SZ) free(P) +#else + /* Required parameters for static configuration */ + #define HEATSHRINK_STATIC_INPUT_BUFFER_SIZE 32 + #define HEATSHRINK_STATIC_WINDOW_BITS 8 + #define HEATSHRINK_STATIC_LOOKAHEAD_BITS 4 +#endif + +/* Turn on logging for debugging. */ +#define HEATSHRINK_DEBUGGING_LOGS 0 + +/* Use indexing for faster compression. (This requires additional space.) */ +#define HEATSHRINK_USE_INDEX 1 + +#endif diff --git a/libraries/heatshrink-0.4.1/src/heatshrink_encoder.c b/libraries/heatshrink-0.4.1/src/heatshrink_encoder.c new file mode 100644 index 00000000..9bea0fc4 --- /dev/null +++ b/libraries/heatshrink-0.4.1/src/heatshrink_encoder.c @@ -0,0 +1,606 @@ +#include +#include +#include +#include "heatshrink_encoder.h" + +typedef enum { + HSES_NOT_FULL, /* input buffer not full enough */ + HSES_FILLED, /* buffer is full */ + HSES_SEARCH, /* searching for patterns */ + HSES_YIELD_TAG_BIT, /* yield tag bit */ + HSES_YIELD_LITERAL, /* emit literal byte */ + HSES_YIELD_BR_INDEX, /* yielding backref index */ + HSES_YIELD_BR_LENGTH, /* yielding backref length */ + HSES_SAVE_BACKLOG, /* copying buffer to backlog */ + HSES_FLUSH_BITS, /* flush bit buffer */ + HSES_DONE, /* done */ +} HSE_state; + +#if HEATSHRINK_DEBUGGING_LOGS +#include +#include +#include +#define LOG(...) fprintf(stderr, __VA_ARGS__) +#define ASSERT(X) assert(X) +static const char *state_names[] = { + "not_full", + "filled", + "search", + "yield_tag_bit", + "yield_literal", + "yield_br_index", + "yield_br_length", + "save_backlog", + "flush_bits", + "done", +}; +#else +#define LOG(...) /* no-op */ +#define ASSERT(X) /* no-op */ +#endif + +// Encoder flags +enum { + FLAG_IS_FINISHING = 0x01, +}; + +typedef struct { + uint8_t *buf; /* output buffer */ + size_t buf_size; /* buffer size */ + size_t *output_size; /* bytes pushed to buffer, so far */ +} output_info; + +#define MATCH_NOT_FOUND ((uint16_t)-1) + +static uint16_t get_input_offset(heatshrink_encoder *hse); +static uint16_t get_input_buffer_size(heatshrink_encoder *hse); +static uint16_t get_lookahead_size(heatshrink_encoder *hse); +static void add_tag_bit(heatshrink_encoder *hse, output_info *oi, uint8_t tag); +static int can_take_byte(output_info *oi); +static int is_finishing(heatshrink_encoder *hse); +static void save_backlog(heatshrink_encoder *hse); + +/* Push COUNT (max 8) bits to the output buffer, which has room. */ +static void push_bits(heatshrink_encoder *hse, uint8_t count, uint8_t bits, + output_info *oi); +static uint8_t push_outgoing_bits(heatshrink_encoder *hse, output_info *oi); +static void push_literal_byte(heatshrink_encoder *hse, output_info *oi); + +#if HEATSHRINK_DYNAMIC_ALLOC +heatshrink_encoder *heatshrink_encoder_alloc(uint8_t window_sz2, + uint8_t lookahead_sz2) { + if ((window_sz2 < HEATSHRINK_MIN_WINDOW_BITS) || + (window_sz2 > HEATSHRINK_MAX_WINDOW_BITS) || + (lookahead_sz2 < HEATSHRINK_MIN_LOOKAHEAD_BITS) || + (lookahead_sz2 >= window_sz2)) { + return NULL; + } + + /* Note: 2 * the window size is used because the buffer needs to fit + * (1 << window_sz2) bytes for the current input, and an additional + * (1 << window_sz2) bytes for the previous buffer of input, which + * will be scanned for useful backreferences. */ + size_t buf_sz = (2 << window_sz2); + + heatshrink_encoder *hse = HEATSHRINK_MALLOC(sizeof(*hse) + buf_sz); + if (hse == NULL) { return NULL; } + hse->window_sz2 = window_sz2; + hse->lookahead_sz2 = lookahead_sz2; + heatshrink_encoder_reset(hse); + +#if HEATSHRINK_USE_INDEX + size_t index_sz = buf_sz*sizeof(uint16_t); + hse->search_index = HEATSHRINK_MALLOC(index_sz + sizeof(struct hs_index)); + if (hse->search_index == NULL) { + HEATSHRINK_FREE(hse, sizeof(*hse) + buf_sz); + return NULL; + } + hse->search_index->size = index_sz; +#endif + + LOG("-- allocated encoder with buffer size of %zu (%u byte input size)\n", + buf_sz, get_input_buffer_size(hse)); + return hse; +} + +void heatshrink_encoder_free(heatshrink_encoder *hse) { + size_t buf_sz = (2 << HEATSHRINK_ENCODER_WINDOW_BITS(hse)); +#if HEATSHRINK_USE_INDEX + size_t index_sz = sizeof(struct hs_index) + hse->search_index->size; + HEATSHRINK_FREE(hse->search_index, index_sz); + (void)index_sz; +#endif + HEATSHRINK_FREE(hse, sizeof(heatshrink_encoder) + buf_sz); + (void)buf_sz; +} +#endif + +void heatshrink_encoder_reset(heatshrink_encoder *hse) { + size_t buf_sz = (2 << HEATSHRINK_ENCODER_WINDOW_BITS(hse)); + memset(hse->buffer, 0, buf_sz); + hse->input_size = 0; + hse->state = HSES_NOT_FULL; + hse->match_scan_index = 0; + hse->flags = 0; + hse->bit_index = 0x80; + hse->current_byte = 0x00; + hse->match_length = 0; + + hse->outgoing_bits = 0x0000; + hse->outgoing_bits_count = 0; + + #ifdef LOOP_DETECT + hse->loop_detect = (uint32_t)-1; + #endif +} + +HSE_sink_res heatshrink_encoder_sink(heatshrink_encoder *hse, + uint8_t *in_buf, size_t size, size_t *input_size) { + if ((hse == NULL) || (in_buf == NULL) || (input_size == NULL)) { + return HSER_SINK_ERROR_NULL; + } + + /* Sinking more content after saying the content is done, tsk tsk */ + if (is_finishing(hse)) { return HSER_SINK_ERROR_MISUSE; } + + /* Sinking more content before processing is done */ + if (hse->state != HSES_NOT_FULL) { return HSER_SINK_ERROR_MISUSE; } + + uint16_t write_offset = get_input_offset(hse) + hse->input_size; + uint16_t ibs = get_input_buffer_size(hse); + uint16_t rem = ibs - hse->input_size; + uint16_t cp_sz = rem < size ? rem : size; + + memcpy(&hse->buffer[write_offset], in_buf, cp_sz); + *input_size = cp_sz; + hse->input_size += cp_sz; + + LOG("-- sunk %u bytes (of %zu) into encoder at %d, input buffer now has %u\n", + cp_sz, size, write_offset, hse->input_size); + if (cp_sz == rem) { + LOG("-- internal buffer is now full\n"); + hse->state = HSES_FILLED; + } + + return HSER_SINK_OK; +} + + +/*************** + * Compression * + ***************/ + +static uint16_t find_longest_match(heatshrink_encoder *hse, uint16_t start, + uint16_t end, const uint16_t maxlen, uint16_t *match_length); +static void do_indexing(heatshrink_encoder *hse); + +static HSE_state st_step_search(heatshrink_encoder *hse); +static HSE_state st_yield_tag_bit(heatshrink_encoder *hse, + output_info *oi); +static HSE_state st_yield_literal(heatshrink_encoder *hse, + output_info *oi); +static HSE_state st_yield_br_index(heatshrink_encoder *hse, + output_info *oi); +static HSE_state st_yield_br_length(heatshrink_encoder *hse, + output_info *oi); +static HSE_state st_save_backlog(heatshrink_encoder *hse); +static HSE_state st_flush_bit_buffer(heatshrink_encoder *hse, + output_info *oi); + +HSE_poll_res heatshrink_encoder_poll(heatshrink_encoder *hse, + uint8_t *out_buf, size_t out_buf_size, size_t *output_size) { + if ((hse == NULL) || (out_buf == NULL) || (output_size == NULL)) { + return HSER_POLL_ERROR_NULL; + } + if (out_buf_size == 0) { + LOG("-- MISUSE: output buffer size is 0\n"); + return HSER_POLL_ERROR_MISUSE; + } + *output_size = 0; + + output_info oi; + oi.buf = out_buf; + oi.buf_size = out_buf_size; + oi.output_size = output_size; + + while (1) { + LOG("-- polling, state %u (%s), flags 0x%02x\n", + hse->state, state_names[hse->state], hse->flags); + + uint8_t in_state = hse->state; + switch (in_state) { + case HSES_NOT_FULL: + return HSER_POLL_EMPTY; + case HSES_FILLED: + do_indexing(hse); + hse->state = HSES_SEARCH; + break; + case HSES_SEARCH: + hse->state = st_step_search(hse); + break; + case HSES_YIELD_TAG_BIT: + hse->state = st_yield_tag_bit(hse, &oi); + break; + case HSES_YIELD_LITERAL: + hse->state = st_yield_literal(hse, &oi); + break; + case HSES_YIELD_BR_INDEX: + hse->state = st_yield_br_index(hse, &oi); + break; + case HSES_YIELD_BR_LENGTH: + hse->state = st_yield_br_length(hse, &oi); + break; + case HSES_SAVE_BACKLOG: + hse->state = st_save_backlog(hse); + break; + case HSES_FLUSH_BITS: + hse->state = st_flush_bit_buffer(hse, &oi); + case HSES_DONE: + return HSER_POLL_EMPTY; + default: + LOG("-- bad state %s\n", state_names[hse->state]); + return HSER_POLL_ERROR_MISUSE; + } + + if (hse->state == in_state) { + /* Check if output buffer is exhausted. */ + if (*output_size == out_buf_size) return HSER_POLL_MORE; + } + } +} + +HSE_finish_res heatshrink_encoder_finish(heatshrink_encoder *hse) { + if (hse == NULL) { return HSER_FINISH_ERROR_NULL; } + LOG("-- setting is_finishing flag\n"); + hse->flags |= FLAG_IS_FINISHING; + if (hse->state == HSES_NOT_FULL) { hse->state = HSES_FILLED; } + return hse->state == HSES_DONE ? HSER_FINISH_DONE : HSER_FINISH_MORE; +} + +static HSE_state st_step_search(heatshrink_encoder *hse) { + uint16_t window_length = get_input_buffer_size(hse); + uint16_t lookahead_sz = get_lookahead_size(hse); + uint16_t msi = hse->match_scan_index; + LOG("## step_search, scan @ +%d (%d/%d), input size %d\n", + msi, hse->input_size + msi, 2*window_length, hse->input_size); + + bool fin = is_finishing(hse); + if (fin && hse->input_size == 0) { + return HSES_FLUSH_BITS; + } else if (msi > hse->input_size - (fin ? 1 : lookahead_sz)) { + /* Current search buffer is exhausted, copy it into the + * backlog and await more input. */ + LOG("-- end of search @ %d\n", msi); + return fin ? HSES_FLUSH_BITS : HSES_SAVE_BACKLOG; + } + + uint16_t input_offset = get_input_offset(hse); + uint16_t end = input_offset + msi; + uint16_t start = end - window_length; + + uint16_t max_possible = lookahead_sz; + if (hse->input_size - msi < lookahead_sz) { + max_possible = hse->input_size - msi; + } + + uint16_t match_length = 0; + uint16_t match_pos = find_longest_match(hse, + start, end, max_possible, &match_length); + + if (match_pos == MATCH_NOT_FOUND) { + LOG("ss Match not found\n"); + hse->match_scan_index++; + hse->match_length = 0; + return HSES_YIELD_TAG_BIT; + } else { + LOG("ss Found match of %d bytes at %d\n", match_length, match_pos); + hse->match_pos = match_pos; + hse->match_length = match_length; + ASSERT(match_pos <= 1 << HEATSHRINK_ENCODER_WINDOW_BITS(hse) /*window_length*/); + + return HSES_YIELD_TAG_BIT; + } +} + +static HSE_state st_yield_tag_bit(heatshrink_encoder *hse, + output_info *oi) { + if (can_take_byte(oi)) { + if (hse->match_length == 0) { + add_tag_bit(hse, oi, HEATSHRINK_LITERAL_MARKER); + return HSES_YIELD_LITERAL; + } else { + add_tag_bit(hse, oi, HEATSHRINK_BACKREF_MARKER); + hse->outgoing_bits = hse->match_pos - 1; + hse->outgoing_bits_count = HEATSHRINK_ENCODER_WINDOW_BITS(hse); + return HSES_YIELD_BR_INDEX; + } + } else { + return HSES_YIELD_TAG_BIT; /* output is full, continue */ + } +} + +static HSE_state st_yield_literal(heatshrink_encoder *hse, + output_info *oi) { + if (can_take_byte(oi)) { + push_literal_byte(hse, oi); + return HSES_SEARCH; + } else { + return HSES_YIELD_LITERAL; + } +} + +static HSE_state st_yield_br_index(heatshrink_encoder *hse, + output_info *oi) { + if (can_take_byte(oi)) { + LOG("-- yielding backref index %u\n", hse->match_pos); + if (push_outgoing_bits(hse, oi) > 0) { + return HSES_YIELD_BR_INDEX; /* continue */ + } else { + hse->outgoing_bits = hse->match_length - 1; + hse->outgoing_bits_count = HEATSHRINK_ENCODER_LOOKAHEAD_BITS(hse); + return HSES_YIELD_BR_LENGTH; /* done */ + } + } else { + return HSES_YIELD_BR_INDEX; /* continue */ + } +} + +static HSE_state st_yield_br_length(heatshrink_encoder *hse, + output_info *oi) { + if (can_take_byte(oi)) { + LOG("-- yielding backref length %u\n", hse->match_length); + if (push_outgoing_bits(hse, oi) > 0) { + return HSES_YIELD_BR_LENGTH; + } else { + hse->match_scan_index += hse->match_length; + hse->match_length = 0; + return HSES_SEARCH; + } + } else { + return HSES_YIELD_BR_LENGTH; + } +} + +static HSE_state st_save_backlog(heatshrink_encoder *hse) { + LOG("-- saving backlog\n"); + save_backlog(hse); + return HSES_NOT_FULL; +} + +static HSE_state st_flush_bit_buffer(heatshrink_encoder *hse, + output_info *oi) { + if (hse->bit_index == 0x80) { + LOG("-- done!\n"); + return HSES_DONE; + } else if (can_take_byte(oi)) { + LOG("-- flushing remaining byte (bit_index == 0x%02x)\n", hse->bit_index); + oi->buf[(*oi->output_size)++] = hse->current_byte; + LOG("-- done!\n"); + return HSES_DONE; + } else { + return HSES_FLUSH_BITS; + } +} + +static void add_tag_bit(heatshrink_encoder *hse, output_info *oi, uint8_t tag) { + LOG("-- adding tag bit: %d\n", tag); + push_bits(hse, 1, tag, oi); +} + +static uint16_t get_input_offset(heatshrink_encoder *hse) { + return get_input_buffer_size(hse); +} + +static uint16_t get_input_buffer_size(heatshrink_encoder *hse) { + return (1 << HEATSHRINK_ENCODER_WINDOW_BITS(hse)); + (void)hse; +} + +static uint16_t get_lookahead_size(heatshrink_encoder *hse) { + return (1 << HEATSHRINK_ENCODER_LOOKAHEAD_BITS(hse)); + (void)hse; +} + +static void do_indexing(heatshrink_encoder *hse) { +#if HEATSHRINK_USE_INDEX + /* Build an index array I that contains flattened linked lists + * for the previous instances of every byte in the buffer. + * + * For example, if buf[200] == 'x', then index[200] will either + * be an offset i such that buf[i] == 'x', or a negative offset + * to indicate end-of-list. This significantly speeds up matching, + * while only using sizeof(uint16_t)*sizeof(buffer) bytes of RAM. + * + * Future optimization options: + * 1. Since any negative value represents end-of-list, the other + * 15 bits could be used to improve the index dynamically. + * + * 2. Likewise, the last lookahead_sz bytes of the index will + * not be usable, so temporary data could be stored there to + * dynamically improve the index. + * */ + struct hs_index *hsi = HEATSHRINK_ENCODER_INDEX(hse); + int16_t last[256]; + memset(last, 0xFF, sizeof(last)); + + uint8_t * const data = hse->buffer; + int16_t * const index = hsi->index; + + const uint16_t input_offset = get_input_offset(hse); + const uint16_t end = input_offset + hse->input_size; + + for (uint16_t i=0; iflags & FLAG_IS_FINISHING; +} + +static int can_take_byte(output_info *oi) { + return *oi->output_size < oi->buf_size; +} + +/* Return the longest match for the bytes at buf[end:end+maxlen] between + * buf[start] and buf[end-1]. If no match is found, return -1. */ +static uint16_t find_longest_match(heatshrink_encoder *hse, uint16_t start, + uint16_t end, const uint16_t maxlen, uint16_t *match_length) { + LOG("-- scanning for match of buf[%u:%u] between buf[%u:%u] (max %u bytes)\n", + end, end + maxlen, start, end + maxlen - 1, maxlen); + uint8_t *buf = hse->buffer; + + uint16_t match_maxlen = 0; + uint16_t match_index = MATCH_NOT_FOUND; + + uint16_t len = 0; + uint8_t * const needlepoint = &buf[end]; +#if HEATSHRINK_USE_INDEX + struct hs_index *hsi = HEATSHRINK_ENCODER_INDEX(hse); + int16_t pos = hsi->index[end]; + + while (pos - (int16_t)start >= 0) { + uint8_t * const pospoint = &buf[pos]; + len = 0; + + /* Only check matches that will potentially beat the current maxlen. + * This is redundant with the index if match_maxlen is 0, but the + * added branch overhead to check if it == 0 seems to be worse. */ + if (pospoint[match_maxlen] != needlepoint[match_maxlen]) { + pos = hsi->index[pos]; + continue; + } + + for (len = 1; len < maxlen; len++) { + if (pospoint[len] != needlepoint[len]) break; + } + + if (len > match_maxlen) { + match_maxlen = len; + match_index = pos; + if (len == maxlen) { break; } /* won't find better */ + } + pos = hsi->index[pos]; + } +#else + for (int16_t pos=end - 1; pos - (int16_t)start >= 0; pos--) { + uint8_t * const pospoint = &buf[pos]; + if ((pospoint[match_maxlen] == needlepoint[match_maxlen]) + && (*pospoint == *needlepoint)) { + for (len=1; len cmp buf[%d] == 0x%02x against %02x (start %u)\n", + pos + len, pospoint[len], needlepoint[len], start); + } + if (pospoint[len] != needlepoint[len]) { break; } + } + if (len > match_maxlen) { + match_maxlen = len; + match_index = pos; + if (len == maxlen) { break; } /* don't keep searching */ + } + } + } +#endif + + const size_t break_even_point = + (1 + HEATSHRINK_ENCODER_WINDOW_BITS(hse) + + HEATSHRINK_ENCODER_LOOKAHEAD_BITS(hse)); + + /* Instead of comparing break_even_point against 8*match_maxlen, + * compare match_maxlen against break_even_point/8 to avoid + * overflow. Since MIN_WINDOW_BITS and MIN_LOOKAHEAD_BITS are 4 and + * 3, respectively, break_even_point/8 will always be at least 1. */ + if (match_maxlen > (break_even_point / 8)) { + LOG("-- best match: %u bytes at -%u\n", + match_maxlen, end - match_index); + *match_length = match_maxlen; + return end - match_index; + } + LOG("-- none found\n"); + return MATCH_NOT_FOUND; +} + +static uint8_t push_outgoing_bits(heatshrink_encoder *hse, output_info *oi) { + uint8_t count = 0; + uint8_t bits = 0; + if (hse->outgoing_bits_count > 8) { + count = 8; + bits = hse->outgoing_bits >> (hse->outgoing_bits_count - 8); + } else { + count = hse->outgoing_bits_count; + bits = hse->outgoing_bits; + } + + if (count > 0) { + LOG("-- pushing %d outgoing bits: 0x%02x\n", count, bits); + push_bits(hse, count, bits, oi); + hse->outgoing_bits_count -= count; + } + return count; +} + +/* Push COUNT (max 8) bits to the output buffer, which has room. + * Bytes are set from the lowest bits, up. */ +static void push_bits(heatshrink_encoder *hse, uint8_t count, uint8_t bits, + output_info *oi) { + ASSERT(count <= 8); + LOG("++ push_bits: %d bits, input of 0x%02x\n", count, bits); + + /* If adding a whole byte and at the start of a new output byte, + * just push it through whole and skip the bit IO loop. */ + if (count == 8 && hse->bit_index == 0x80) { + oi->buf[(*oi->output_size)++] = bits; + } else { + for (int i=count - 1; i>=0; i--) { + bool bit = bits & (1 << i); + if (bit) { hse->current_byte |= hse->bit_index; } + if (0) { + LOG(" -- setting bit %d at bit index 0x%02x, byte => 0x%02x\n", + bit ? 1 : 0, hse->bit_index, hse->current_byte); + } + hse->bit_index >>= 1; + if (hse->bit_index == 0x00) { + hse->bit_index = 0x80; + LOG(" > pushing byte 0x%02x\n", hse->current_byte); + oi->buf[(*oi->output_size)++] = hse->current_byte; + hse->current_byte = 0x00; + } + } + } +} + +static void push_literal_byte(heatshrink_encoder *hse, output_info *oi) { + uint16_t processed_offset = hse->match_scan_index - 1; + uint16_t input_offset = get_input_offset(hse) + processed_offset; + uint8_t c = hse->buffer[input_offset]; + LOG("-- yielded literal byte 0x%02x ('%c') from +%d\n", + c, isprint(c) ? c : '.', input_offset); + push_bits(hse, 8, c, oi); +} + +static void save_backlog(heatshrink_encoder *hse) { + size_t input_buf_sz = get_input_buffer_size(hse); + + uint16_t msi = hse->match_scan_index; + + /* Copy processed data to beginning of buffer, so it can be + * used for future matches. Don't bother checking whether the + * input is less than the maximum size, because if it isn't, + * we're done anyway. */ + uint16_t rem = input_buf_sz - msi; // unprocessed bytes + uint16_t shift_sz = input_buf_sz + rem; + + memmove(&hse->buffer[0], + &hse->buffer[input_buf_sz - rem], + shift_sz); + + hse->match_scan_index = 0; + hse->input_size -= input_buf_sz - rem; +} diff --git a/libraries/heatshrink-0.4.1/src/heatshrink_encoder.h b/libraries/heatshrink-0.4.1/src/heatshrink_encoder.h new file mode 100644 index 00000000..18c17731 --- /dev/null +++ b/libraries/heatshrink-0.4.1/src/heatshrink_encoder.h @@ -0,0 +1,109 @@ +#ifndef HEATSHRINK_ENCODER_H +#define HEATSHRINK_ENCODER_H + +#include +#include +#include "heatshrink_common.h" +#include "heatshrink_config.h" + +typedef enum { + HSER_SINK_OK, /* data sunk into input buffer */ + HSER_SINK_ERROR_NULL=-1, /* NULL argument */ + HSER_SINK_ERROR_MISUSE=-2, /* API misuse */ +} HSE_sink_res; + +typedef enum { + HSER_POLL_EMPTY, /* input exhausted */ + HSER_POLL_MORE, /* poll again for more output */ + HSER_POLL_ERROR_NULL=-1, /* NULL argument */ + HSER_POLL_ERROR_MISUSE=-2, /* API misuse */ +} HSE_poll_res; + +typedef enum { + HSER_FINISH_DONE, /* encoding is complete */ + HSER_FINISH_MORE, /* more output remaining; use poll */ + HSER_FINISH_ERROR_NULL=-1, /* NULL argument */ +} HSE_finish_res; + +#if HEATSHRINK_DYNAMIC_ALLOC +#define HEATSHRINK_ENCODER_WINDOW_BITS(HSE) \ + ((HSE)->window_sz2) +#define HEATSHRINK_ENCODER_LOOKAHEAD_BITS(HSE) \ + ((HSE)->lookahead_sz2) +#define HEATSHRINK_ENCODER_INDEX(HSE) \ + ((HSE)->search_index) +struct hs_index { + uint16_t size; + int16_t index[]; +}; +#else +#define HEATSHRINK_ENCODER_WINDOW_BITS(_) \ + (HEATSHRINK_STATIC_WINDOW_BITS) +#define HEATSHRINK_ENCODER_LOOKAHEAD_BITS(_) \ + (HEATSHRINK_STATIC_LOOKAHEAD_BITS) +#define HEATSHRINK_ENCODER_INDEX(HSE) \ + (&(HSE)->search_index) +struct hs_index { + uint16_t size; + int16_t index[2 << HEATSHRINK_STATIC_WINDOW_BITS]; +}; +#endif + +typedef struct { + uint16_t input_size; /* bytes in input buffer */ + uint16_t match_scan_index; + uint16_t match_length; + uint16_t match_pos; + uint16_t outgoing_bits; /* enqueued outgoing bits */ + uint8_t outgoing_bits_count; + uint8_t flags; + uint8_t state; /* current state machine node */ + uint8_t current_byte; /* current byte of output */ + uint8_t bit_index; /* current bit index */ +#if HEATSHRINK_DYNAMIC_ALLOC + uint8_t window_sz2; /* 2^n size of window */ + uint8_t lookahead_sz2; /* 2^n size of lookahead */ +#if HEATSHRINK_USE_INDEX + struct hs_index *search_index; +#endif + /* input buffer and / sliding window for expansion */ + uint8_t buffer[]; +#else + #if HEATSHRINK_USE_INDEX + struct hs_index search_index; + #endif + /* input buffer and / sliding window for expansion */ + uint8_t buffer[2 << HEATSHRINK_ENCODER_WINDOW_BITS(_)]; +#endif +} heatshrink_encoder; + +#if HEATSHRINK_DYNAMIC_ALLOC +/* Allocate a new encoder struct and its buffers. + * Returns NULL on error. */ +heatshrink_encoder *heatshrink_encoder_alloc(uint8_t window_sz2, + uint8_t lookahead_sz2); + +/* Free an encoder. */ +void heatshrink_encoder_free(heatshrink_encoder *hse); +#endif + +/* Reset an encoder. */ +void heatshrink_encoder_reset(heatshrink_encoder *hse); + +/* Sink up to SIZE bytes from IN_BUF into the encoder. + * INPUT_SIZE is set to the number of bytes actually sunk (in case a + * buffer was filled.). */ +HSE_sink_res heatshrink_encoder_sink(heatshrink_encoder *hse, + uint8_t *in_buf, size_t size, size_t *input_size); + +/* Poll for output from the encoder, copying at most OUT_BUF_SIZE bytes into + * OUT_BUF (setting *OUTPUT_SIZE to the actual amount copied). */ +HSE_poll_res heatshrink_encoder_poll(heatshrink_encoder *hse, + uint8_t *out_buf, size_t out_buf_size, size_t *output_size); + +/* Notify the encoder that the input stream is finished. + * If the return value is HSER_FINISH_MORE, there is still more output, so + * call heatshrink_encoder_poll and repeat. */ +HSE_finish_res heatshrink_encoder_finish(heatshrink_encoder *hse); + +#endif diff --git a/libraries/heatshrink-0.4.1/test/encoder_finish_empty.c b/libraries/heatshrink-0.4.1/test/encoder_finish_empty.c new file mode 100644 index 00000000..1d91d781 --- /dev/null +++ b/libraries/heatshrink-0.4.1/test/encoder_finish_empty.c @@ -0,0 +1,21 @@ +#include +#include +#include + +#include "heatshrink_encoder.h" + +int main(void) { + heatshrink_encoder *encoder = heatshrink_encoder_alloc(8, 4); + assert(encoder != NULL); + + uint8_t output[8]; + size_t output_size = sizeof(output); + assert(heatshrink_encoder_finish(encoder) == HSER_FINISH_MORE); + assert(heatshrink_encoder_poll(encoder, output, sizeof(output), + &output_size) == HSER_POLL_EMPTY); + assert(output_size == 0); + assert(heatshrink_encoder_finish(encoder) == HSER_FINISH_DONE); + + heatshrink_encoder_free(encoder); + return 0; +}