Public Access
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:
@@ -0,0 +1,114 @@
|
||||
#include "file_receiver.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace roro {
|
||||
|
||||
namespace {
|
||||
|
||||
int hexDigit(char c) {
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Splits on spaces (no iostreams: they cost about 200 KB of flash on the device).
|
||||
std::vector<std::string> words(const std::string& s) {
|
||||
std::vector<std::string> out;
|
||||
size_t i = 0;
|
||||
while (i < s.size()) {
|
||||
if (s[i] == ' ') {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
size_t end = s.find(' ', i);
|
||||
if (end == std::string::npos) end = s.size();
|
||||
out.push_back(s.substr(i, end - i));
|
||||
i = end;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string FileReceiver::begin(const std::string& args, uint32_t nowMs) {
|
||||
reset();
|
||||
std::vector<std::string> w = words(args);
|
||||
if (w.size() != 3) return "usage: sd put <path> <size> <sha256>";
|
||||
const std::string &path = w[0], &size = w[1], &sha = w[2];
|
||||
if (path.empty() || path[0] != '/' || path.size() > 128 || path.find("..") != std::string::npos)
|
||||
return "the path must be absolute, without ..";
|
||||
if (size.empty() || size.size() > 9 || size.find_first_not_of("0123456789") != std::string::npos)
|
||||
return "bad size";
|
||||
uint32_t bytes = static_cast<uint32_t>(std::strtoul(size.c_str(), nullptr, 10));
|
||||
if (bytes == 0 || bytes > kMaxBytes) return "bad size";
|
||||
if (sha.size() != 64) return "bad sha256";
|
||||
for (size_t i = 0; i < 32; i++) {
|
||||
int hi = hexDigit(sha[2 * i]), lo = hexDigit(sha[2 * i + 1]);
|
||||
if (hi < 0 || lo < 0) return "bad sha256";
|
||||
expected_[i] = static_cast<uint8_t>(hi << 4 | lo);
|
||||
}
|
||||
path_ = path;
|
||||
size_ = bytes;
|
||||
lastActivityMs_ = nowMs;
|
||||
chunk_.reserve(kChunk);
|
||||
state_ = State::Receiving;
|
||||
return "";
|
||||
}
|
||||
|
||||
size_t FileReceiver::wanted() const {
|
||||
if (state_ != State::Receiving) return 0;
|
||||
return std::min<size_t>(kChunk, size_ - received_) - chunk_.size();
|
||||
}
|
||||
|
||||
size_t FileReceiver::feed(const uint8_t* data, size_t len, uint32_t nowMs) {
|
||||
if (state_ != State::Receiving || len == 0) return 0;
|
||||
size_t take = std::min(len, wanted());
|
||||
chunk_.insert(chunk_.end(), data, data + take);
|
||||
sha_.update(data, take);
|
||||
lastActivityMs_ = nowMs;
|
||||
if (wanted() > 0) return take;
|
||||
|
||||
if (received_ + chunk_.size() == size_) {
|
||||
// The last chunk: refuse it before writing if the file arrived damaged.
|
||||
uint8_t got[32];
|
||||
sha_.finish(got);
|
||||
if (std::memcmp(got, expected_, sizeof got) != 0) {
|
||||
fail("checksum mismatch");
|
||||
return take;
|
||||
}
|
||||
}
|
||||
state_ = State::Writing;
|
||||
return take;
|
||||
}
|
||||
|
||||
void FileReceiver::chunkWritten(bool ok, uint32_t nowMs) {
|
||||
if (state_ != State::Writing) return;
|
||||
if (!ok) return fail("write failed");
|
||||
received_ += static_cast<uint32_t>(chunk_.size());
|
||||
chunk_.clear();
|
||||
lastActivityMs_ = nowMs;
|
||||
state_ = received_ == size_ ? State::Finishing : State::Receiving;
|
||||
}
|
||||
|
||||
void FileReceiver::finished(bool ok) {
|
||||
if (state_ != State::Finishing) return;
|
||||
if (!ok) return fail("rename failed");
|
||||
state_ = State::Done;
|
||||
}
|
||||
|
||||
void FileReceiver::tick(uint32_t nowMs) {
|
||||
if (state_ != State::Receiving && state_ != State::Writing && state_ != State::Finishing) return;
|
||||
if (nowMs - lastActivityMs_ >= kTimeoutMs) fail(state_ == State::Receiving ? "timed out" : "card not writable");
|
||||
}
|
||||
|
||||
void FileReceiver::fail(const char* why) {
|
||||
error_ = why;
|
||||
chunk_.clear();
|
||||
state_ = State::Failed;
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user