Public Access
Found by M3's shared-bus test: when the card refused a write, the retry closed the file (losing up to 3 KB of earlier chunks still in the write buffer), then truncate() extended it back with zeros. The checksum only covered the received bytes, so `put` reported success with 3 KB of zeros on the card. Now a retry gives up if the card lost data, and the finished file is read back and must hash the same before it's renamed. The card refuses a write about once in five 1.7 MB uploads, with the radio asleep as often as listening. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
70 lines
2.9 KiB
C++
70 lines
2.9 KiB
C++
#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);
|
|
// While Finishing: the SHA-256 of the file as read back from the card. The checksum on the
|
|
// received bytes doesn't prove the card kept them (a failed write can lose buffered data).
|
|
void cardChecked(const uint8_t digest[32]);
|
|
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
|