From e3d6e3260946917be860b42332f6babe2d38cc15 Mon Sep 17 00:00:00 2001 From: Juan Biondi Date: Wed, 21 Jan 2026 14:29:39 +0100 Subject: [PATCH 1/3] docs: Add detailed webserver documentation (#446) ## More detailed documentation * **What is the goal of this PR?** Add more information about the exposed webserver. * **What changes are included?** Detailed documentation for the webserver endpoints (`./docs/webserver-endpoints.md`) Adding a table of content so it is easier to navigate directly to the section you're interested on (Almost all `.md` files or at least all those relevant) ## Additional Context Not sure if this would get accepted but I thought it might be useful for those trying to create separate apps that would sync files to the device. It was at least to me trying to upload files using python as stated [here](https://github.com/crosspoint-reader/crosspoint-reader/discussions/434#discussioncomment-15545349) --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**PARTIALLY**_ --- USER_GUIDE.md | 21 +++ docs/troubleshooting.md | 57 +++++++ docs/webserver-endpoints.md | 331 ++++++++++++++++++++++++++++++++++++ docs/webserver.md | 85 +-------- 4 files changed, 411 insertions(+), 83 deletions(-) create mode 100644 docs/troubleshooting.md create mode 100644 docs/webserver-endpoints.md diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 0c85269..d670abb 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -2,6 +2,27 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware controls, navigation, and reading features of the device. +- [CrossPoint User Guide](#crosspoint-user-guide) + - [1. Hardware Overview](#1-hardware-overview) + - [Button Layout](#button-layout) + - [2. Power \& Startup](#2-power--startup) + - [Power On / Off](#power-on--off) + - [First Launch](#first-launch) + - [3. Screens](#3-screens) + - [3.1 Home Screen](#31-home-screen) + - [3.2 Book Selection](#32-book-selection) + - [3.3 Reading Mode](#33-reading-mode) + - [3.4 File Upload Screen](#34-file-upload-screen) + - [3.5 Settings](#35-settings) + - [3.6 Sleep Screen](#36-sleep-screen) + - [4. Reading Mode](#4-reading-mode) + - [Page Turning](#page-turning) + - [Chapter Navigation](#chapter-navigation) + - [System Navigation](#system-navigation) + - [5. Chapter Selection Screen](#5-chapter-selection-screen) + - [6. Current Limitations \& Roadmap](#6-current-limitations--roadmap) + + ## 1. Hardware Overview The device utilises the standard buttons on the Xtink X4 (in the same layout as the manufacturer firmware, by default): diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..7c50a30 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,57 @@ +# Troubleshooting + +This document show most common issues and possible solutions while using the device features. + +- [Troubleshooting](#troubleshooting) + - [Cannot See the Device on the Network](#cannot-see-the-device-on-the-network) + - [Connection Drops or Times Out](#connection-drops-or-times-out) + - [Upload Fails](#upload-fails) + - [Saved Password Not Working](#saved-password-not-working) + +### Cannot See the Device on the Network + +**Problem:** Browser shows "Cannot connect" or "Site can't be reached" + +**Solutions:** + +1. Verify both devices are on the **same WiFi network** + - Check your computer/phone WiFi settings + - Confirm the CrossPoint Reader shows "Connected" status +2. Double-check the IP address + - Make sure you typed it correctly + - Include `http://` at the beginning +3. Try disabling VPN if you're using one +4. Some networks have "client isolation" enabled - check with your network administrator + +### Connection Drops or Times Out + +**Problem:** WiFi connection is unstable + +**Solutions:** + +1. Move closer to the WiFi router +2. Check signal strength on the device (should be at least `||` or better) +3. Avoid interference from other devices +4. Try a different WiFi network if available + +### Upload Fails + +**Problem:** File upload doesn't complete or shows an error + +**Solutions:** + +1. Ensure the file is a valid `.epub` file +2. Check that the SD card has enough free space +3. Try uploading a smaller file first to test +4. Refresh the browser page and try again + +### Saved Password Not Working + +**Problem:** Device fails to connect with saved credentials + +**Solutions:** + +1. When connection fails, you'll be prompted to "Forget Network" +2. Select **Yes** to remove the saved password +3. Reconnect and enter the password again +4. Choose to save the new password diff --git a/docs/webserver-endpoints.md b/docs/webserver-endpoints.md new file mode 100644 index 0000000..0abe9df --- /dev/null +++ b/docs/webserver-endpoints.md @@ -0,0 +1,331 @@ +# Webserver Endpoints + +This document describes all HTTP and WebSocket endpoints available on the CrossPoint Reader webserver. + +- [Webserver Endpoints](#webserver-endpoints) + - [Overview](#overview) + - [HTTP Endpoints](#http-endpoints) + - [GET `/` - Home Page](#get----home-page) + - [GET `/files` - File Browser Page](#get-files---file-browser-page) + - [GET `/api/status` - Device Status](#get-apistatus---device-status) + - [GET `/api/files` - List Files](#get-apifiles---list-files) + - [POST `/upload` - Upload File](#post-upload---upload-file) + - [POST `/mkdir` - Create Folder](#post-mkdir---create-folder) + - [POST `/delete` - Delete File or Folder](#post-delete---delete-file-or-folder) + - [WebSocket Endpoint](#websocket-endpoint) + - [Port 81 - Fast Binary Upload](#port-81---fast-binary-upload) + - [Network Modes](#network-modes) + - [Station Mode (STA)](#station-mode-sta) + - [Access Point Mode (AP)](#access-point-mode-ap) + - [Notes](#notes) + + +## Overview + +The CrossPoint Reader exposes a webserver for file management and device monitoring: + +- **HTTP Server**: Port 80 +- **WebSocket Server**: Port 81 (for fast binary uploads) + +--- + +## HTTP Endpoints + +### GET `/` - Home Page + +Serves the home page HTML interface. + +**Request:** +```bash +curl http://crosspoint.local/ +``` + +**Response:** HTML page (200 OK) + +--- + +### GET `/files` - File Browser Page + +Serves the file browser HTML interface. + +**Request:** +```bash +curl http://crosspoint.local/files +``` + +**Response:** HTML page (200 OK) + +--- + +### GET `/api/status` - Device Status + +Returns JSON with device status information. + +**Request:** +```bash +curl http://crosspoint.local/api/status +``` + +**Response (200 OK):** +```json +{ + "version": "1.0.0", + "ip": "192.168.1.100", + "mode": "STA", + "rssi": -45, + "freeHeap": 123456, + "uptime": 3600 +} +``` + +| Field | Type | Description | +| ---------- | ------ | --------------------------------------------------------- | +| `version` | string | CrossPoint firmware version | +| `ip` | string | Device IP address | +| `mode` | string | `"STA"` (connected to WiFi) or `"AP"` (access point mode) | +| `rssi` | number | WiFi signal strength in dBm (0 in AP mode) | +| `freeHeap` | number | Free heap memory in bytes | +| `uptime` | number | Seconds since device boot | + +--- + +### GET `/api/files` - List Files + +Returns a JSON array of files and folders in the specified directory. + +**Request:** +```bash +# List root directory +curl http://crosspoint.local/api/files + +# List specific directory +curl "http://crosspoint.local/api/files?path=/Books" +``` + +**Query Parameters:** + +| Parameter | Required | Default | Description | +| --------- | -------- | ------- | ---------------------- | +| `path` | No | `/` | Directory path to list | + +**Response (200 OK):** +```json +[ + {"name": "MyBook.epub", "size": 1234567, "isDirectory": false, "isEpub": true}, + {"name": "Notes", "size": 0, "isDirectory": true, "isEpub": false}, + {"name": "document.pdf", "size": 54321, "isDirectory": false, "isEpub": false} +] +``` + +| Field | Type | Description | +| ------------- | ------- | ---------------------------------------- | +| `name` | string | File or folder name | +| `size` | number | Size in bytes (0 for directories) | +| `isDirectory` | boolean | `true` if the item is a folder | +| `isEpub` | boolean | `true` if the file has `.epub` extension | + +**Notes:** +- Hidden files (starting with `.`) are automatically filtered out +- System folders (`System Volume Information`, `XTCache`) are hidden + +--- + +### POST `/upload` - Upload File + +Uploads a file to the SD card via multipart form data. + +**Request:** +```bash +# Upload to root directory +curl -X POST -F "file=@mybook.epub" http://crosspoint.local/upload + +# Upload to specific directory +curl -X POST -F "file=@mybook.epub" "http://crosspoint.local/upload?path=/Books" +``` + +**Query Parameters:** + +| Parameter | Required | Default | Description | +| --------- | -------- | ------- | ------------------------------- | +| `path` | No | `/` | Target directory for the upload | + +**Response (200 OK):** +``` +File uploaded successfully: mybook.epub +``` + +**Error Responses:** + +| Status | Body | Cause | +| ------ | ----------------------------------------------- | --------------------------- | +| 400 | `Failed to create file on SD card` | Cannot create file | +| 400 | `Failed to write to SD card - disk may be full` | Write error during upload | +| 400 | `Failed to write final data to SD card` | Error flushing final buffer | +| 400 | `Upload aborted` | Client aborted the upload | +| 400 | `Unknown error during upload` | Unspecified error | + +**Notes:** +- Existing files with the same name will be overwritten +- Uses a 4KB buffer for efficient SD card writes + +--- + +### POST `/mkdir` - Create Folder + +Creates a new folder on the SD card. + +**Request:** +```bash +curl -X POST -d "name=NewFolder&path=/" http://crosspoint.local/mkdir +``` + +**Form Parameters:** + +| Parameter | Required | Default | Description | +| --------- | -------- | ------- | ---------------------------- | +| `name` | Yes | - | Name of the folder to create | +| `path` | No | `/` | Parent directory path | + +**Response (200 OK):** +``` +Folder created: NewFolder +``` + +**Error Responses:** + +| Status | Body | Cause | +| ------ | ----------------------------- | ----------------------------- | +| 400 | `Missing folder name` | `name` parameter not provided | +| 400 | `Folder name cannot be empty` | Empty folder name | +| 400 | `Folder already exists` | Folder with same name exists | +| 500 | `Failed to create folder` | SD card error | + +--- + +### POST `/delete` - Delete File or Folder + +Deletes a file or folder from the SD card. + +**Request:** +```bash +# Delete a file +curl -X POST -d "path=/Books/mybook.epub&type=file" http://crosspoint.local/delete + +# Delete an empty folder +curl -X POST -d "path=/OldFolder&type=folder" http://crosspoint.local/delete +``` + +**Form Parameters:** + +| Parameter | Required | Default | Description | +| --------- | -------- | ------- | -------------------------------- | +| `path` | Yes | - | Path to the item to delete | +| `type` | No | `file` | Type of item: `file` or `folder` | + +**Response (200 OK):** +``` +Deleted successfully +``` + +**Error Responses:** + +| Status | Body | Cause | +| ------ | --------------------------------------------- | ----------------------------- | +| 400 | `Missing path` | `path` parameter not provided | +| 400 | `Cannot delete root directory` | Attempted to delete `/` | +| 400 | `Folder is not empty. Delete contents first.` | Non-empty folder | +| 403 | `Cannot delete system files` | Hidden file (starts with `.`) | +| 403 | `Cannot delete protected items` | Protected system folder | +| 404 | `Item not found` | Path does not exist | +| 500 | `Failed to delete item` | SD card error | + +**Protected Items:** +- Files/folders starting with `.` +- `System Volume Information` +- `XTCache` + +--- + +## WebSocket Endpoint + +### Port 81 - Fast Binary Upload + +A WebSocket endpoint for high-speed binary file uploads. More efficient than HTTP multipart for large files. + +**Connection:** +``` +ws://crosspoint.local:81/ +``` + +**Protocol:** + +1. **Client** sends TEXT message: `START:::` +2. **Server** responds with TEXT: `READY` +3. **Client** sends BINARY messages with file data chunks +4. **Server** sends TEXT progress updates: `PROGRESS::` +5. **Server** sends TEXT when complete: `DONE` or `ERROR:` + +**Example Session:** + +``` +Client -> "START:mybook.epub:1234567:/Books" +Server -> "READY" +Client -> [binary chunk 1] +Client -> [binary chunk 2] +Server -> "PROGRESS:65536:1234567" +Client -> [binary chunk 3] +... +Server -> "PROGRESS:1234567:1234567" +Server -> "DONE" +``` + +**Error Messages:** + +| Message | Cause | +| --------------------------------- | ---------------------------------- | +| `ERROR:Failed to create file` | Cannot create file on SD card | +| `ERROR:Invalid START format` | Malformed START message | +| `ERROR:No upload in progress` | Binary data received without START | +| `ERROR:Write failed - disk full?` | SD card write error | + +**Example with `websocat`:** +```bash +# Interactive session +websocat ws://crosspoint.local:81 + +# Then type: +START:mybook.epub:1234567:/Books +# Wait for READY, then send binary data +``` + +**Notes:** +- Progress updates are sent every 64KB or at completion +- Disconnection during upload will delete the incomplete file +- Existing files with the same name will be overwritten + +--- + +## Network Modes + +The device can operate in two network modes: + +### Station Mode (STA) +- Device connects to an existing WiFi network +- IP address assigned by router/DHCP +- `mode` field in `/api/status` returns `"STA"` +- `rssi` field shows signal strength + +### Access Point Mode (AP) +- Device creates its own WiFi hotspot +- Default IP is typically `192.168.4.1` +- `mode` field in `/api/status` returns `"AP"` +- `rssi` field returns `0` + +--- + +## Notes + +- These examples use `crosspoint.local`. If your network does not support mDNS or the address does not resolve, replace it with the specific **IP Address** displayed on your device screen (e.g., `http://192.168.1.102/`). +- All paths on the SD card start with `/` +- Trailing slashes are automatically stripped (except for root `/`) +- The webserver uses chunked transfer encoding for file listings diff --git a/docs/webserver.md b/docs/webserver.md index 2285a92..355bac4 100644 --- a/docs/webserver.md +++ b/docs/webserver.md @@ -172,89 +172,7 @@ This is useful for organizing your ebooks by genre, author, or series. ## Command Line File Management -For power users, you can manage files directly from your terminal using `curl` while the device is in File Upload mode. - -### Uploading a File -To upload a file to the root directory, use the following command: -```bash -curl -F "file=@book.epub" "http://crosspoint.local/upload?path=/" -``` - -* **`-F "file=@filename"`**: Points to the local file on your computer. -* **`path=/`**: The destination folder on the device SD card. - -### Deleting a File - -To delete a specific file, provide the full path on the SD card: - -```bash -curl -F "path=/folder/file.epub" "http://crosspoint.local/delete" -``` - -### Advanced Flags - -For more reliable transfers of large EPUB files, consider adding these flags: - -* `-#`: Shows a simple progress bar. -* `--connect-timeout 30`: Limits how long curl waits to establish a connection (in seconds). -* `--max-time 300`: Sets a maximum duration for the entire transfer (5 minutes). - -> [!NOTE] -> These examples use `crosspoint.local`. If your network does not support mDNS or the address does not resolve, replace it with the specific **IP Address** displayed on your device screen (e.g., `http://192.168.1.102/`). - ---- - -## Troubleshooting - -### Cannot See the Device on the Network - -**Problem:** Browser shows "Cannot connect" or "Site can't be reached" - -**Solutions:** - -1. Verify both devices are on the **same WiFi network** - - Check your computer/phone WiFi settings - - Confirm the CrossPoint Reader shows "Connected" status -2. Double-check the IP address - - Make sure you typed it correctly - - Include `http://` at the beginning -3. Try disabling VPN if you're using one -4. Some networks have "client isolation" enabled - check with your network administrator - -### Connection Drops or Times Out - -**Problem:** WiFi connection is unstable - -**Solutions:** - -1. Move closer to the WiFi router -2. Check signal strength on the device (should be at least `||` or better) -3. Avoid interference from other devices -4. Try a different WiFi network if available - -### Upload Fails - -**Problem:** File upload doesn't complete or shows an error - -**Solutions:** - -1. Ensure the file is a valid `.epub` file -2. Check that the SD card has enough free space -3. Try uploading a smaller file first to test -4. Refresh the browser page and try again - -### Saved Password Not Working - -**Problem:** Device fails to connect with saved credentials - -**Solutions:** - -1. When connection fails, you'll be prompted to "Forget Network" -2. Select **Yes** to remove the saved password -3. Reconnect and enter the password again -4. Choose to save the new password - ---- +For power users, you can manage files directly from your terminal using `curl` while the device is in File Upload mode a detailed documentation can be found [here](./webserver-endpoints.md). ## Security Notes @@ -303,4 +221,5 @@ Your uploaded files will be immediately available in the file browser! ## Related Documentation - [User Guide](../USER_GUIDE.md) - General device operation +- [Troubleshooting](./troubleshooting.md) - Troubleshooting - [README](../README.md) - Project overview and features From 47ef92e8fd7d204fcf6ed0d20498f4886a83bbd8 Mon Sep 17 00:00:00 2001 From: KasyanDiGris Date: Wed, 21 Jan 2026 17:43:51 +0300 Subject: [PATCH 2/3] fix: OPDS browser OOM (#403) ## Summary - Rewrite OpdsParser to stream parsing instead of full content - Fix OOM due to big http xml response Closes #385 --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**NO**_ --- lib/OpdsParser/OpdsParser.cpp | 47 ++++++++++++------- lib/OpdsParser/OpdsParser.h | 25 ++++++---- lib/OpdsParser/OpdsStream.cpp | 15 ++++++ lib/OpdsParser/OpdsStream.h | 23 +++++++++ .../browser/OpdsBookBrowserActivity.cpp | 23 +++++---- src/network/HttpDownloader.cpp | 17 +++++-- src/network/HttpDownloader.h | 2 + 7 files changed, 112 insertions(+), 40 deletions(-) create mode 100644 lib/OpdsParser/OpdsStream.cpp create mode 100644 lib/OpdsParser/OpdsStream.h diff --git a/lib/OpdsParser/OpdsParser.cpp b/lib/OpdsParser/OpdsParser.cpp index da4042f..4b58d8f 100644 --- a/lib/OpdsParser/OpdsParser.cpp +++ b/lib/OpdsParser/OpdsParser.cpp @@ -4,6 +4,14 @@ #include +OpdsParser::OpdsParser() { + parser = XML_ParserCreate(nullptr); + if (!parser) { + errorOccured = true; + Serial.printf("[%lu] [OPDS] Couldn't allocate memory for parser\n", millis()); + } +} + OpdsParser::~OpdsParser() { if (parser) { XML_StopParser(parser, XML_FALSE); @@ -14,13 +22,11 @@ OpdsParser::~OpdsParser() { } } -bool OpdsParser::parse(const char* xmlData, const size_t length) { - clear(); +size_t OpdsParser::write(uint8_t c) { return write(&c, 1); } - parser = XML_ParserCreate(nullptr); - if (!parser) { - Serial.printf("[%lu] [OPDS] Couldn't allocate memory for parser\n", millis()); - return false; +size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) { + if (errorOccured) { + return length; } XML_SetUserData(parser, this); @@ -28,43 +34,48 @@ bool OpdsParser::parse(const char* xmlData, const size_t length) { XML_SetCharacterDataHandler(parser, characterData); // Parse in chunks to avoid large buffer allocations - const char* currentPos = xmlData; + const char* currentPos = reinterpret_cast(xmlData); size_t remaining = length; constexpr size_t chunkSize = 1024; while (remaining > 0) { void* const buf = XML_GetBuffer(parser, chunkSize); if (!buf) { + errorOccured = true; Serial.printf("[%lu] [OPDS] Couldn't allocate memory for buffer\n", millis()); XML_ParserFree(parser); parser = nullptr; - return false; + return length; } const size_t toRead = remaining < chunkSize ? remaining : chunkSize; memcpy(buf, currentPos, toRead); - const bool isFinal = (remaining == toRead); - if (XML_ParseBuffer(parser, static_cast(toRead), isFinal) == XML_STATUS_ERROR) { + if (XML_ParseBuffer(parser, static_cast(toRead), 0) == XML_STATUS_ERROR) { + errorOccured = true; Serial.printf("[%lu] [OPDS] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser), XML_ErrorString(XML_GetErrorCode(parser))); XML_ParserFree(parser); parser = nullptr; - return false; + return length; } currentPos += toRead; remaining -= toRead; } - - // Clean up parser - XML_ParserFree(parser); - parser = nullptr; - - Serial.printf("[%lu] [OPDS] Parsed %zu entries\n", millis(), entries.size()); - return true; + return length; } +void OpdsParser::flush() { + if (XML_Parse(parser, nullptr, 0, XML_TRUE) != XML_STATUS_OK) { + errorOccured = true; + XML_ParserFree(parser); + parser = nullptr; + } +} + +bool OpdsParser::error() const { return errorOccured; } + void OpdsParser::clear() { entries.clear(); currentEntry = OpdsEntry{}; diff --git a/lib/OpdsParser/OpdsParser.h b/lib/OpdsParser/OpdsParser.h index acb4b69..570ac4c 100644 --- a/lib/OpdsParser/OpdsParser.h +++ b/lib/OpdsParser/OpdsParser.h @@ -1,4 +1,5 @@ #pragma once +#include #include #include @@ -42,28 +43,30 @@ using OpdsBook = OpdsEntry; * } * } */ -class OpdsParser { +class OpdsParser final : public Print { public: - OpdsParser() = default; + OpdsParser(); ~OpdsParser(); // Disable copy OpdsParser(const OpdsParser&) = delete; OpdsParser& operator=(const OpdsParser&) = delete; - /** - * Parse an OPDS XML feed. - * @param xmlData Pointer to the XML data - * @param length Length of the XML data - * @return true if parsing succeeded, false on error - */ - bool parse(const char* xmlData, size_t length); + size_t write(uint8_t) override; + size_t write(const uint8_t*, size_t) override; + + void flush() override; + + bool error() const; + + operator bool() { return !error(); } /** * Get the parsed entries (both navigation and book entries). * @return Vector of OpdsEntry entries */ - const std::vector& getEntries() const { return entries; } + const std::vector& getEntries() const& { return entries; } + std::vector getEntries() && { return std::move(entries); } /** * Get only book entries (legacy compatibility). @@ -96,4 +99,6 @@ class OpdsParser { bool inAuthor = false; bool inAuthorName = false; bool inId = false; + + bool errorOccured = false; }; diff --git a/lib/OpdsParser/OpdsStream.cpp b/lib/OpdsParser/OpdsStream.cpp new file mode 100644 index 0000000..742624a --- /dev/null +++ b/lib/OpdsParser/OpdsStream.cpp @@ -0,0 +1,15 @@ +#include "OpdsStream.h" + +OpdsParserStream::OpdsParserStream(OpdsParser& parser) : parser(parser) {} + +int OpdsParserStream::available() { return 0; } + +int OpdsParserStream::peek() { abort(); } + +int OpdsParserStream::read() { abort(); } + +size_t OpdsParserStream::write(uint8_t c) { return parser.write(c); } + +size_t OpdsParserStream::write(const uint8_t* buffer, size_t size) { return parser.write(buffer, size); } + +OpdsParserStream::~OpdsParserStream() { parser.flush(); } diff --git a/lib/OpdsParser/OpdsStream.h b/lib/OpdsParser/OpdsStream.h new file mode 100644 index 0000000..c72f2b6 --- /dev/null +++ b/lib/OpdsParser/OpdsStream.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include "OpdsParser.h" + +class OpdsParserStream : public Stream { + public: + explicit OpdsParserStream(OpdsParser& parser); + + // That functions are not implimented for that stream + int available() override; + int peek() override; + int read() override; + + virtual size_t write(uint8_t c) override; + virtual size_t write(const uint8_t* buffer, size_t size) override; + + ~OpdsParserStream() override; + + private: + OpdsParser& parser; +}; diff --git a/src/activities/browser/OpdsBookBrowserActivity.cpp b/src/activities/browser/OpdsBookBrowserActivity.cpp index 677f9ca..555cba9 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.cpp +++ b/src/activities/browser/OpdsBookBrowserActivity.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "CrossPointSettings.h" @@ -265,23 +266,27 @@ void OpdsBookBrowserActivity::fetchFeed(const std::string& path) { std::string url = UrlUtils::buildUrl(serverUrl, path); Serial.printf("[%lu] [OPDS] Fetching: %s\n", millis(), url.c_str()); - std::string content; - if (!HttpDownloader::fetchUrl(url, content)) { - state = BrowserState::ERROR; - errorMessage = "Failed to fetch feed"; - updateRequired = true; - return; + OpdsParser parser; + + { + OpdsParserStream stream{parser}; + if (!HttpDownloader::fetchUrl(url, stream)) { + state = BrowserState::ERROR; + errorMessage = "Failed to fetch feed"; + updateRequired = true; + return; + } } - OpdsParser parser; - if (!parser.parse(content.c_str(), content.size())) { + if (!parser) { state = BrowserState::ERROR; errorMessage = "Failed to parse feed"; updateRequired = true; return; } - entries = parser.getEntries(); + entries = std::move(parser).getEntries(); + Serial.printf("[%lu] [OPDS] Found %d entries\n", millis(), entries.size()); selectorIndex = 0; if (entries.empty()) { diff --git a/src/network/HttpDownloader.cpp b/src/network/HttpDownloader.cpp index c4de3a0..fe65ea6 100644 --- a/src/network/HttpDownloader.cpp +++ b/src/network/HttpDownloader.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -9,7 +10,7 @@ #include "util/UrlUtils.h" -bool HttpDownloader::fetchUrl(const std::string& url, std::string& outContent) { +bool HttpDownloader::fetchUrl(const std::string& url, Stream& outContent) { // Use WiFiClientSecure for HTTPS, regular WiFiClient for HTTP std::unique_ptr client; if (UrlUtils::isHttpsUrl(url)) { @@ -34,10 +35,20 @@ bool HttpDownloader::fetchUrl(const std::string& url, std::string& outContent) { return false; } - outContent = http.getString().c_str(); + http.writeToStream(&outContent); + http.end(); - Serial.printf("[%lu] [HTTP] Fetched %zu bytes\n", millis(), outContent.size()); + Serial.printf("[%lu] [HTTP] Fetch success\n", millis()); + return true; +} + +bool HttpDownloader::fetchUrl(const std::string& url, std::string& outContent) { + StreamString stream; + if (!fetchUrl(url, stream)) { + return false; + } + outContent = stream.c_str(); return true; } diff --git a/src/network/HttpDownloader.h b/src/network/HttpDownloader.h index e6e0f16..ac520a4 100644 --- a/src/network/HttpDownloader.h +++ b/src/network/HttpDownloader.h @@ -27,6 +27,8 @@ class HttpDownloader { */ static bool fetchUrl(const std::string& url, std::string& outContent); + static bool fetchUrl(const std::string& url, Stream& stream); + /** * Download a file to the SD card. * @param url The URL to download From 3ce11f14ce7bc3ce1f2f040bfb09a9b3d9f87f72 Mon Sep 17 00:00:00 2001 From: Dave Allie Date: Thu, 22 Jan 2026 02:20:22 +1100 Subject: [PATCH 3/3] chore: Cut release 0.15.0 --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index ef27ffd..7f42637 100644 --- a/platformio.ini +++ b/platformio.ini @@ -2,7 +2,7 @@ default_envs = default [crosspoint] -version = 0.14.0 +version = 0.15.0 [base] platform = espressif32 @ 6.12.0