Fix Cascade power saving, USB Companion, and mOTA flows
This commit is contained in:
1 parent
21bfc97712
commit
7a4da3ccfb
59 files changed
+3946
-2437
No files matched your search
@@ -1,4 +1,5 @@
|
||||
#include "ArduinoSerialInterface.h"
|
||||
#include "CompanionFrameQueue.h"
|
||||
|
||||
#define RECV_STATE_IDLE 0
|
||||
#define RECV_STATE_HDR_FOUND 1
|
||||
@@ -11,6 +12,103 @@ void ArduinoSerialInterface::resetReceiveState() {
|
||||
_secondaryControlSequencePos = 0;
|
||||
_frame_len = 0;
|
||||
rx_len = 0;
|
||||
_last_rx_byte_ms = 0;
|
||||
}
|
||||
|
||||
void ArduinoSerialInterface::serviceReceiveTimeout() {
|
||||
if (_state != RECV_STATE_IDLE && _serial != nullptr
|
||||
&& _serial->available() == 0
|
||||
&& (uint32_t)(millis() - _last_rx_byte_ms) >= RX_FRAME_TIMEOUT_MS) {
|
||||
// A truncated length-prefixed frame must not hold the MCU awake forever or
|
||||
// turn bytes from a later session into the missing tail of the old frame.
|
||||
resetReceiveState();
|
||||
}
|
||||
}
|
||||
|
||||
void ArduinoSerialInterface::resetTransmitState() {
|
||||
_tx_queue_len = 0;
|
||||
_tx_offset = 0;
|
||||
}
|
||||
|
||||
bool ArduinoSerialInterface::enqueueFrame(const uint8_t src[], size_t len) {
|
||||
if (src == nullptr || len == 0 || len > MAX_FRAME_SIZE) return false;
|
||||
|
||||
// MSG_WAITING is level-triggered. Retaining more than one copy only takes
|
||||
// space away from command replies while a USB host is backpressuring us.
|
||||
if (src[0] == 0x83) {
|
||||
for (uint8_t i = 0; i < _tx_queue_len; ++i) {
|
||||
if (_tx_queue[i].len > 3 && _tx_queue[i].buf[3] == 0x83) return true;
|
||||
}
|
||||
}
|
||||
|
||||
const bool delivery_required = mesh::companionFrameRequiresDelivery(src, len);
|
||||
if (!delivery_required && _tx_queue_len >= TX_QUEUE_SIZE - 1) {
|
||||
return false; // reserve one slot for a response or required push
|
||||
}
|
||||
|
||||
if (_tx_queue_len == TX_QUEUE_SIZE) {
|
||||
if (!delivery_required) return false;
|
||||
|
||||
// A required frame may replace queued best-effort traffic. Never replace
|
||||
// the head after any of it has reached the host: doing so would splice two
|
||||
// frames together on the byte stream.
|
||||
const uint8_t first_evictable = _tx_offset == 0 ? 0 : 1;
|
||||
int evict = -1;
|
||||
for (int i = TX_QUEUE_SIZE - 1; i >= first_evictable; --i) {
|
||||
const TxFrame& queued = _tx_queue[i];
|
||||
if (!mesh::companionFrameRequiresDelivery(&queued.buf[3], queued.len - 3)) {
|
||||
evict = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (evict < 0) return false;
|
||||
for (uint8_t i = (uint8_t)evict; i + 1 < _tx_queue_len; ++i) {
|
||||
_tx_queue[i] = _tx_queue[i + 1];
|
||||
}
|
||||
--_tx_queue_len;
|
||||
}
|
||||
|
||||
TxFrame& frame = _tx_queue[_tx_queue_len++];
|
||||
frame.len = (uint16_t)(len + 3);
|
||||
frame.buf[0] = '>';
|
||||
frame.buf[1] = (uint8_t)(len & 0xFF);
|
||||
frame.buf[2] = (uint8_t)(len >> 8);
|
||||
memcpy(&frame.buf[3], src, len);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ArduinoSerialInterface::serviceTransmit() {
|
||||
if (!_flow_ctl || _passthroughMode || _serial == nullptr) return;
|
||||
if (!isConnected()) {
|
||||
// A disconnected USB endpoint cannot make progress. Its host also loses
|
||||
// any partial endpoint data on disconnect, so begin the next session clean.
|
||||
resetTransmitState();
|
||||
return;
|
||||
}
|
||||
|
||||
while (_tx_queue_len > 0) {
|
||||
TxFrame& frame = _tx_queue[0];
|
||||
const size_t remaining = frame.len - _tx_offset;
|
||||
int available = _serial->availableForWrite();
|
||||
if (available <= 0) return;
|
||||
|
||||
// A UART or CDC FIFO can be smaller than MAX_FRAME_SIZE. Drain the frame in
|
||||
// bounded chunks, retaining the offset and never allowing another frame to
|
||||
// interleave until this header and body are complete.
|
||||
const size_t attempt = (size_t)available < remaining
|
||||
? (size_t)available : remaining;
|
||||
size_t written = _serial->write(&frame.buf[_tx_offset], attempt);
|
||||
if (written > attempt) written = attempt;
|
||||
_tx_offset += (uint16_t)written;
|
||||
if (_tx_offset < frame.len) return;
|
||||
|
||||
for (uint8_t i = 0; i + 1 < _tx_queue_len; ++i) {
|
||||
_tx_queue[i] = _tx_queue[i + 1];
|
||||
}
|
||||
--_tx_queue_len;
|
||||
_tx_offset = 0;
|
||||
if (written < attempt) return;
|
||||
}
|
||||
}
|
||||
|
||||
bool ArduinoSerialInterface::checkControlSequence(uint8_t c,
|
||||
@@ -39,6 +137,7 @@ void ArduinoSerialInterface::setPassthroughMode(bool enabled) {
|
||||
_controlSequenceReceived = false;
|
||||
_secondaryControlSequenceReceived = false;
|
||||
resetReceiveState();
|
||||
resetTransmitState();
|
||||
}
|
||||
|
||||
bool ArduinoSerialInterface::takeControlSequence() {
|
||||
@@ -57,37 +156,52 @@ void ArduinoSerialInterface::enable() {
|
||||
_isEnabled = true;
|
||||
_controlSequenceReceived = false;
|
||||
_secondaryControlSequenceReceived = false;
|
||||
_has_received_frame = false;
|
||||
resetReceiveState();
|
||||
resetTransmitState();
|
||||
}
|
||||
void ArduinoSerialInterface::disable() {
|
||||
_isEnabled = false;
|
||||
_controlSequenceReceived = false;
|
||||
_secondaryControlSequenceReceived = false;
|
||||
_has_received_frame = false;
|
||||
resetReceiveState();
|
||||
resetTransmitState();
|
||||
}
|
||||
|
||||
bool ArduinoSerialInterface::isConnected() const {
|
||||
if (_serial == nullptr) return false;
|
||||
if (_conn_check) return _conn_check();
|
||||
return true; // no way of knowing, so assume yes
|
||||
}
|
||||
|
||||
void ArduinoSerialInterface::loop() {
|
||||
serviceReceiveTimeout();
|
||||
serviceTransmit();
|
||||
}
|
||||
|
||||
bool ArduinoSerialInterface::isReadBusy() const {
|
||||
return false;
|
||||
return _state != RECV_STATE_IDLE;
|
||||
}
|
||||
|
||||
bool ArduinoSerialInterface::isWriteBusy() const {
|
||||
if (_passthroughMode) return false;
|
||||
if (_passthroughMode || _serial == nullptr) return false;
|
||||
if (_flow_ctl && isConnected()) {
|
||||
return const_cast<Stream*>(_serial)->availableForWrite() < (int)(MAX_FRAME_SIZE + 3);
|
||||
if (_tx_queue_len > 0) return true;
|
||||
return const_cast<Stream*>(_serial)->availableForWrite() <= 0;
|
||||
}
|
||||
// while nobody drains the port the TX buffer stays full, so never report
|
||||
// busy in that case: it would stall the paced streams on all interfaces
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ArduinoSerialInterface::hasPendingIO() const {
|
||||
return isReadBusy() || _tx_queue_len > 0;
|
||||
}
|
||||
|
||||
size_t ArduinoSerialInterface::writeFrame(const uint8_t src[], size_t len) {
|
||||
if (len > MAX_FRAME_SIZE) {
|
||||
// frame is too big!
|
||||
if (src == nullptr || len == 0 || len > MAX_FRAME_SIZE || _serial == nullptr) {
|
||||
// invalid frame or an interface which has not begun yet
|
||||
return 0;
|
||||
}
|
||||
if (_passthroughMode) return len;
|
||||
@@ -95,29 +209,29 @@ size_t ArduinoSerialInterface::writeFrame(const uint8_t src[], size_t len) {
|
||||
if (!isConnected()) {
|
||||
return len; // nobody is listening, drop instead of filling the TX buffer
|
||||
}
|
||||
if (_serial->availableForWrite() < (int)(len + 3)) {
|
||||
// a short write would tear the length prefixed framing, and as there is
|
||||
// neither a checksum nor a resync marker the receiver would stay out of
|
||||
// sync forever - so drop the whole frame instead
|
||||
return 0;
|
||||
}
|
||||
if (!enqueueFrame(src, len)) return 0;
|
||||
serviceTransmit();
|
||||
return len;
|
||||
}
|
||||
|
||||
uint8_t hdr[3];
|
||||
hdr[0] = '>';
|
||||
hdr[1] = (len & 0xFF); // LSB
|
||||
hdr[2] = (len >> 8); // MSB
|
||||
|
||||
_serial->write(hdr, 3);
|
||||
return _serial->write(src, len);
|
||||
uint8_t frame[MAX_FRAME_SIZE + 3];
|
||||
frame[0] = '>';
|
||||
frame[1] = (uint8_t)(len & 0xFF); // LSB
|
||||
frame[2] = (uint8_t)(len >> 8); // MSB
|
||||
memcpy(&frame[3], src, len);
|
||||
return _serial->write(frame, len + 3) == len + 3 ? len : 0;
|
||||
}
|
||||
|
||||
size_t ArduinoSerialInterface::checkRecvFrame(uint8_t dest[]) {
|
||||
if (_serial == nullptr || dest == nullptr) return 0;
|
||||
serviceReceiveTimeout();
|
||||
serviceTransmit();
|
||||
if (_passthroughMode) return 0;
|
||||
|
||||
while (_serial->available()) {
|
||||
int c = _serial->read();
|
||||
if (c < 0) break;
|
||||
_last_rx_byte_ms = millis();
|
||||
|
||||
switch (_state) {
|
||||
case RECV_STATE_IDLE:
|
||||
@@ -152,6 +266,7 @@ size_t ArduinoSerialInterface::checkRecvFrame(uint8_t dest[]) {
|
||||
memcpy(dest, rx_buf, _frame_len);
|
||||
_state = RECV_STATE_IDLE; // reset state, for next frame
|
||||
_last_frame_ms = millis(); // a real client is talking to us
|
||||
_has_received_frame = true;
|
||||
return _frame_len;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,14 @@ public:
|
||||
typedef bool (*ConnectedCheck)();
|
||||
|
||||
private:
|
||||
static constexpr uint8_t TX_QUEUE_SIZE = 4;
|
||||
static constexpr uint32_t RX_FRAME_TIMEOUT_MS = 1000;
|
||||
|
||||
struct TxFrame {
|
||||
uint16_t len;
|
||||
uint8_t buf[MAX_FRAME_SIZE + 3];
|
||||
};
|
||||
|
||||
bool _isEnabled;
|
||||
bool _passthroughMode;
|
||||
bool _controlSequenceReceived;
|
||||
@@ -20,15 +28,24 @@ private:
|
||||
uint16_t _frame_len;
|
||||
uint16_t rx_len;
|
||||
uint32_t _last_frame_ms;
|
||||
uint32_t _last_rx_byte_ms;
|
||||
bool _has_received_frame;
|
||||
Stream* _serial;
|
||||
const char* _controlSequence;
|
||||
const char* _secondaryControlSequence;
|
||||
ConnectedCheck _conn_check;
|
||||
uint8_t rx_buf[MAX_FRAME_SIZE];
|
||||
TxFrame _tx_queue[TX_QUEUE_SIZE];
|
||||
uint8_t _tx_queue_len;
|
||||
uint16_t _tx_offset;
|
||||
|
||||
bool checkControlSequence(uint8_t c, const char* sequence,
|
||||
size_t& position, bool& received);
|
||||
void resetReceiveState();
|
||||
void serviceReceiveTimeout();
|
||||
void resetTransmitState();
|
||||
bool enqueueFrame(const uint8_t src[], size_t len);
|
||||
void serviceTransmit();
|
||||
|
||||
public:
|
||||
ArduinoSerialInterface()
|
||||
@@ -36,8 +53,10 @@ public:
|
||||
_controlSequenceReceived(false), _secondaryControlSequenceReceived(false),
|
||||
_flow_ctl(false), _state(0), _controlSequencePos(0),
|
||||
_secondaryControlSequencePos(0), _frame_len(0), rx_len(0),
|
||||
_last_frame_ms(0), _serial(nullptr), _controlSequence(nullptr),
|
||||
_secondaryControlSequence(nullptr), _conn_check(nullptr) {}
|
||||
_last_frame_ms(0), _last_rx_byte_ms(0), _has_received_frame(false),
|
||||
_serial(nullptr), _controlSequence(nullptr),
|
||||
_secondaryControlSequence(nullptr), _conn_check(nullptr),
|
||||
_tx_queue_len(0), _tx_offset(0) {}
|
||||
|
||||
void begin(Stream& serial, const char* controlSequence = nullptr,
|
||||
const char* secondaryControlSequence = nullptr) {
|
||||
@@ -48,7 +67,9 @@ public:
|
||||
_controlSequenceReceived = false;
|
||||
_secondaryControlSequenceReceived = false;
|
||||
_last_frame_ms = 0;
|
||||
_has_received_frame = false;
|
||||
resetReceiveState();
|
||||
resetTransmitState();
|
||||
#ifdef RAK_4631
|
||||
pinMode(WB_IO2, OUTPUT);
|
||||
#endif
|
||||
@@ -71,12 +92,17 @@ public:
|
||||
// millis() of the last completely received frame, 0 if none since boot.
|
||||
// Useful as an activity-based connection check where no DTR state exists.
|
||||
uint32_t getLastFrameMillis() const { return _last_frame_ms; }
|
||||
bool hasReceivedFrame() const { return _has_received_frame; }
|
||||
|
||||
// Optional: only hand a frame to the stream when it fits into the TX buffer
|
||||
// as a whole, and report busy while it does not, so bulk streams get paced.
|
||||
// Optional: queue complete frames and drain one at a time according to the
|
||||
// stream's TX capacity, so short writes cannot discard or interleave bytes.
|
||||
// Report busy while a frame remains queued so bulk streams get paced.
|
||||
// Only enable this for streams which really implement availableForWrite()
|
||||
// (USB-CDC does, the Print default returns 0).
|
||||
void enableFlowControl(bool enable) { _flow_ctl = enable; }
|
||||
void enableFlowControl(bool enable) {
|
||||
if (!enable) resetTransmitState();
|
||||
_flow_ctl = enable;
|
||||
}
|
||||
|
||||
// BaseSerialInterface methods
|
||||
void enable() override;
|
||||
@@ -84,9 +110,11 @@ public:
|
||||
bool isEnabled() const override { return _isEnabled; }
|
||||
|
||||
bool isConnected() const override;
|
||||
void loop() override;
|
||||
|
||||
bool isReadBusy() const override;
|
||||
bool isWriteBusy() const override;
|
||||
bool hasPendingIO() const override;
|
||||
size_t writeFrame(const uint8_t src[], size_t len) override;
|
||||
size_t checkRecvFrame(uint8_t dest[]) override;
|
||||
};
|
||||
@@ -25,6 +25,11 @@ public:
|
||||
// Returns true once for each pending Bluetooth pairing prompt. Non-BLE
|
||||
// transports keep the default implementation so UI code can poll safely.
|
||||
virtual bool takePairingRequest() { return false; }
|
||||
// Multi-transport implementations can pin a sequence of response frames to
|
||||
// the interface which supplied the current command. Single transports have
|
||||
// nothing to route, so their default implementations are no-ops.
|
||||
virtual void lockReplyRoute() {}
|
||||
virtual void unlockReplyRoute() {}
|
||||
virtual size_t writeFrame(const uint8_t src[], size_t len) = 0;
|
||||
virtual size_t checkRecvFrame(uint8_t dest[]) = 0;
|
||||
};
|
||||
@@ -3689,6 +3689,13 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
sprintf(reply, "OK - %s,%lu,%lu", enable ? "on" : "off",
|
||||
(unsigned long)rx_us, (unsigned long)sleep_us);
|
||||
}
|
||||
} else if (strcmp(config, "powersaving on") == 0
|
||||
|| strcmp(config, "powersaving off") == 0) {
|
||||
const bool enabled = strcmp(&config[12], "on") == 0;
|
||||
_prefs->powersaving_enabled = enabled ? 1 : 0;
|
||||
_sensors->setPowerSavingEnabled(enabled);
|
||||
savePrefs();
|
||||
sprintf(reply, "OK - powersaving %s", enabled ? "on" : "off");
|
||||
} else if (memcmp(config, "radio ", 6) == 0) {
|
||||
strcpy(tmp, &config[6]);
|
||||
const char *parts[4];
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
#ifndef DEFAULT_CAD_ENABLED
|
||||
#define DEFAULT_CAD_ENABLED 0
|
||||
#endif
|
||||
#ifndef DEFAULT_POWERSAVING_ENABLED
|
||||
#define DEFAULT_POWERSAVING_ENABLED 0
|
||||
#endif
|
||||
|
||||
#if defined(ESP32_PLATFORM) || defined(USER_GPIO_CONTROL)
|
||||
#include <helpers/UserGpio.h>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "BaseSerialInterface.h"
|
||||
#include "CompanionFrameQueue.h"
|
||||
|
||||
#ifndef MAX_INTERFACES
|
||||
// ble, usb, wifi, ethernet
|
||||
@@ -26,6 +27,31 @@ private:
|
||||
|
||||
bool _enabled = false;
|
||||
RegisteredInterface _interfaces[MAX_INTERFACES] = {};
|
||||
BaseSerialInterface* _lastRxInterface = nullptr;
|
||||
BaseSerialInterface* _lockedReplyInterface = nullptr;
|
||||
|
||||
bool isAvailableReplyTarget(BaseSerialInterface* target) const {
|
||||
if (target == nullptr) return false;
|
||||
for (auto iface : _interfaces) {
|
||||
if (iface.instance == target) {
|
||||
return target->isEnabled() && target->isConnected();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void clearReplyRouteFor(BaseSerialInterface* target) {
|
||||
// Keep a locked pointer until the response producer observes that its
|
||||
// target is unavailable and aborts the transaction. Clearing it here would
|
||||
// make the remaining frames fall back to broadcast on another interface.
|
||||
if (_lockedReplyInterface == target) return;
|
||||
if (_lastRxInterface == target) _lastRxInterface = nullptr;
|
||||
}
|
||||
|
||||
BaseSerialInterface* replyTarget() const {
|
||||
return _lockedReplyInterface != nullptr
|
||||
? _lockedReplyInterface : _lastRxInterface;
|
||||
}
|
||||
|
||||
public:
|
||||
bool addInterface(InterfaceType type, BaseSerialInterface* iface) {
|
||||
@@ -56,6 +82,7 @@ public:
|
||||
// find and remove interface
|
||||
for(int i = 0; i < MAX_INTERFACES; i++){
|
||||
if(_interfaces[i].instance == iface){
|
||||
clearReplyRouteFor(iface);
|
||||
_interfaces[i] = {};
|
||||
return true;
|
||||
}
|
||||
@@ -77,6 +104,7 @@ public:
|
||||
for(auto iface : _interfaces){
|
||||
if(iface.instance && iface.type == InterfaceType::Bluetooth){
|
||||
iface.instance->disable();
|
||||
clearReplyRouteFor(iface.instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,6 +133,8 @@ public:
|
||||
// enable all interfaces
|
||||
void enable() override {
|
||||
_enabled = true;
|
||||
_lastRxInterface = nullptr;
|
||||
_lockedReplyInterface = nullptr;
|
||||
for(auto iface : _interfaces){
|
||||
if(iface.instance){
|
||||
iface.instance->enable();
|
||||
@@ -115,6 +145,8 @@ public:
|
||||
// disable all interfaces
|
||||
void disable() override {
|
||||
_enabled = false;
|
||||
_lastRxInterface = nullptr;
|
||||
_lockedReplyInterface = nullptr;
|
||||
for(auto iface : _interfaces){
|
||||
if(iface.instance){
|
||||
iface.instance->disable();
|
||||
@@ -132,9 +164,17 @@ public:
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if any interface is connected
|
||||
// A locked multi-frame reply belongs to one client. Treat losing that
|
||||
// client as a disconnect even if another transport remains connected, so
|
||||
// the producer aborts instead of leaking the remainder to somebody else.
|
||||
if (_lockedReplyInterface != nullptr) {
|
||||
return isAvailableReplyTarget(_lockedReplyInterface);
|
||||
}
|
||||
|
||||
// check if any enabled interface is connected
|
||||
for(auto iface : _interfaces){
|
||||
if(iface.instance && iface.instance->isConnected()) {
|
||||
if(iface.instance && iface.instance->isEnabled()
|
||||
&& iface.instance->isConnected()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -158,7 +198,12 @@ public:
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if any interface is busy
|
||||
// Pace a response stream against its destination. A slow inactive BLE or
|
||||
// WiFi client must not stall a contact sync running over USB (or vice versa).
|
||||
BaseSerialInterface* target = replyTarget();
|
||||
if (isAvailableReplyTarget(target)) return target->isWriteBusy();
|
||||
|
||||
// With no requester yet, preserve the aggregate behavior used for pushes.
|
||||
for(auto iface : _interfaces){
|
||||
if(iface.instance && iface.instance->isEnabled() && iface.instance->isWriteBusy()){
|
||||
return true;
|
||||
@@ -202,13 +247,35 @@ public:
|
||||
return false;
|
||||
}
|
||||
|
||||
void lockReplyRoute() override {
|
||||
if (isAvailableReplyTarget(_lastRxInterface)) {
|
||||
_lockedReplyInterface = _lastRxInterface;
|
||||
}
|
||||
}
|
||||
|
||||
void unlockReplyRoute() override {
|
||||
_lockedReplyInterface = nullptr;
|
||||
}
|
||||
|
||||
size_t writeFrame(const uint8_t src[], size_t len) override {
|
||||
// don't write when disabled or nothing provided
|
||||
if(!_enabled || len == 0){
|
||||
if(!_enabled || src == nullptr || len == 0){
|
||||
return 0;
|
||||
}
|
||||
|
||||
// write frame to all enabled interfaces
|
||||
// Responses and delivery-required completion pushes belong to the client
|
||||
// which supplied the latest command. Best-effort asynchronous observations
|
||||
// remain broadcast so passive connected apps can keep their view fresh.
|
||||
if (mesh::companionFrameRequiresDelivery(src, len)) {
|
||||
BaseSerialInterface* target = replyTarget();
|
||||
if (target != nullptr) {
|
||||
if (!isAvailableReplyTarget(target)) return 0;
|
||||
return target->writeFrame(src, len);
|
||||
}
|
||||
}
|
||||
|
||||
// Before any command establishes a reply route, or for best-effort pushes,
|
||||
// write the frame to all enabled interfaces.
|
||||
bool allSuccessful = true;
|
||||
for(auto iface : _interfaces){
|
||||
if(iface.instance && iface.instance->isEnabled()){
|
||||
@@ -224,15 +291,30 @@ public:
|
||||
|
||||
size_t checkRecvFrame(uint8_t dest[]) override {
|
||||
// don't read when disabled
|
||||
if(!_enabled){
|
||||
if(!_enabled || dest == nullptr){
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Keep a multi-frame response transaction on its originating transport.
|
||||
// Other interfaces retain their input until the producer unlocks the route.
|
||||
if (_lockedReplyInterface != nullptr) {
|
||||
if (!isAvailableReplyTarget(_lockedReplyInterface)) return 0;
|
||||
size_t frameSize = _lockedReplyInterface->checkRecvFrame(dest);
|
||||
if (frameSize > 0) _lastRxInterface = _lockedReplyInterface;
|
||||
return frameSize;
|
||||
}
|
||||
|
||||
if (_lastRxInterface != nullptr
|
||||
&& !isAvailableReplyTarget(_lastRxInterface)) {
|
||||
_lastRxInterface = nullptr;
|
||||
}
|
||||
|
||||
// try to read a frame from any enabled interface
|
||||
for(auto iface : _interfaces){
|
||||
if(iface.instance && iface.instance->isEnabled()){
|
||||
size_t frameSize = iface.instance->checkRecvFrame(dest);
|
||||
if(frameSize > 0){
|
||||
_lastRxInterface = iface.instance;
|
||||
return frameSize;
|
||||
}
|
||||
}
|
||||
|
||||
+1617
-1618
File diff suppressed because it is too large.
Load diff
@@ -4,7 +4,7 @@
|
||||
#include "OtaStore.h"
|
||||
|
||||
// An OtaStore that captures an in-transit `.mota` onto a HOST folder over the mota-seeder link (the WRITE
|
||||
// half of MotaSeederProto: OP_STAT/BEGIN/WRITE/SREAD/FIN). This is the destination for `ota pull <#> folder`:
|
||||
// half of MotaSeederProto: OP_STAT/BEGIN/WRITE/SREAD/FIN). This is the destination for `ota pull <id> folder`:
|
||||
// blocks stream straight to the host `<mid>.mota` - the device holds NO RAM/flash staging for it.
|
||||
//
|
||||
// begin(total) -> OP_BEGIN (host creates a 0xFF-filled <midhex>.mota.part)
|
||||
|
||||
+204
-56
@@ -1,6 +1,6 @@
|
||||
#include "OtaCli.h"
|
||||
#include "OtaContext.h"
|
||||
#include "FolderMotaStore.h" // `ota pull <#> folder` destination (set_mid on the connected folder store)
|
||||
#include "FolderMotaStore.h" // `ota pull <id> folder` destination (set_mid on the connected folder store)
|
||||
#include "OtaVerify.h"
|
||||
#include "OtaSelf.h"
|
||||
#include "OtaTargets.h" // ota_target_env_name(): human-readable name for a target_id (no string on the wire)
|
||||
@@ -83,6 +83,21 @@ static const char* state_short(OtaManager::FetchState s) {
|
||||
}
|
||||
}
|
||||
|
||||
static const char* fetch_error_word(OtaManager::FetchError error) {
|
||||
switch (error) {
|
||||
case OtaManager::FETCH_ERROR_MANIFEST: return "invalid manifest";
|
||||
case OtaManager::FETCH_ERROR_HASH_ALGO: return "unsupported hash";
|
||||
case OtaManager::FETCH_ERROR_CODEC: return "unsupported codec";
|
||||
case OtaManager::FETCH_ERROR_GEOMETRY: return "invalid geometry";
|
||||
case OtaManager::FETCH_ERROR_TOO_LARGE: return "image too large";
|
||||
case OtaManager::FETCH_ERROR_STORAGE: return "storage error";
|
||||
case OtaManager::FETCH_ERROR_INTEGRITY: return "integrity check";
|
||||
case OtaManager::FETCH_ERROR_MANIFEST_TIMEOUT: return "manifest timeout";
|
||||
case OtaManager::FETCH_ERROR_LEAVES_TIMEOUT: return "leaves timeout";
|
||||
default: return "none";
|
||||
}
|
||||
}
|
||||
|
||||
// Render the packed fw_version as "v1.2.3" (or "v1.2.3.4" when a prerelease byte is set).
|
||||
static void ver_str(char* out, size_t cap, uint32_t v) {
|
||||
FwVersion fw = FwVersion::unpack(v);
|
||||
@@ -129,16 +144,16 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
if (is_cmd(a, "help|?|h", &rest)) {
|
||||
#if defined(OTA_SEEDER_ONLY)
|
||||
snprintf(reply, 160,
|
||||
"OTA seeder: status | stats | ls=find images | get <#> folder=capture | cancel | "
|
||||
"OTA seeder: status | stats | ls=find images | get <id> folder=capture | cancel | "
|
||||
"announce | folder | config. LoRa install is disabled.");
|
||||
#elif defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE)
|
||||
strcpy(reply,
|
||||
"OTA: status | stats | ls | get | install | rescue install <hash16> | cancel | announce | "
|
||||
"self | folder | config | key");
|
||||
"OTA: status | stats | ls | get <id> flash [rescue] | install | rescue install <hash16> | "
|
||||
"cancel | announce | self | folder | config | key");
|
||||
#else
|
||||
snprintf(reply, 160,
|
||||
"OTA: status | stats=admin ids/hashes | ls=find updates | get <#>=download | install | cancel | "
|
||||
"announce | self | folder | cache | config | key. Use `ota ls [page]`.");
|
||||
"OTA: status | stats | ls | get <id> flash | install | cancel | announce | self | folder | "
|
||||
"cache | config | key. `ota ls [page]`; folder [validate].");
|
||||
#endif
|
||||
|
||||
// ---- inventory dashboard: running fw (self), the one fetch session, serving state ----
|
||||
@@ -162,7 +177,12 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
unsigned have = (unsigned)c.manager.blocksHave(), tot = (unsigned)c.manager.blocksTotal();
|
||||
unsigned pct = tot ? (unsigned)((uint64_t)have * 100 / tot) : 0;
|
||||
unsigned age = c.session_started_ms ? (unsigned)((millis() - c.session_started_ms) / 1000) : 0;
|
||||
snprintf(dl, sizeof dl, "download: %s %u/%u (%u%%) id=%s %us", state_word(fs), have, tot, pct, midhx, age);
|
||||
if (fs == OtaManager::FAILED)
|
||||
snprintf(dl, sizeof dl, "download: failed (%s) %u/%u id=%s",
|
||||
fetch_error_word(c.manager.fetchError()), have, tot, midhx);
|
||||
else
|
||||
snprintf(dl, sizeof dl, "download: %s %u/%u (%u%%) id=%s %us",
|
||||
state_word(fs), have, tot, pct, midhx, age);
|
||||
}
|
||||
const char* hw = (c.hw_id[0]) ? c.hw_id : "?";
|
||||
const char* tenv = ota_target_env_name(c.manager.target()); // env name, or "?" if not in the table
|
||||
@@ -211,7 +231,12 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
unsigned have = (unsigned)c.manager.blocksHave(), tot = (unsigned)c.manager.blocksTotal();
|
||||
unsigned pct = tot ? (unsigned)((uint64_t)have * 100 / tot) : 0;
|
||||
unsigned age = c.session_started_ms ? (unsigned)((millis() - c.session_started_ms) / 1000) : 0;
|
||||
snprintf(fbuf, sizeof fbuf, "fetch %s %u/%u %u%% id=%s %us", state_short(fs), have, tot, pct, fmid, age);
|
||||
if (fs == OtaManager::FAILED)
|
||||
snprintf(fbuf, sizeof fbuf, "fetch failed:%s %u/%u id=%s",
|
||||
fetch_error_word(c.manager.fetchError()), have, tot, fmid);
|
||||
else
|
||||
snprintf(fbuf, sizeof fbuf, "fetch %s %u/%u %u%% id=%s %us",
|
||||
state_short(fs), have, tot, pct, fmid, age);
|
||||
}
|
||||
uint8_t af = c.manager.autofetch();
|
||||
snprintf(reply, 160, "OTA | fw %s id=%s body=%s %ub %uK | serv %u dg=%s | %s | af=%s hops=%u",
|
||||
@@ -236,11 +261,18 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
snprintf(reply, CAP, "ERR update page %u out of range (1-%u)", (unsigned)page, (unsigned)pages);
|
||||
return true;
|
||||
}
|
||||
int n = snprintf(reply, CAP, "Updates %u/%u (%u src) - `ota get <#>`:",
|
||||
int n = snprintf(reply, CAP, "Updates %u/%u (%u src; refreshing):",
|
||||
(unsigned)page, (unsigned)pages, (unsigned)c.manager.sourceCount());
|
||||
OtaManager::FetchState fs = c.manager.fetchState();
|
||||
const uint8_t* cur = (fs != OtaManager::IDLE) ? c.manager.fetchManifestId() : nullptr;
|
||||
uint32_t myt = c.manager.target(); // effective target (EndF identity if present, else build flag)
|
||||
#if defined(NRF52_PLATFORM)
|
||||
const OtaBlCaps& list_bl = c.bootloaderCaps();
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE)
|
||||
SelfFwInfo list_self;
|
||||
bool list_has_endf = ota_self_firmware(list_self) && list_self.valid;
|
||||
#endif
|
||||
uint32_t now = millis(); int shown = 0;
|
||||
uint16_t first = (uint16_t)(page - 1) * PAGE_SIZE;
|
||||
uint16_t last = first + PAGE_SIZE; if (last > count) last = count;
|
||||
@@ -258,18 +290,32 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
}
|
||||
uint32_t age = (now - h->last_ms) / 1000; if (age > 99999) age = 99999;
|
||||
char ver[20]; ver_str(ver, sizeof ver, h->fw_version);
|
||||
// What is this update for? "yours" if same target (hw+role) as us; else the target's env name when we
|
||||
// know it (named locally from its 4-byte target_id - no string travels on the wire); else the raw
|
||||
// target_id hex (an env this build's OtaTargets.h table doesn't know) or '?' for an unset target.
|
||||
// Target equality alone is not an install-safety claim. Surface local codec/bootloader limitations,
|
||||
// and flag the explicit rescue path when this internal-flash nRF52 has no valid running EndF.
|
||||
char hwbuf[16];
|
||||
const char* fit;
|
||||
const char* env = ota_target_env_name(h->target_id);
|
||||
if (myt && h->target_id == myt) fit = "yours";
|
||||
else if (env) fit = env;
|
||||
else if (h->target_id == 0) fit = "?";
|
||||
if (myt && h->target_id == myt) {
|
||||
bool installable = c.manager.codecOk(h->codec);
|
||||
#if defined(NRF52_PLATFORM)
|
||||
installable = installable && list_bl.present && list_bl.apply_abi >= MOTA_FORMAT_VER
|
||||
&& h->codec < 16 && (list_bl.codec_mask & (1u << h->codec));
|
||||
#if defined(OTA_SD_STORE)
|
||||
installable = installable && (list_bl.storage_flags & OTA_BL_STORAGE_SD);
|
||||
#endif
|
||||
#endif
|
||||
if (!installable) fit = "unsupported";
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE)
|
||||
else if (!list_has_endf) fit = "rescue";
|
||||
#endif
|
||||
else fit = "same target";
|
||||
} else if (env) fit = env;
|
||||
else if (h->target_id == 0) fit = "?";
|
||||
else { snprintf(hwbuf, sizeof hwbuf, "hw %08X", (unsigned)h->target_id); fit = hwbuf; }
|
||||
n += snprintf(reply + n, CAP - n, "\n %u) %s %s [%s] %un %us%s", (unsigned)(i + 1), ver,
|
||||
codec_kind(h->codec), fit, (unsigned)h->n_seeders, (unsigned)age, tag);
|
||||
char midhx[9]; mesh::Utils::toHex(midhx, h->mid, 4);
|
||||
n += snprintf(reply + n, CAP - n, "\n %u) %s %s %s [%s] %un %us%s",
|
||||
(unsigned)(i + 1), midhx, ver, codec_kind(h->codec), fit,
|
||||
(unsigned)h->n_seeders, (unsigned)age, tag);
|
||||
shown++;
|
||||
}
|
||||
if (shown == 0) strcpy(reply, "No updates seen yet - re-run `ota ls` in a few seconds (just asked around).");
|
||||
@@ -283,19 +329,29 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
while (p[i] && p[i] != ' ' && i < (int)sizeof(selstr) - 1) { selstr[i] = p[i]; i++; }
|
||||
selstr[i] = 0;
|
||||
const char* dst = p + i; while (*dst == ' ') dst++;
|
||||
if (selstr[0] == 0) { strcpy(reply, "usage: ota pull <#> <flash|folder> (see `ota ls`)"); return true; }
|
||||
if (selstr[0] == 0) { strcpy(reply, "usage: ota pull <id> flash [rescue] | folder [validate] (see `ota ls`)"); return true; }
|
||||
// resolve the catalogue row (index or explicit manifest_id)
|
||||
const OtaManager::CatRow* sel = nullptr; uint8_t mid[4];
|
||||
bool isnum = (selstr[0] == '#');
|
||||
if (!isnum) { isnum = true; for (const char* x = selstr; *x; x++) if (*x < '0' || *x > '9') { isnum = false; break; } }
|
||||
if (isnum) {
|
||||
int idx = atoi(selstr[0] == '#' ? selstr + 1 : selstr);
|
||||
if (idx >= 1 && idx <= c.manager.catalogCount()) sel = c.manager.catalogRow((uint8_t)(idx - 1));
|
||||
} else if (mesh::Utils::fromHex(mid, 4, selstr)) {
|
||||
// An eight-digit all-numeric manifest ID is still an ID, not a huge list index. Bare short decimal
|
||||
// values retain the legacy index form; `#N` is the unambiguous explicit index spelling.
|
||||
size_t selector_len = strlen(selstr);
|
||||
bool explicit_index = selstr[0] == '#';
|
||||
const char* index_text = explicit_index ? selstr + 1 : selstr;
|
||||
bool decimal = *index_text != 0;
|
||||
uint16_t index = 0;
|
||||
for (const char* x = index_text; *x && decimal; x++) {
|
||||
if (*x < '0' || *x > '9') decimal = false;
|
||||
else if (index > 255 / 10 || (index == 255 / 10 && (uint8_t)(*x - '0') > 255 % 10)) decimal = false;
|
||||
else index = (uint16_t)(index * 10 + (uint8_t)(*x - '0'));
|
||||
}
|
||||
bool use_index = explicit_index || (selector_len != 8 && decimal);
|
||||
if (use_index && decimal) {
|
||||
if (index >= 1 && index <= c.manager.catalogCount()) sel = c.manager.catalogRow((uint8_t)(index - 1));
|
||||
} else if (!explicit_index && selector_len == 8 && mesh::Utils::fromHex(mid, 4, selstr)) {
|
||||
for (uint8_t k = 0; k < c.manager.catalogCount(); k++)
|
||||
if (memcmp(c.manager.catalogRow(k)->mid, mid, 4) == 0) { sel = c.manager.catalogRow(k); break; }
|
||||
}
|
||||
if (!sel) { strcpy(reply, "ERR no such update (see the numbers in `ota ls`)"); return true; }
|
||||
if (!sel) { strcpy(reply, "ERR no such update (copy its eight-digit ID from `ota ls`)"); return true; }
|
||||
// destination is MANDATORY: with none given, show the choices (flash always; folder iff a link is up).
|
||||
if (*dst == 0) {
|
||||
#if defined(OTA_SEEDER_ONLY)
|
||||
@@ -315,40 +371,112 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
return true;
|
||||
}
|
||||
if (c.apply_pending) { strcpy(reply, "ERR busy applying"); return true; }
|
||||
// optional 3rd token: `folder validate` -> leaf-diff warm-start against a seed motatool staged (capture only)
|
||||
bool validate = false;
|
||||
{ const char* q = dst; while (*q && *q != ' ') q++; while (*q == ' ') q++;
|
||||
if (strncmp(q, "validate", 8) == 0) validate = true; }
|
||||
uint8_t selmid[4]; uint32_t seltgt = sel->target_id; memcpy(selmid, sel->mid, 4); // sel may move on reset
|
||||
OtaStore* store; const char* dname;
|
||||
if (strncmp(dst, "flash", 5) == 0) {
|
||||
// Parse exact destination/options. Prefix matches used to accept typos such as "flashgarbage" and an
|
||||
// ignored third token, which is especially unsafe for the explicit no-EndF rescue acknowledgement.
|
||||
char destination[8] = {0}, option[10] = {0}, extra[2] = {0};
|
||||
int parts = sscanf(dst, "%7s %9s %1s", destination, option, extra);
|
||||
if (parts < 1 || parts > 2) {
|
||||
strcpy(reply, "ERR usage: ota pull <id> flash [rescue] | folder [validate]");
|
||||
return true;
|
||||
}
|
||||
bool to_flash = strcmp(destination, "flash") == 0;
|
||||
bool to_folder = strcmp(destination, "folder") == 0;
|
||||
bool validate = to_folder && parts == 2 && strcmp(option, "validate") == 0;
|
||||
bool rescue = to_flash && parts == 2 && strcmp(option, "rescue") == 0;
|
||||
if ((!to_flash && !to_folder) || (parts == 2 && !validate && !rescue)) {
|
||||
strcpy(reply, "ERR usage: ota pull <id> flash [rescue] | folder [validate]");
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t selmid[4]; uint32_t seltgt = sel->target_id; uint8_t selcodec = sel->codec;
|
||||
memcpy(selmid, sel->mid, 4); // sel may move when catalog traffic arrives
|
||||
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
c.stopSdCacheFetch(); // explicit operator work owns the receive slot
|
||||
#endif
|
||||
OtaManager::FetchState current = c.manager.fetchState();
|
||||
if (current != OtaManager::IDLE && current != OtaManager::FAILED) {
|
||||
snprintf(reply, 160, "ERR OTA slot is %s; use `ota cancel` before replacing it", state_word(current));
|
||||
return true;
|
||||
}
|
||||
|
||||
OtaStore* store = nullptr; const char* dname = nullptr;
|
||||
if (to_flash) {
|
||||
#if defined(OTA_SEEDER_ONLY)
|
||||
strcpy(reply, "ERR seeder-only build cannot stage or install firmware; use `folder`");
|
||||
return true;
|
||||
#else
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
c.stopSdCacheFetch(); // manual install download takes priority over archiving
|
||||
#endif
|
||||
store = &c.fetch_store; c.fetch_store.clear(); dname = "flash"; validate = false; // seed lives in the folder
|
||||
#if defined(NRF52_PLATFORM) && !defined(OTA_SD_STORE)
|
||||
c.manager.set_accept_full(false); // nRF52 flash can install only in-place deltas
|
||||
#endif
|
||||
#endif
|
||||
} else if (strncmp(dst, "folder", 6) == 0) {
|
||||
if (!c.folder_dest) { strcpy(reply, "ERR no folder connected (run motatool serve --tcp/--serial)"); return true; }
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
c.stopSdCacheFetch(); // an explicit host capture also takes priority
|
||||
#endif
|
||||
c.folder_dest->set_mid(selmid); store = c.folder_dest; dname = validate ? "folder+validate" : "folder";
|
||||
if (!c.manager.codecOk(selcodec)) {
|
||||
snprintf(reply, 160,
|
||||
"ERR %s codec %u cannot be installed by this build; use `ota pull %s folder` to capture it",
|
||||
codec_kind(selcodec), (unsigned)selcodec, selstr);
|
||||
return true;
|
||||
}
|
||||
#if defined(NRF52_PLATFORM)
|
||||
c.manager.set_accept_full(true); // capture can store a full image; it is not applied
|
||||
const OtaBlCaps& bl = c.bootloaderCaps();
|
||||
if (!bl.present) {
|
||||
strcpy(reply, "ERR bootloader has no mOTA apply support; update it over USB first");
|
||||
return true;
|
||||
}
|
||||
if (bl.apply_abi < MOTA_FORMAT_VER || selcodec >= 16 || !(bl.codec_mask & (1u << selcodec))) {
|
||||
snprintf(reply, 160, "ERR bootloader cannot apply mOTA ABI %u codec %u (has abi=%u codecs=0x%x)",
|
||||
MOTA_FORMAT_VER, (unsigned)selcodec, bl.apply_abi, bl.codec_mask);
|
||||
return true;
|
||||
}
|
||||
#if defined(OTA_SD_STORE)
|
||||
if (!(bl.storage_flags & OTA_BL_STORAGE_SD)) {
|
||||
strcpy(reply, "ERR bootloader cannot apply an update staged on SD; update it over USB first");
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
} else { strcpy(reply, "ERR destination must be `flash` or `folder`"); return true; }
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE)
|
||||
SelfFwInfo self;
|
||||
bool has_endf = ota_self_firmware(self) && self.valid;
|
||||
if (!has_endf && !rescue) {
|
||||
snprintf(reply, 160,
|
||||
"ERR no EndF; retry `ota pull %s flash rescue`, then use `ota rescue install <hash16>`",
|
||||
selstr);
|
||||
return true;
|
||||
}
|
||||
if (!has_endf && seltgt != c.manager.target()) {
|
||||
strcpy(reply, "ERR rescue requires an update for this exact target");
|
||||
return true;
|
||||
}
|
||||
if (has_endf && rescue) {
|
||||
strcpy(reply, "ERR rescue is only for a running firmware with no valid EndF; use `flash`");
|
||||
return true;
|
||||
}
|
||||
#else
|
||||
if (rescue) {
|
||||
strcpy(reply, "ERR rescue is available only on internal-flash nRF52 builds");
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
store = &c.fetch_store; dname = rescue ? "flash+rescue" : "flash";
|
||||
#endif
|
||||
} else if (to_folder) {
|
||||
if (!c.folder_dest) { strcpy(reply, "ERR no folder connected (run motatool serve --tcp/--serial)"); return true; }
|
||||
c.folder_dest->set_mid(selmid); store = c.folder_dest; dname = validate ? "folder+validate" : "folder";
|
||||
}
|
||||
c.manager.reset_session();
|
||||
c.fetch_to_folder = to_folder;
|
||||
c.manager.set_fetch_store(store); // stage this pull to the chosen destination
|
||||
c.manager.pull(selmid, seltgt, validate); // sets want + begins the manifest fetch now
|
||||
OtaManager::PullResult result = to_folder
|
||||
? c.manager.pull_archive(selmid, seltgt, validate)
|
||||
: c.manager.pull(selmid, seltgt, false);
|
||||
char midhx[9]; mesh::Utils::toHex(midhx, selmid, 4);
|
||||
snprintf(reply, 160, "OK pulling mid=%s -> %s (primary traffic)", midhx, dname);
|
||||
if (result != OtaManager::PULL_STARTED && result != OtaManager::PULL_RESUMED) {
|
||||
c.manager.reset_session();
|
||||
c.fetch_to_folder = false;
|
||||
c.manager.set_fetch_store(&c.fetch_store);
|
||||
const char* why = result == OtaManager::PULL_NO_STORE ? "no destination store"
|
||||
: result == OtaManager::PULL_BUSY ? "receive slot busy" : "invalid manifest id";
|
||||
snprintf(reply, 160, "ERR pull did not start: %s", why);
|
||||
return true;
|
||||
}
|
||||
snprintf(reply, 160, "OK %s mid=%s -> %s (primary traffic)",
|
||||
result == OtaManager::PULL_RESUMED ? "resuming" : "pulling", midhx, dname);
|
||||
|
||||
// ---- discard the current session (e.g. a stalled old fetch) to free the slot ----
|
||||
} else if (is_cmd(a, "drop|cancel|stop", &rest)) {
|
||||
@@ -359,6 +487,7 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
c.stopSdCacheFetch();
|
||||
#endif
|
||||
c.manager.reset_session(); c.manager.want(0); c.manager.want_mid(nullptr);
|
||||
c.fetch_to_folder = false;
|
||||
c.manager.set_fetch_store(&c.fetch_store); // revert to the default flash store (a folder pull switched it)
|
||||
#if defined(NRF52_PLATFORM) && !defined(OTA_SD_STORE)
|
||||
c.manager.set_accept_full(false);
|
||||
@@ -405,6 +534,10 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
// the exact 8-byte base hash carried by the already-fetched package. The bootloader independently
|
||||
// hashes the running app and refuses a mismatch before writing any application flash.
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE) && !defined(OTA_SEEDER_ONLY)
|
||||
if (c.fetch_to_folder) {
|
||||
strcpy(reply, "ERR the complete update was captured to a folder, not staged for install; use `ota cancel`");
|
||||
return true;
|
||||
}
|
||||
const char* hash_text = nullptr;
|
||||
uint8_t operator_base_hash[8];
|
||||
if (!is_cmd(rest, "install", &hash_text) ||
|
||||
@@ -439,6 +572,10 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
if (c.fetch_to_folder) {
|
||||
strcpy(reply, "ERR the complete update was captured to a folder, not staged for install; use `ota cancel`");
|
||||
return true;
|
||||
}
|
||||
if (c.manager.fetchState() != OtaManager::COMPLETE || c.fetch_store.staged_size() == 0) {
|
||||
sprintf(reply, "ERR no complete update fetched (fetch=%c %u/%u)",
|
||||
fstate_char(c.manager.fetchState()), (unsigned)c.manager.blocksHave(),
|
||||
@@ -455,7 +592,7 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
// node hosts MANY images (any architecture) it doesn't hold in flash. Trustless (fetchers verify). --
|
||||
} else if (is_cmd(a, "folder|fold", &rest)) {
|
||||
const char* p = rest;
|
||||
if (strncmp(p, "on", 2) == 0) {
|
||||
if (strcmp(p, "on") == 0) {
|
||||
#if defined(OTA_FOLDER_SERIAL)
|
||||
#if !defined(OTA_SEEDER_ONLY)
|
||||
if (!c.serving) c.serving = ota_serve_self(c, 0); // keep serving our own fw alongside the folder
|
||||
@@ -465,22 +602,29 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
#else
|
||||
strcpy(reply, "ERR not built with OTA_FOLDER_SERIAL (set the seeder UART in platformio.ini)");
|
||||
#endif
|
||||
} else if (strncmp(p, "off", 3) == 0) {
|
||||
} else if (strcmp(p, "off") == 0) {
|
||||
c.detach_folder(); c.manager.announce();
|
||||
#if defined(OTA_SEEDER_ONLY)
|
||||
strcpy(reply, "OK folder detached (serving nothing)");
|
||||
#else
|
||||
strcpy(reply, "OK folder detached (still serving own fw)");
|
||||
#endif
|
||||
} else { // status + list served entries (* = our own fw)
|
||||
int n = snprintf(reply, 159, "folder=%s serving=%u:", c.folder_active ? "on" : "off",
|
||||
(unsigned)c.manager.servedCount());
|
||||
} else if (*p == 0) { // status + list served entries (* = our own fw)
|
||||
uint16_t offered = 0, advertised = 0;
|
||||
bool have_stats = c.folderSourceStats(offered, advertised);
|
||||
int n = have_stats
|
||||
? snprintf(reply, 159, "folder=%s host=%u/%u serving=%u:", c.folder_active ? "on" : "off",
|
||||
(unsigned)advertised, (unsigned)offered, (unsigned)c.manager.servedCount())
|
||||
: snprintf(reply, 159, "folder=%s serving=%u:", c.folder_active ? "on" : "off",
|
||||
(unsigned)c.manager.servedCount());
|
||||
for (uint8_t i = 0; i < c.manager.servedCount() && n < 148; i++) {
|
||||
const OtaManager::ServeEntry* e = c.manager.servedEntry(i);
|
||||
if (!e) break;
|
||||
char midhx[9]; mesh::Utils::toHex(midhx, e->mid, 4);
|
||||
n += snprintf(reply + n, 159 - n, " %s%s/%08X", e->is_self ? "*" : "", midhx, (unsigned)e->target_id);
|
||||
}
|
||||
} else {
|
||||
strcpy(reply, "ERR usage: ota folder [on|off]");
|
||||
}
|
||||
|
||||
// ---- SD OTA archive: default-on capture of every advertised mOTA, retained and served after reboot. ----
|
||||
@@ -674,6 +818,10 @@ static bool handle_dev(const char* d, char* reply, OtaContext& c) {
|
||||
strcpy(reply, "OK announced");
|
||||
|
||||
} else if (strncmp(d, "verify", 6) == 0) {
|
||||
if (c.fetch_to_folder && c.manager.fetchState() == OtaManager::COMPLETE) {
|
||||
strcpy(reply, "ERR completed fetch is in the host folder, not local verification storage");
|
||||
return true;
|
||||
}
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
if (c.manager.fetchState() == OtaManager::COMPLETE) {
|
||||
VerifyResult r = ota_verify(static_cast<const OtaStore&>(c.fetch_store), c.allow);
|
||||
@@ -729,7 +877,7 @@ static bool handle_dev(const char* d, char* reply, OtaContext& c) {
|
||||
#endif
|
||||
c.manager.clear_primary();
|
||||
c.serve_expected = 0; c.serving = false; c.releaseServeBuffer();
|
||||
c.fetch_store.clear(); c.manager.reset_session();
|
||||
c.fetch_store.clear(); c.manager.reset_session(); c.fetch_to_folder = false;
|
||||
strcpy(reply, "OK cleared");
|
||||
|
||||
} else {
|
||||
|
||||
@@ -114,12 +114,13 @@ struct OtaContext {
|
||||
bool config_dirty = false; // CLI set a policy/key -> CommonCLI persists + clears
|
||||
char hw_id[33] = {0}; // this device's hardware tag (from board.getOtaHwId(), set in begin)
|
||||
|
||||
// ---- Pull destinations (`ota pull <#> <dest>`): where a fetched .mota is staged. `flash` (fetch_store,
|
||||
// ---- Pull destinations (`ota pull <id> <dest>`): where a fetched .mota is staged. `flash` (fetch_store,
|
||||
// always present) or `folder` - an external host folder over the seeder link, registered by the app
|
||||
// while a motatool `serve` connection is attached (else no `folder` dest is offered). Captures the
|
||||
// container to the host as <mid>.mota so an exact firmware copy can be pulled for delta-building. ----
|
||||
FolderMotaStore* folder_dest = nullptr; // non-null == a folder destination is currently connected
|
||||
char folder_dest_info[24] = {0}; // human id of the link (e.g. "tcp 192.168.4.5", "serial")
|
||||
bool fetch_to_folder = false; // COMPLETE belongs to a capture destination, never install it
|
||||
void set_folder_dest(FolderMotaStore* fs, const char* info) {
|
||||
folder_dest = fs;
|
||||
strncpy(folder_dest_info, info ? info : "?", sizeof(folder_dest_info) - 1);
|
||||
@@ -150,6 +151,11 @@ struct OtaContext {
|
||||
}
|
||||
|
||||
bool apply_fetched_impl(const uint8_t* rescue_base_hash, char* msg) {
|
||||
if (fetch_to_folder) {
|
||||
strncpy(msg, "refused: completed fetch was captured to a folder, not local install storage", 96);
|
||||
msg[95] = 0;
|
||||
return false;
|
||||
}
|
||||
#if defined(OTA_SEEDER_ONLY)
|
||||
strncpy(msg, "refused: this build serves mOTA images but cannot install one", 96);
|
||||
msg[95] = 0;
|
||||
@@ -234,6 +240,9 @@ struct OtaContext {
|
||||
FOLDER_LINK_TCP,
|
||||
};
|
||||
FolderLink folderLink() const { return _folder_link; }
|
||||
bool folderSourceStats(uint16_t& offered, uint16_t& advertised) const {
|
||||
return manager.sourceStats(_folder_source, offered, advertised);
|
||||
}
|
||||
|
||||
bool attach_folder_source(MotaSource* source, FolderLink link, const char* label,
|
||||
char* msg, size_t cap) {
|
||||
@@ -250,8 +259,11 @@ struct OtaContext {
|
||||
}
|
||||
if (folder_active && _folder_source == source) {
|
||||
manager.refresh_sources();
|
||||
snprintf(msg, cap, "OK folder refreshed (%s) - serving %u mOTA total",
|
||||
label ? label : "external", (unsigned)manager.servedCount());
|
||||
uint16_t offered = 0, advertised = 0;
|
||||
manager.sourceStats(source, offered, advertised);
|
||||
snprintf(msg, cap, "OK folder refreshed (%s): advertising %u/%u host mOTAs%s",
|
||||
label ? label : "external", (unsigned)advertised, (unsigned)offered,
|
||||
advertised < offered ? " (some omitted: serve cap/invalid/duplicate)" : "");
|
||||
return true;
|
||||
}
|
||||
if (folder_active && _folder_source) {
|
||||
@@ -268,13 +280,11 @@ struct OtaContext {
|
||||
folder_active = true;
|
||||
_folder_link = link;
|
||||
_folder_source = source;
|
||||
#if defined(OTA_SEEDER_ONLY)
|
||||
snprintf(msg, cap, "OK folder attached (%s) - serving %u host mOTA total",
|
||||
label ? label : "external", (unsigned)manager.servedCount());
|
||||
#else
|
||||
snprintf(msg, cap, "OK folder attached (%s) - serving %u mOTA total (own fw + folder)",
|
||||
label ? label : "external", (unsigned)manager.servedCount());
|
||||
#endif
|
||||
uint16_t offered = 0, advertised = 0;
|
||||
manager.sourceStats(source, offered, advertised);
|
||||
snprintf(msg, cap, "OK folder attached (%s): advertising %u/%u host mOTAs%s",
|
||||
label ? label : "external", (unsigned)advertised, (unsigned)offered,
|
||||
advertised < offered ? " (some omitted: serve cap/invalid/duplicate)" : "");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -333,6 +343,7 @@ struct OtaContext {
|
||||
manager.want(0);
|
||||
manager.want_mid(nullptr);
|
||||
manager.set_fetch_store(&fetch_store);
|
||||
fetch_to_folder = false;
|
||||
_sd_cache_fetching = false;
|
||||
}
|
||||
|
||||
@@ -398,6 +409,7 @@ struct OtaContext {
|
||||
memcpy(_sd_cache_mid, row->mid, 4);
|
||||
sd_cache.set_mid(row->mid);
|
||||
manager.set_fetch_store(&sd_cache);
|
||||
fetch_to_folder = false;
|
||||
_sd_cache_fetching = true;
|
||||
manager.pull_archive(row->mid, row->target_id);
|
||||
return;
|
||||
@@ -427,6 +439,7 @@ struct OtaContext {
|
||||
target_id = 0;
|
||||
#endif
|
||||
manager.begin(target_id, send, ctx);
|
||||
fetch_to_folder = false;
|
||||
if (hw) { strncpy(hw_id, hw, sizeof(hw_id) - 1); hw_id[sizeof(hw_id) - 1] = 0; }
|
||||
// a node only fetches firmware it can apply: ESP32 A/B -> sequential, nRF52 single-slot -> in-place
|
||||
#if defined(OTA_SEEDER_ONLY)
|
||||
|
||||
+228
-50
@@ -44,9 +44,14 @@ uint8_t* OtaManager::ensureScratch() {
|
||||
void OtaManager::begin(uint32_t my_target_id, OtaSend send, void* ctx) {
|
||||
_target = my_target_id; _send = send; _ctx = ctx;
|
||||
_fstate = IDLE; _have = 0; _fbc = 0;
|
||||
_fetch_error = FETCH_ERROR_NONE;
|
||||
_archive_fetch = false; _validate = false;
|
||||
clearFetchIntent();
|
||||
_resume_verify_idx = 0; _resume_invalidated = false;
|
||||
_resume_merkle.reset();
|
||||
_n_serve = 0; _n_src_obj = 0; _view0.valid = false; _srcv.valid = false;
|
||||
memset(_src_offered, 0, sizeof(_src_offered));
|
||||
memset(_src_advertised, 0, sizeof(_src_advertised));
|
||||
_n_src = 0; _n_cat = 0;
|
||||
clearPendingEgress();
|
||||
}
|
||||
@@ -67,7 +72,7 @@ bool OtaManager::serve(const uint8_t* mota, uint32_t len) {
|
||||
_view0.scratch = scratch; _view0.scratch_sz = OTA_PROOFGEN_SCRATCH; // <=1024 blocks (RAM .mota is small)
|
||||
_view0.valid = true;
|
||||
clearPendingEgress();
|
||||
registerSelfEntry();
|
||||
if (_n_src_obj) refresh_sources(); else registerSelfEntry();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -84,7 +89,7 @@ bool OtaManager::serve_self(const uint8_t* manifest, uint16_t mfl, const uint8_t
|
||||
_view0.scratch = proof_scratch; _view0.scratch_sz = proof_scratch_sz; // sized for our (large) image
|
||||
_view0.valid = true;
|
||||
clearPendingEgress();
|
||||
registerSelfEntry();
|
||||
if (_n_src_obj) refresh_sources(); else registerSelfEntry();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -112,7 +117,10 @@ bool OtaManager::add_source(MotaSource* src) {
|
||||
if (_src_list[i] == src) { refresh_sources(); return true; }
|
||||
}
|
||||
if (_n_src_obj >= OTA_MAX_SOURCE_OBJ) return false;
|
||||
_src_list[_n_src_obj++] = src;
|
||||
_src_list[_n_src_obj] = src;
|
||||
_src_offered[_n_src_obj] = 0;
|
||||
_src_advertised[_n_src_obj] = 0;
|
||||
_n_src_obj++;
|
||||
refresh_sources();
|
||||
return true;
|
||||
}
|
||||
@@ -121,8 +129,15 @@ bool OtaManager::remove_source(MotaSource* src) {
|
||||
if (!src) return false;
|
||||
for (uint8_t i = 0; i < _n_src_obj; i++) {
|
||||
if (_src_list[i] != src) continue;
|
||||
for (uint8_t j = i + 1; j < _n_src_obj; j++) _src_list[j - 1] = _src_list[j];
|
||||
_src_list[--_n_src_obj] = nullptr;
|
||||
for (uint8_t j = i + 1; j < _n_src_obj; j++) {
|
||||
_src_list[j - 1] = _src_list[j];
|
||||
_src_offered[j - 1] = _src_offered[j];
|
||||
_src_advertised[j - 1] = _src_advertised[j];
|
||||
}
|
||||
_n_src_obj--;
|
||||
_src_list[_n_src_obj] = nullptr;
|
||||
_src_offered[_n_src_obj] = 0;
|
||||
_src_advertised[_n_src_obj] = 0;
|
||||
refresh_sources();
|
||||
return true;
|
||||
}
|
||||
@@ -134,10 +149,14 @@ void OtaManager::refresh_sources() {
|
||||
_n_serve = 0;
|
||||
if (_view0.valid) registerSelfEntry();
|
||||
for (uint8_t s = 0; s < _n_src_obj; s++) {
|
||||
_src_offered[s] = 0;
|
||||
_src_advertised[s] = 0;
|
||||
MotaSource* src = _src_list[s];
|
||||
if (!src) continue;
|
||||
uint8_t cnt = src->count();
|
||||
for (uint8_t i = 0; i < cnt && _n_serve < OTA_MAX_SERVE; i++) {
|
||||
_src_offered[s] = cnt;
|
||||
for (uint8_t i = 0; i < cnt; i++) {
|
||||
if (_n_serve >= OTA_MAX_SERVE) break;
|
||||
MotaDesc d;
|
||||
if (!src->describe(i, d)) continue;
|
||||
if (d.block_count == 0 || (uint64_t)d.block_count * 4 > OTA_PROOFGEN_SCRATCH) continue;
|
||||
@@ -161,14 +180,31 @@ void OtaManager::refresh_sources() {
|
||||
e.target_id = d.target_id; e.fw_version = d.fw_version;
|
||||
e.codec_id = d.codec_id; e.flags = d.flags; e.have_count = d.block_count; // a folder mota is fully held
|
||||
e.is_self = false; e.src = src; e.src_idx = i; e.desc = d;
|
||||
_src_advertised[s]++;
|
||||
}
|
||||
}
|
||||
_srcv.valid = false; // a loaded source view may now be stale; reloads on demand
|
||||
}
|
||||
|
||||
bool OtaManager::sourceStats(const MotaSource* src, uint16_t& offered, uint16_t& advertised) const {
|
||||
offered = 0;
|
||||
advertised = 0;
|
||||
if (!src) return false;
|
||||
for (uint8_t i = 0; i < _n_src_obj; i++) {
|
||||
if (_src_list[i] != src) continue;
|
||||
offered = _src_offered[i];
|
||||
advertised = _src_advertised[i];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void OtaManager::clear_sources() {
|
||||
clearPendingEgress();
|
||||
_n_src_obj = 0; _srcv.valid = false;
|
||||
memset(_src_list, 0, sizeof(_src_list));
|
||||
memset(_src_offered, 0, sizeof(_src_offered));
|
||||
memset(_src_advertised, 0, sizeof(_src_advertised));
|
||||
_n_serve = 0;
|
||||
if (_view0.valid) registerSelfEntry();
|
||||
}
|
||||
@@ -176,12 +212,8 @@ void OtaManager::clear_sources() {
|
||||
void OtaManager::clear_primary() {
|
||||
clearPendingEgress();
|
||||
_view0.valid = false;
|
||||
if (_n_serve > 0 && _serve[0].is_self) {
|
||||
for (uint8_t i = 1; i < _n_serve; i++) {
|
||||
_serve[i - 1] = _serve[i];
|
||||
}
|
||||
_n_serve--;
|
||||
}
|
||||
if (_n_src_obj) refresh_sources();
|
||||
else _n_serve = 0;
|
||||
}
|
||||
|
||||
int OtaManager::serveEntryIndex(const uint8_t* mid) const {
|
||||
@@ -531,6 +563,40 @@ void OtaManager::serviceEgress() {
|
||||
|
||||
// ---------------- fetch ----------------
|
||||
|
||||
// A source's set digest is the lifetime of its catalog rows. When that digest changes (or the source table
|
||||
// evicts the source), remove only that seeder's association from every row and recompute aggregate progress.
|
||||
// Rows with no remaining source disappear, so numeric/MID selection cannot target firmware nobody offers.
|
||||
void OtaManager::invalidateCatalogSeeder(const uint8_t* seeder) {
|
||||
if (!seeder) return;
|
||||
CatRow* catalog = catalogData();
|
||||
for (uint16_t i = 0; i < _n_cat; ) {
|
||||
CatRow& row = catalog[i];
|
||||
int found = -1;
|
||||
for (uint8_t k = 0; k < row.n_seeders; k++) {
|
||||
if (memcmp(row.seeders[k], seeder, 4) == 0) { found = k; break; }
|
||||
}
|
||||
if (found < 0) { i++; continue; }
|
||||
for (uint8_t k = (uint8_t)found + 1; k < row.n_seeders; k++) {
|
||||
memcpy(row.seeders[k - 1], row.seeders[k], 4);
|
||||
row.seeder_have[k - 1] = row.seeder_have[k];
|
||||
row.seeder_last_ms[k - 1] = row.seeder_last_ms[k];
|
||||
}
|
||||
row.n_seeders--;
|
||||
if (row.n_seeders == 0) {
|
||||
for (uint16_t j = i + 1; j < _n_cat; j++) catalog[j - 1] = catalog[j];
|
||||
_n_cat--;
|
||||
continue;
|
||||
}
|
||||
row.have_max = 0;
|
||||
row.last_ms = 0;
|
||||
for (uint8_t k = 0; k < row.n_seeders; k++) {
|
||||
if (row.seeder_have[k] > row.have_max) row.have_max = row.seeder_have[k];
|
||||
if (row.seeder_last_ms[k] > row.last_ms) row.last_ms = row.seeder_last_ms[k];
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// A tiny per-node BEACON: record the source; ask it for its catalog (OTA_QUERY) only when we're
|
||||
// interested AND its set-digest is one we haven't catalogued yet (so a stable mesh is query-free).
|
||||
void OtaManager::handleAdv(const uint8_t* m, uint16_t n) {
|
||||
@@ -538,7 +604,16 @@ void OtaManager::handleAdv(const uint8_t* m, uint16_t n) {
|
||||
if (!decode_adv(m, n, a)) return;
|
||||
bool have_sid = (_seeder_id[0] | _seeder_id[1] | _seeder_id[2] | _seeder_id[3]) != 0;
|
||||
if (have_sid && memcmp(a.seeder_id, _seeder_id, 4) == 0) return; // our own beacon, re-flooded
|
||||
if (a.n_motas == 0) return; // source offers nothing
|
||||
if (a.n_motas == 0) { // source explicitly withdrew its set
|
||||
invalidateCatalogSeeder(a.seeder_id);
|
||||
for (uint8_t i = 0; i < _n_src; i++) {
|
||||
if (memcmp(_sources[i].seeder, a.seeder_id, 4) != 0) continue;
|
||||
for (uint8_t j = i + 1; j < _n_src; j++) _sources[j - 1] = _sources[j];
|
||||
_n_src--;
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
int slot = -1, lru = 0; // find/insert the source (LRU evict)
|
||||
for (int i = 0; i < _n_src; i++) {
|
||||
@@ -546,9 +621,20 @@ void OtaManager::handleAdv(const uint8_t* m, uint16_t n) {
|
||||
if (_sources[i].last_ms < _sources[lru].last_ms) lru = i;
|
||||
}
|
||||
bool fresh = (slot < 0);
|
||||
if (fresh) { slot = (_n_src < OTA_MAX_SOURCES) ? _n_src++ : lru; _sources[slot] = Source{}; }
|
||||
if (fresh) {
|
||||
// Passive HAVE traffic can leave rows for a source before its beacon is retained. Start its advertised
|
||||
// digest with a clean association, and purge the evicted source when the fixed source table is full.
|
||||
invalidateCatalogSeeder(a.seeder_id);
|
||||
if (_n_src < OTA_MAX_SOURCES) slot = _n_src++;
|
||||
else {
|
||||
invalidateCatalogSeeder(_sources[lru].seeder);
|
||||
slot = lru;
|
||||
}
|
||||
_sources[slot] = Source{};
|
||||
}
|
||||
Source& s = _sources[slot];
|
||||
bool changed = fresh || memcmp(s.digest, a.set_digest, 4) != 0;
|
||||
if (changed && !fresh) invalidateCatalogSeeder(s.seeder);
|
||||
memcpy(s.seeder, a.seeder_id, 4); memcpy(s.digest, a.set_digest, 4);
|
||||
s.n_motas = a.n_motas; s.last_ms = _now_ms;
|
||||
if (changed) {
|
||||
@@ -627,6 +713,13 @@ void OtaManager::handleHave(const uint8_t* m, uint16_t n) {
|
||||
if (hv.frag_total == 0 || hv.frag_total > OTA_HAVE_MAX_FRAGMENTS || hv.frag_idx >= hv.frag_total) return;
|
||||
bool have_sid = (_seeder_id[0] | _seeder_id[1] | _seeder_id[2] | _seeder_id[3]) != 0;
|
||||
if (have_sid && memcmp(hv.seeder_id, _seeder_id, 4) == 0) return; // our own catalog
|
||||
// Once a newer beacon changed this source's digest, delayed HAVE fragments from its old set must not
|
||||
// resurrect rows we just invalidated. Unknown sources remain cacheable because HAVE is broadcast/passive.
|
||||
for (uint8_t i = 0; i < _n_src; i++) {
|
||||
if (memcmp(_sources[i].seeder, hv.seeder_id, 4) != 0) continue;
|
||||
if (memcmp(_sources[i].digest, hv.set_digest, 4) != 0) return;
|
||||
break;
|
||||
}
|
||||
// PASSIVE: every node caches rows it overhears. A source is catalogued only after EVERY advertised
|
||||
// fragment arrived; otherwise a timed recovery QUERY asks for just the missing bitmap.
|
||||
for (uint8_t i = 0; i < _n_src; i++) {
|
||||
@@ -667,17 +760,30 @@ void OtaManager::handleHave(const uint8_t* m, uint16_t n) {
|
||||
catalog[slot] = CatRow{};
|
||||
memcpy(catalog[slot].mid, mid, 4);
|
||||
memcpy(catalog[slot].seeders[0], hv.seeder_id, 4);
|
||||
catalog[slot].seeder_have[0] = (uint16_t)have_count;
|
||||
catalog[slot].seeder_last_ms[0] = _now_ms;
|
||||
catalog[slot].n_seeders = 1;
|
||||
} else {
|
||||
CatRow& cc = catalog[slot]; // count DISTINCT sources (no double-count)
|
||||
bool known = false;
|
||||
int known = -1;
|
||||
for (uint8_t k = 0; k < cc.n_seeders; k++)
|
||||
if (memcmp(cc.seeders[k], hv.seeder_id, 4) == 0) { known = true; break; }
|
||||
if (!known && cc.n_seeders < OTA_CAT_SEEDERS) memcpy(cc.seeders[cc.n_seeders++], hv.seeder_id, 4);
|
||||
if (memcmp(cc.seeders[k], hv.seeder_id, 4) == 0) { known = k; break; }
|
||||
if (known < 0 && cc.n_seeders < OTA_CAT_SEEDERS) {
|
||||
known = cc.n_seeders++;
|
||||
memcpy(cc.seeders[known], hv.seeder_id, 4);
|
||||
}
|
||||
if (known >= 0) {
|
||||
cc.seeder_have[known] = (uint16_t)have_count;
|
||||
cc.seeder_last_ms[known] = _now_ms;
|
||||
}
|
||||
}
|
||||
CatRow& c = catalog[slot];
|
||||
c.target_id = target; c.fw_version = fwver; c.codec = codec; c.flags = flags; c.last_ms = _now_ms;
|
||||
if (have_count > c.have_max) c.have_max = have_count; // best-known progress among sources
|
||||
c.target_id = target; c.fw_version = fwver; c.codec = codec; c.flags = flags;
|
||||
c.have_max = 0; c.last_ms = 0;
|
||||
for (uint8_t k = 0; k < c.n_seeders; k++) {
|
||||
if (c.seeder_have[k] > c.have_max) c.have_max = c.seeder_have[k];
|
||||
if (c.seeder_last_ms[k] > c.last_ms) c.last_ms = c.seeder_last_ms[k];
|
||||
}
|
||||
if (wantRow(mid, target, codec, flags)) startFetch(mid, target);
|
||||
}
|
||||
}
|
||||
@@ -833,17 +939,79 @@ bool OtaManager::blockInPipeline(uint32_t block) const {
|
||||
return findReassemblySlot(block) >= 0;
|
||||
}
|
||||
|
||||
bool OtaManager::fetchActive() const {
|
||||
return _fstate == FETCHING || _fstate == WANT_MANIFEST || _fstate == WANT_LEAVES
|
||||
|| _fstate == VERIFYING_STAGED || _fstate == PAUSED;
|
||||
}
|
||||
|
||||
void OtaManager::clearFetchIntent() {
|
||||
_desired_target = 0;
|
||||
_have_desired_mid = false;
|
||||
memset(_desired_mid, 0, sizeof(_desired_mid));
|
||||
}
|
||||
|
||||
void OtaManager::failFetch(FetchError error) {
|
||||
_fetch_error = error;
|
||||
_fstate = FAILED;
|
||||
clearReassembly();
|
||||
freeLeaves();
|
||||
_validate = false;
|
||||
_archive_fetch = false;
|
||||
clearFetchIntent();
|
||||
}
|
||||
|
||||
void OtaManager::completeFetch() {
|
||||
_fetch_error = FETCH_ERROR_NONE;
|
||||
_fstate = COMPLETE;
|
||||
_validate = false;
|
||||
_archive_fetch = false;
|
||||
clearFetchIntent();
|
||||
}
|
||||
|
||||
OtaManager::PullResult OtaManager::pull(const uint8_t* mid, uint32_t target, bool validate) {
|
||||
if (!mid) return PULL_BAD_MID;
|
||||
if (!_fetch) return PULL_NO_STORE;
|
||||
if (fetchActive()) return PULL_BUSY;
|
||||
_archive_fetch = false;
|
||||
_desired_target = target;
|
||||
memcpy(_desired_mid, mid, sizeof(_desired_mid));
|
||||
_have_desired_mid = true;
|
||||
reDiscover();
|
||||
PullResult result = startFetch(mid, target, validate);
|
||||
if (result != PULL_STARTED && result != PULL_RESUMED) clearFetchIntent();
|
||||
return result;
|
||||
}
|
||||
|
||||
OtaManager::PullResult OtaManager::pull_archive(const uint8_t* mid, uint32_t target, bool validate) {
|
||||
if (!mid) return PULL_BAD_MID;
|
||||
if (!_fetch) return PULL_NO_STORE;
|
||||
if (fetchActive()) return PULL_BUSY;
|
||||
_archive_fetch = true;
|
||||
_desired_target = target;
|
||||
memcpy(_desired_mid, mid, sizeof(_desired_mid));
|
||||
_have_desired_mid = true;
|
||||
reDiscover();
|
||||
PullResult result = startFetch(mid, target, validate);
|
||||
if (result != PULL_STARTED && result != PULL_RESUMED) {
|
||||
_archive_fetch = false;
|
||||
clearFetchIntent();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Begin (or resume) fetching a chosen mid: try a staged-partial resume first, else request the manifest.
|
||||
void OtaManager::startFetch(const uint8_t* mid, uint32_t target, bool validate) {
|
||||
OtaManager::PullResult OtaManager::startFetch(const uint8_t* mid, uint32_t target, bool validate) {
|
||||
(void)target;
|
||||
if (!_fetch || _fstate == FETCHING || _fstate == WANT_MANIFEST
|
||||
|| _fstate == WANT_LEAVES || _fstate == VERIFYING_STAGED
|
||||
|| _fstate == PAUSED) return;
|
||||
if (!mid) return PULL_BAD_MID;
|
||||
if (!_fetch) return PULL_NO_STORE;
|
||||
if (fetchActive()) return PULL_BUSY;
|
||||
_fetch_error = FETCH_ERROR_NONE;
|
||||
_validate = validate; // motatool folder-capture warm-start (seed leaf-diff)
|
||||
// A validate pull is a FRESH seed capture, not a resume: the store already holds the seed's payload (not a
|
||||
// real partial), so never adopt it via resumeStaged - always re-begin and run the manifest->leaves->diff.
|
||||
if (!validate && resumeStaged(mid)) return; // (non-validate) resume a partial container left in flash
|
||||
if (!validate && resumeStaged(mid)) return PULL_RESUMED; // adopt a partial container left in the store
|
||||
memcpy(_fid, mid, 4);
|
||||
_have = 0; _fbc = 0; _ftotal = 0; _fflags = 0;
|
||||
_observed_path_transmissions = 0; // learn the actual source/relay path from this fetch's replies
|
||||
_fstate = WANT_MANIFEST;
|
||||
_mf_total = 0; _mf_mask = 0; _mf_len = 0; _mf_retries = 0; _loop_last_mfmask = 0; // fresh manifest reassembly
|
||||
@@ -851,6 +1019,7 @@ void OtaManager::startFetch(const uint8_t* mid, uint32_t target, bool validate)
|
||||
GetManifestMsg gm; memcpy(gm.manifest_id, _fid, 4); gm.want_mask = 0xFFFF; // first ask: send all fragments
|
||||
uint8_t b[16];
|
||||
emit(b, encode_get_manifest(b, sizeof(b), gm), false);
|
||||
return PULL_STARTED;
|
||||
}
|
||||
|
||||
void OtaManager::handleManifest(const uint8_t* m, uint16_t n) {
|
||||
@@ -882,42 +1051,44 @@ void OtaManager::handleManifest(const uint8_t* m, uint16_t n) {
|
||||
|
||||
const uint8_t* mf = _mf_buf; // fully reassembled manifest-minus-leaves
|
||||
uint32_t mfl = _mf_len;
|
||||
if (mfl != MOTA_MFL) { _fstate = FAILED; return; } // manifest-minus-leaves is a fixed 197 bytes
|
||||
if (mf[2] != HASH_ALGO_SHA256) { _fstate = FAILED; return; } // this implementation supports sha2-256 only
|
||||
if (!_archive_fetch && !codecOk(mf[56])) { _fstate = IDLE; return; } // incompatible install codec
|
||||
if (mfl != MOTA_MFL) { failFetch(FETCH_ERROR_MANIFEST); return; } // fixed 197-byte manifest
|
||||
if (mf[2] != HASH_ALGO_SHA256) { failFetch(FETCH_ERROR_HASH_ALGO); return; }
|
||||
if (!_archive_fetch && !codecOk(mf[56])) { failFetch(FETCH_ERROR_CODEC); return; }
|
||||
uint32_t payload_size = rd_u32le(mf + 15);
|
||||
uint8_t bsl = mf[19];
|
||||
if (bsl >= 32) { _fstate = FAILED; return; }
|
||||
if (bsl >= 32) { failFetch(FETCH_ERROR_GEOMETRY); return; }
|
||||
uint32_t bs = 1u << bsl;
|
||||
// a block must fit our reassembly buffer (and be non-empty) - reject an oversized block_size up front
|
||||
if (bs == 0 || bs > OTA_MAX_BLOCK || payload_size == 0) { _fstate = FAILED; return; }
|
||||
if (bs == 0 || bs > OTA_MAX_BLOCK || payload_size == 0) { failFetch(FETCH_ERROR_GEOMETRY); return; }
|
||||
uint32_t bc = (payload_size + bs - 1) / bs;
|
||||
if (bc > 0xFFFFu) { _fstate = FAILED; return; } // block_idx is uint16 on the wire - can't address more
|
||||
if (bc > 0xFFFFu) { failFetch(FETCH_ERROR_TOO_LARGE); return; } // uint16 block index on the wire
|
||||
if (_archive_fetch && (uint64_t)bc * 4 > OTA_PROOFGEN_SCRATCH) {
|
||||
_fstate = FAILED; return; // retaining an image we cannot subsequently seed is useless
|
||||
failFetch(FETCH_ERROR_TOO_LARGE); return; // retaining an image we cannot seed is useless
|
||||
}
|
||||
memcpy(_froot, mf + 20, 4);
|
||||
|
||||
uint32_t leaves_off = 8 + mfl;
|
||||
uint32_t payload_off = leaves_off + bc * 4;
|
||||
uint64_t total64 = (uint64_t)payload_off + payload_size + 5;
|
||||
if (total64 > UINT32_MAX) { _fstate = FAILED; return; }
|
||||
if (total64 > UINT32_MAX) { failFetch(FETCH_ERROR_TOO_LARGE); return; }
|
||||
uint32_t total = (uint32_t)total64;
|
||||
|
||||
// Hand the store the parsed layout BEFORE begin(), so a partition-backed store (ESP32) can choose
|
||||
// placement and refuse an unfittable fetch up front: a FULL payload streams to the inactive slot,
|
||||
// a delta's whole container is staged together. (image_size at mf+11, is_full from flags at mf+1.)
|
||||
bool is_full = (mf[1] & MFLAG_FULL) != 0;
|
||||
if (!_fetch->plan_layout(is_full, rd_u32le(mf + 11), payload_off, payload_size)) { _fstate = FAILED; return; }
|
||||
if (!_fetch->begin(total)) { _fstate = FAILED; return; }
|
||||
if (!_fetch->plan_layout(is_full, rd_u32le(mf + 11), payload_off, payload_size)) {
|
||||
failFetch(FETCH_ERROR_STORAGE); return;
|
||||
}
|
||||
if (!_fetch->begin(total)) { failFetch(FETCH_ERROR_STORAGE); return; }
|
||||
// declare the metadata extent so a flash store can pin it (leaves are written all transfer long)
|
||||
if (!_fetch->set_meta_size(payload_off)) { _fstate = FAILED; return; }
|
||||
if (!_fetch->set_meta_size(payload_off)) { failFetch(FETCH_ERROR_STORAGE); return; }
|
||||
uint8_t hdr[8];
|
||||
memcpy(hdr, MOTA_MAGIC, 4);
|
||||
wr_u32le(hdr + 4, total);
|
||||
if (!_fetch->write(0, hdr, 8) ||
|
||||
!_fetch->write(8, mf, mfl) ||
|
||||
!_fetch->write(total - 5, MOTA_TRAILER, 5)) { _fstate = FAILED; return; }
|
||||
!_fetch->write(total - 5, MOTA_TRAILER, 5)) { failFetch(FETCH_ERROR_STORAGE); return; }
|
||||
|
||||
_fflags = mf[1]; // manifest flags (FULL/SIGNED) of the fetch in progress (auto-install gate)
|
||||
_fpoff = payload_off; _floff = leaves_off; _fpsize = payload_size; _fbc = bc; _fbs = bs;
|
||||
@@ -1004,8 +1175,9 @@ void OtaManager::diffStep() {
|
||||
OTA_DBG("OTA: leaf-diff %u/%u already valid; fetching the rest\n", (unsigned)_have, (unsigned)_fbc);
|
||||
freeLeaves(); // clears _diffing + frees the buffer
|
||||
if (_have >= _fbc) { // seed covered the whole image
|
||||
_fstate = storedLeavesRootMatches() && _fetch->finalize()
|
||||
? COMPLETE : FAILED;
|
||||
if (!storedLeavesRootMatches()) failFetch(FETCH_ERROR_INTEGRITY);
|
||||
else if (!_fetch->finalize()) failFetch(FETCH_ERROR_STORAGE);
|
||||
else completeFetch();
|
||||
return;
|
||||
}
|
||||
_fstate = FETCHING; requestMissing();
|
||||
@@ -1045,6 +1217,7 @@ bool OtaManager::resumeStaged(const uint8_t* want_mid) {
|
||||
_ftotal = total;
|
||||
clearReassembly();
|
||||
_observed_path_transmissions = 0;
|
||||
_fetch_error = FETCH_ERROR_NONE;
|
||||
beginStagedVerification();
|
||||
OTA_DBG("OTA: RESUME verifying %u blocks total=%u\n",
|
||||
(unsigned)bc, (unsigned)total);
|
||||
@@ -1097,7 +1270,7 @@ void OtaManager::verifyStagedStep() {
|
||||
const uint32_t index = _resume_verify_idx;
|
||||
if (!_fetch->read(_floff + index * 4,
|
||||
stored_leaf, sizeof(stored_leaf))) {
|
||||
_fstate = FAILED;
|
||||
failFetch(FETCH_ERROR_STORAGE);
|
||||
return;
|
||||
}
|
||||
if (memcmp(stored_leaf, missing_leaf, sizeof(stored_leaf)) == 0) {
|
||||
@@ -1106,7 +1279,7 @@ void OtaManager::verifyStagedStep() {
|
||||
|
||||
const uint32_t length = blockLen(index);
|
||||
if (!_fetch->read(_fpoff + index * _fbs, _reasm[0].buf, length)) {
|
||||
_fstate = FAILED;
|
||||
failFetch(FETCH_ERROR_STORAGE);
|
||||
return;
|
||||
}
|
||||
uint8_t computed_leaf[4];
|
||||
@@ -1116,14 +1289,14 @@ void OtaManager::verifyStagedStep() {
|
||||
// requests this block again; a stale marker must never bless bad bytes.
|
||||
if (!_fetch->write(_floff + index * 4,
|
||||
missing_leaf, sizeof(missing_leaf))) {
|
||||
_fstate = FAILED;
|
||||
failFetch(FETCH_ERROR_STORAGE);
|
||||
return;
|
||||
}
|
||||
_resume_invalidated = true;
|
||||
continue;
|
||||
}
|
||||
if (!_resume_merkle.add(computed_leaf)) {
|
||||
_fstate = FAILED;
|
||||
failFetch(FETCH_ERROR_INTEGRITY);
|
||||
return;
|
||||
}
|
||||
_have++;
|
||||
@@ -1135,10 +1308,13 @@ void OtaManager::verifyStagedStep() {
|
||||
|
||||
if (_have == _fbc) {
|
||||
uint8_t root[4];
|
||||
_fstate = _resume_merkle.finish(root)
|
||||
&& memcmp(root, _froot, sizeof(root)) == 0
|
||||
&& _fetch->finalize()
|
||||
? COMPLETE : FAILED;
|
||||
if (!_resume_merkle.finish(root) || memcmp(root, _froot, sizeof(root)) != 0) {
|
||||
failFetch(FETCH_ERROR_INTEGRITY);
|
||||
} else if (!_fetch->finalize()) {
|
||||
failFetch(FETCH_ERROR_STORAGE);
|
||||
} else {
|
||||
completeFetch();
|
||||
}
|
||||
} else {
|
||||
_fstate = FETCHING;
|
||||
requestMissing();
|
||||
@@ -1201,6 +1377,7 @@ bool OtaManager::handleProof(const uint8_t* m, uint16_t n) {
|
||||
uint8_t leaf[4]; merkle_leaf(leaf, slot.buf, blen);
|
||||
if (!_fetch->write(_fpoff + block * _fbs, slot.buf, blen) ||
|
||||
!_fetch->write(_floff + block * 4, leaf, 4)) {
|
||||
_fetch_error = FETCH_ERROR_STORAGE;
|
||||
_fstate = PAUSED; clearReassembly(); return true;
|
||||
}
|
||||
_have++;
|
||||
@@ -1218,8 +1395,9 @@ bool OtaManager::handleProof(const uint8_t* m, uint16_t n) {
|
||||
// Every leaf must be readable and collectively match the manifest root.
|
||||
// A scratch allocation/read failure is an integrity failure, never success.
|
||||
clearReassembly();
|
||||
_fstate = storedLeavesRootMatches() ? COMPLETE : FAILED;
|
||||
if (_fstate == COMPLETE && !_fetch->finalize()) _fstate = FAILED;
|
||||
if (!storedLeavesRootMatches()) failFetch(FETCH_ERROR_INTEGRITY);
|
||||
else if (!_fetch->finalize()) failFetch(FETCH_ERROR_STORAGE);
|
||||
else completeFetch();
|
||||
OTA_DBG("OTA: transfer %s\n", _fstate == COMPLETE ? "COMPLETE" : "FAILED(integrity/storage)");
|
||||
return true;
|
||||
}
|
||||
@@ -1361,7 +1539,7 @@ void OtaManager::loop() {
|
||||
// the link and burn the retry cap while fragments are still arriving (mirrors FETCHING + WANT_LEAVES).
|
||||
// Give up after a cap of stalled retries so an unreachable mid doesn't pin the single fetch slot forever.
|
||||
if (_mf_mask == _loop_last_mfmask) {
|
||||
if (++_mf_retries > OTA_MANIFEST_MAX_RETRY) { _fstate = FAILED; return; }
|
||||
if (++_mf_retries > OTA_MANIFEST_MAX_RETRY) { failFetch(FETCH_ERROR_MANIFEST_TIMEOUT); return; }
|
||||
GetManifestMsg gm; memcpy(gm.manifest_id, _fid, 4);
|
||||
// request only the manifest fragments still missing; 0xFFFF ("send all") until we know frag_total
|
||||
gm.want_mask = (_mf_total > 0) ? (uint16_t)(frag_full_mask(_mf_total) & ~_mf_mask) : 0xFFFF;
|
||||
@@ -1378,7 +1556,7 @@ void OtaManager::loop() {
|
||||
// tick congests the link (and burns the retry cap) while fragments are still streaming in. On a stall,
|
||||
// ask for just the missing bitmap (anti-burst); give up (FAILED) after a cap of stalled retries.
|
||||
if (_lv_mask == _loop_last_lvmask) {
|
||||
if (++_lv_retries > OTA_LEAVES_MAX_RETRY) { freeLeaves(); _fstate = FAILED; return; }
|
||||
if (++_lv_retries > OTA_LEAVES_MAX_RETRY) { failFetch(FETCH_ERROR_LEAVES_TIMEOUT); return; }
|
||||
GetLeavesMsg gl; memcpy(gl.manifest_id, _fid, 4);
|
||||
gl.want_mask = (_lv_total > 0) ? (uint16_t)(frag_full_mask(_lv_total) & ~_lv_mask) : 0xFFFF;
|
||||
if (gl.want_mask == 0) gl.want_mask = 0xFFFF; // safety: never send an empty request
|
||||
|
||||
@@ -84,7 +84,7 @@ typedef bool (*ServeReadFn)(void* ctx, uint32_t off, uint8_t* buf, uint32_t len)
|
||||
#if defined(OTA_SD_STORE)
|
||||
#define OTA_MAX_SERVE 255 // protocol maximum: own fw plus up to 254 persistent SD archive entries
|
||||
#else
|
||||
#define OTA_MAX_SERVE 12 // mOTAs THIS node offers (own fw + external folder); == one HAVE fragment
|
||||
#define OTA_MAX_SERVE 24 // own fw plus a 23-step external bridge chain (fragmented OTA_HAVE)
|
||||
#endif
|
||||
#endif
|
||||
#ifndef OTA_MAX_SOURCE_OBJ
|
||||
@@ -190,6 +190,27 @@ public:
|
||||
COMPLETE, FAILED, PAUSED
|
||||
};
|
||||
|
||||
// A manual pull is acknowledged only after the manager has actually acquired the receive slot. This
|
||||
// keeps the CLI from reporting "OK pulling" when there is no store or another transfer is still active.
|
||||
enum PullResult : uint8_t {
|
||||
PULL_STARTED, PULL_RESUMED, PULL_NO_STORE, PULL_BUSY, PULL_BAD_MID
|
||||
};
|
||||
|
||||
// Terminal failures remain visible in `ota status` instead of collapsing back to an indistinguishable
|
||||
// IDLE/no-download state. Values are deliberately coarse: they are operator diagnostics, not wire data.
|
||||
enum FetchError : uint8_t {
|
||||
FETCH_ERROR_NONE,
|
||||
FETCH_ERROR_MANIFEST,
|
||||
FETCH_ERROR_HASH_ALGO,
|
||||
FETCH_ERROR_CODEC,
|
||||
FETCH_ERROR_GEOMETRY,
|
||||
FETCH_ERROR_TOO_LARGE,
|
||||
FETCH_ERROR_STORAGE,
|
||||
FETCH_ERROR_INTEGRITY,
|
||||
FETCH_ERROR_MANIFEST_TIMEOUT,
|
||||
FETCH_ERROR_LEAVES_TIMEOUT
|
||||
};
|
||||
|
||||
// Sentinel for "no block" in the reassembly / peer-REQ / recently-served slots (a real block index is
|
||||
// a small uint16, so 0xFFFFFFFF is never valid).
|
||||
static const uint32_t NO_BLOCK = 0xFFFFFFFFu;
|
||||
@@ -247,6 +268,9 @@ public:
|
||||
// Call this before releasing or overwriting the primary image's backing buffer.
|
||||
void clear_primary();
|
||||
uint8_t servedCount() const { return _n_serve; } // total mOTAs we offer (own fw + folder)
|
||||
// Per-source enumeration accounting. `offered` is what the attached host/source reported; `advertised`
|
||||
// is what fit in this build's serve registry after validation/deduplication/capacity limits.
|
||||
bool sourceStats(const MotaSource* src, uint16_t& offered, uint16_t& advertised) const;
|
||||
// Read-only view of one served entry (for `ota serve` listing): mid/target/fwver/codec/flags + is_self.
|
||||
const ServeEntry* servedEntry(uint8_t i) const { return i < _n_serve ? &_serve[i] : nullptr; }
|
||||
// 4-byte fingerprint of our served set (== the set_digest carried in the beacon); for admin OTA stats.
|
||||
@@ -274,7 +298,7 @@ public:
|
||||
uint32_t wanted() const { return _desired_target; }
|
||||
uint32_t target() const { return _target; } // this node's own OTA target_id (set in begin)
|
||||
|
||||
// Pull a SPECIFIC advertised mOTA by manifest_id (e.g. `ota pull <#>` picks the one more peers have),
|
||||
// Pull a SPECIFIC advertised mOTA by manifest_id (the CLI also accepts a current catalog index),
|
||||
// not just any firmware for the target. mid=nullptr clears the filter (accept any mid for the target).
|
||||
void want_mid(const uint8_t* mid) {
|
||||
if (mid) { for (int i = 0; i < 4; i++) _desired_mid[i] = mid[i]; _have_desired_mid = true; }
|
||||
@@ -287,16 +311,10 @@ public:
|
||||
// `validate` enables the motatool folder-capture warm-start: bulk-fetch the target leaves, diff a seed
|
||||
// build already staged in the destination, and pull DATA only for the differing blocks. Ignored unless a
|
||||
// seed is present (a plain fetch just re-transfers everything). Normal P2P pulls pass false.
|
||||
void pull(const uint8_t* mid, uint32_t target, bool validate = false) {
|
||||
_archive_fetch = false;
|
||||
want(target); want_mid(mid); startFetch(mid, target, validate);
|
||||
}
|
||||
PullResult pull(const uint8_t* mid, uint32_t target, bool validate = false);
|
||||
// Capture an advertised container for relaying, not installation. Archive pulls accept every codec and
|
||||
// target because the receiver only verifies and stores bytes; it never tries to apply the result locally.
|
||||
void pull_archive(const uint8_t* mid, uint32_t target) {
|
||||
_archive_fetch = true;
|
||||
want(target); want_mid(mid); startFetch(mid, target, false);
|
||||
}
|
||||
PullResult pull_archive(const uint8_t* mid, uint32_t target, bool validate = false);
|
||||
// Ask every known source for its catalog (populates `ota neighbors`). Async - rows arrive via OTA_HAVE.
|
||||
void queryAll();
|
||||
// Coarse clock for source/catalog ages + LRU (the Mesh adapter feeds millis; 0 in host tests is fine).
|
||||
@@ -371,7 +389,10 @@ public:
|
||||
|
||||
// Drop the current fetch session back to IDLE so a fresh `ota pull` / advert starts a new one.
|
||||
void reset_session() {
|
||||
_fstate = IDLE; _have = 0; _req_count = 0; _mf_retries = 0;
|
||||
_fstate = IDLE; _have = 0; _fbc = 0; _ftotal = 0; _fflags = 0;
|
||||
_req_count = 0; _mf_retries = 0;
|
||||
_fetch_error = FETCH_ERROR_NONE;
|
||||
clearFetchIntent();
|
||||
clearReassembly();
|
||||
_observed_path_transmissions = 0;
|
||||
_mf_total = 0; _mf_mask = 0; _mf_len = 0; _loop_last_mfmask = 0;
|
||||
@@ -382,6 +403,7 @@ public:
|
||||
}
|
||||
|
||||
FetchState fetchState() const { return _fstate; }
|
||||
FetchError fetchError() const { return _fetch_error; }
|
||||
uint32_t blocksHave() const { return _have; }
|
||||
uint32_t blocksTotal() const { return _fbc; }
|
||||
uint8_t fetchPipelineWidth() const { return _pipeline_width; }
|
||||
@@ -397,6 +419,8 @@ public:
|
||||
uint32_t target_id, fw_version;
|
||||
uint8_t codec, flags;
|
||||
uint8_t seeders[OTA_CAT_SEEDERS][4]; // distinct sources advertising this mid (deduped; capped)
|
||||
uint16_t seeder_have[OTA_CAT_SEEDERS]; // progress reported by each tracked source
|
||||
uint32_t seeder_last_ms[OTA_CAT_SEEDERS]; // age of each tracked source's latest HAVE
|
||||
uint8_t n_seeders; // count of the above (capped at OTA_CAT_SEEDERS) - "N+ nodes have it"
|
||||
uint32_t have_max; // best block-count any source reported (== total when a full copy exists)
|
||||
uint32_t last_ms;
|
||||
@@ -433,8 +457,13 @@ private:
|
||||
bool handleData(const uint8_t* m, uint16_t n);
|
||||
bool handleReqProof(const uint8_t* m, uint16_t n);
|
||||
bool handleProof(const uint8_t* m, uint16_t n);
|
||||
void startFetch(const uint8_t* mid, uint32_t target, bool validate = false); // begin/resume a fetch
|
||||
PullResult startFetch(const uint8_t* mid, uint32_t target, bool validate = false); // begin/resume a fetch
|
||||
bool wantRow(const uint8_t* mid, uint32_t target, uint8_t codec, uint8_t flags) const; // fetch this row?
|
||||
bool fetchActive() const;
|
||||
void clearFetchIntent();
|
||||
void failFetch(FetchError error);
|
||||
void completeFetch();
|
||||
void invalidateCatalogSeeder(const uint8_t* seeder);
|
||||
void clearReassembly(); // forget every in-flight pipeline slot
|
||||
void clearReassemblySlot(uint8_t slot);
|
||||
int findReassemblySlot(uint32_t block) const;
|
||||
@@ -491,6 +520,8 @@ private:
|
||||
ServeEntry _serve[OTA_MAX_SERVE]; // catalog (what we advertise) - entry 0 is view0
|
||||
uint8_t _n_serve = 0;
|
||||
MotaSource* _src_list[OTA_MAX_SOURCE_OBJ] = {nullptr};
|
||||
uint16_t _src_offered[OTA_MAX_SOURCE_OBJ] = {0};
|
||||
uint16_t _src_advertised[OTA_MAX_SOURCE_OBJ] = {0};
|
||||
uint8_t _n_src_obj = 0;
|
||||
uint8_t _src_manifest[OTA_SRC_MANIFEST_MAX]; // manifest-minus-leaves of the loaded source mota
|
||||
#if defined(ESP32_PLATFORM)
|
||||
@@ -520,6 +551,7 @@ private:
|
||||
// fetch
|
||||
OtaStore* _fetch = nullptr;
|
||||
FetchState _fstate = IDLE;
|
||||
FetchError _fetch_error = FETCH_ERROR_NONE;
|
||||
uint8_t _fid[4] = {0};
|
||||
uint8_t _froot[4] = {0};
|
||||
uint32_t _ftotal = 0, _fpoff = 0, _floff = 0, _fpsize = 0, _fbc = 0, _fbs = 0;
|
||||
|
||||
@@ -13,6 +13,36 @@
|
||||
#define RX_POWERSAVING_BALANCED_LEVEL 5
|
||||
#define RX_POWERSAVING_PROFILE_PREAMBLE 16
|
||||
|
||||
// Optional initial settings for infrastructure roles. They are intentionally
|
||||
// disabled unless a build profile supplies them, so upstream/default builds
|
||||
// retain continuous receive. Companion has its own historical defaults below.
|
||||
#ifndef DEFAULT_RXPS_ENABLED
|
||||
#define DEFAULT_RXPS_ENABLED 0
|
||||
#endif
|
||||
#ifndef DEFAULT_RXPS_LEVEL
|
||||
#if DEFAULT_RXPS_ENABLED
|
||||
#define DEFAULT_RXPS_LEVEL RX_POWERSAVING_BALANCED_LEVEL
|
||||
#else
|
||||
#define DEFAULT_RXPS_LEVEL 0
|
||||
#endif
|
||||
#endif
|
||||
#ifndef DEFAULT_RXPS_PREAMBLE
|
||||
#if DEFAULT_RXPS_ENABLED
|
||||
#define DEFAULT_RXPS_PREAMBLE RX_POWERSAVING_PROFILE_PREAMBLE
|
||||
#else
|
||||
#define DEFAULT_RXPS_PREAMBLE 0
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if DEFAULT_RXPS_ENABLED
|
||||
#if DEFAULT_RXPS_LEVEL < 1 || DEFAULT_RXPS_LEVEL > 10
|
||||
#error "DEFAULT_RXPS_LEVEL must be between 1 and 10"
|
||||
#endif
|
||||
#if DEFAULT_RXPS_PREAMBLE != 16 && DEFAULT_RXPS_PREAMBLE != 32
|
||||
#error "DEFAULT_RXPS_PREAMBLE must be 16 or 32"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// Initial settings for companions. Build flags can override the defaults;
|
||||
// roles with runtime RXPS controls persist the operator's selection afterward.
|
||||
#ifndef RXPS_FIXED_ENABLED
|
||||
|
||||
Reference in new issue
Block a user