sd put: copy a file to the SD card over USB serial

scripts/sd_put.sh <file> [card path] sends a file (by default into
/updates, for Update from SD) without taking the card out. The serial
driver drops bytes once its receive buffer is full, so the transfer is
stop-and-wait: 1 KB chunks, each acknowledged once the Storage Service
has written it, into a 2 KB receive buffer. The device checks the
SHA-256 before renaming <path>.part into place, and gives up after 5 s
of silence or a card job that never returns. FileReceiver holds the
logic, with 12 host tests. About 55 KB/s: 1.6 MB in under 30 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
2026-10-04 02:31:19 +02:00
co-authored by Claude Opus 5.5
parent 8ec4e9e449
commit 5b199c2436
7 changed files with 575 additions and 1 deletions
+66
View File
@@ -0,0 +1,66 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>
#include "sha256.h"
namespace roro {
// One file sent over the USB serial console (scripts/sd_put.py), to put an Update File on the SD
// card without taking the card out. The sender writes `sd put <path> <size> <sha256>`, then the raw
// bytes, one chunk at a time, and waits for each chunk to be written before sending the next: the
// serial driver drops bytes once its receive buffer is full. The file lands as `<path>.part` and is
// renamed to `<path>` only once every byte has arrived and the checksum matches.
class FileReceiver {
public:
static constexpr size_t kChunk = 1024; // must stay below the serial receive buffer
static constexpr uint32_t kTimeoutMs = 5000; // silence, or a card job that never completes
static constexpr uint32_t kMaxBytes = 8u << 20; // larger than any app partition
enum class State {
Idle,
Receiving, // waiting for bytes of the current chunk
Writing, // chunk() is complete: write it to partPath(), then call chunkWritten()
Finishing, // everything written and checked: rename partPath() to path(), then finished()
Done,
Failed, // error() says why; partPath() should be removed
};
// Parses "<path> <size> <sha256 hex>". Returns "" when the transfer starts, or what's wrong.
std::string begin(const std::string& args, uint32_t nowMs);
// Takes bytes for the current chunk; returns how many were used (none while a chunk waits).
size_t feed(const uint8_t* data, size_t len, uint32_t nowMs);
void chunkWritten(bool ok, uint32_t nowMs);
void finished(bool ok);
void tick(uint32_t nowMs);
void reset() { *this = FileReceiver(); }
State state() const { return state_; }
bool active() const { return state_ != State::Idle; }
// Bytes still missing from the current chunk: read no more than this from the serial port.
size_t wanted() const;
const std::vector<uint8_t>& chunk() const { return chunk_; }
const std::string& path() const { return path_; }
std::string partPath() const { return path_ + ".part"; }
uint32_t size() const { return size_; }
uint32_t received() const { return received_; }
const std::string& error() const { return error_; }
private:
void fail(const char* why);
State state_ = State::Idle;
std::string path_;
uint32_t size_ = 0;
uint32_t received_ = 0; // bytes in chunks already written
uint8_t expected_[32] = {};
Sha256 sha_;
std::vector<uint8_t> chunk_;
uint32_t lastActivityMs_ = 0;
std::string error_;
};
} // namespace roro