Share nRF52 checked-record reads and command-family lookups without changing storage formats, error protection, fleet authorization, or delegated scopes. Use the existing bounded Merkle proof workspace for qualified ESP32 repeaters while retaining source capacity, wire format, and every enabled feature. Restore advert tab/CRLF delimiters and expose nested qualification build logs. Validation: all 1932 native CI test cases; exact canonical RAK4631 Full, Wio-E5 Mini, Station G3 Full MQTT, and Wireless Tracker Full wrappers; linked RAK crypto/workspace checks and retained Reader code equivalence; focused persistence, authorization, source-proof, and RAM policy regressions.
233 lines
8.6 KiB
C++
233 lines
8.6 KiB
C++
#if defined(ENABLE_OTA)
|
|
#include "OtaSpeedConfig.h"
|
|
#include <helpers/CLICommandUtils.h>
|
|
#include <helpers/PersistentStoreFormat.h>
|
|
#if defined(NRF52_PLATFORM)
|
|
#include <helpers/CheckedSettingsFile.h>
|
|
#include <helpers/FilePresence.h>
|
|
#endif
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#if defined(ESP32_PLATFORM)
|
|
#include <errno.h>
|
|
#include <sys/stat.h>
|
|
#include <helpers/esp32/BootFilePresence.h>
|
|
#endif
|
|
|
|
namespace mesh { namespace ota {
|
|
namespace {
|
|
FILESYSTEM* settings_fs = nullptr;
|
|
float factor = OTA_SPEED_DEFAULT;
|
|
bool held = false;
|
|
bool recoverable_corrupt_store = false;
|
|
constexpr size_t ImageSize = 12;
|
|
const char* const Path = "/ota_speed";
|
|
const char* const Temp = "/ota_speed.tmp";
|
|
const char* const Backup = "/ota_speed.bak";
|
|
|
|
enum class ImageReadResult : uint8_t { Missing, Valid, Invalid, Unreadable };
|
|
|
|
// A failed open is not enough to prove a path is absent: some filesystems
|
|
// report an I/O error as a failed exists() call. Keep those images protected.
|
|
bool pathPresence(const char* path, bool& present) {
|
|
#if defined(NRF52_PLATFORM)
|
|
return filePresence(settings_fs, path, present);
|
|
#elif defined(STM32_PLATFORM)
|
|
struct lfs_info info;
|
|
settings_fs->_lockFS();
|
|
const int result = lfs_stat(settings_fs->_getFS(), path, &info);
|
|
settings_fs->_unlockFS();
|
|
if (result != 0 && result != LFS_ERR_NOENT) return false;
|
|
present = result == 0;
|
|
#elif defined(ESP32_PLATFORM)
|
|
char vfs_path[48];
|
|
const int length = snprintf(vfs_path, sizeof(vfs_path), "/spiffs%s", path);
|
|
if (length < 0 || static_cast<size_t>(length) >= sizeof(vfs_path)) return false;
|
|
if (esp32BootFileKnownAbsent(settings_fs, path)) {
|
|
present = false;
|
|
return true;
|
|
}
|
|
struct stat info;
|
|
const int result = ::stat(vfs_path, &info);
|
|
if (result != 0 && errno != ENOENT) return false;
|
|
present = result == 0;
|
|
#else
|
|
present = settings_fs->exists(path);
|
|
#endif
|
|
return true;
|
|
}
|
|
|
|
bool removeIfPresent(const char* path) {
|
|
bool present = false;
|
|
return pathPresence(path, present) && (!present || settings_fs->remove(path));
|
|
}
|
|
|
|
ImageReadResult readImage(const char* path, uint8_t* image) {
|
|
bool present = false;
|
|
if (!settings_fs || !pathPresence(path, present)) return ImageReadResult::Unreadable;
|
|
if (!present) return ImageReadResult::Missing;
|
|
#if defined(NRF52_PLATFORM)
|
|
const storage::FixedSettingsRead read = storage::readFixedSettingsFileResult(
|
|
settings_fs, path, image, ImageSize);
|
|
if (read != storage::FixedSettingsRead::Complete)
|
|
return read == storage::FixedSettingsRead::WrongSize
|
|
? ImageReadResult::Invalid : ImageReadResult::Unreadable;
|
|
#else
|
|
#if defined(STM32_PLATFORM)
|
|
File file = settings_fs->open(path, FILE_O_READ);
|
|
#else
|
|
File file = settings_fs->open(path, "r");
|
|
#endif
|
|
if (!file) return ImageReadResult::Unreadable;
|
|
const bool right_size = file.size() == ImageSize;
|
|
const bool complete = right_size && file.read(image, ImageSize) == (int)ImageSize;
|
|
file.close();
|
|
if (!right_size) return ImageReadResult::Invalid;
|
|
if (!complete) return ImageReadResult::Unreadable;
|
|
#endif
|
|
float value;
|
|
memcpy(&value, image + 4, sizeof(value));
|
|
return !memcmp(image, "OS\1\0", 4) && validSpeed(value)
|
|
&& storage::readLE32(image + 8) == storage::updateCRC32(0xffffffffU, image, 8)
|
|
? ImageReadResult::Valid : ImageReadResult::Invalid;
|
|
}
|
|
|
|
bool discardCorruptSavedImagesForWrite() {
|
|
if (!held) return true;
|
|
if (!settings_fs || !recoverable_corrupt_store) return false;
|
|
// This is only reached from an explicit setter after both images were read
|
|
// completely and rejected. OTA policy is independent of identity/key data.
|
|
if (!removeIfPresent(Path) || !removeIfPresent(Backup)) return false;
|
|
held = false;
|
|
recoverable_corrupt_store = false;
|
|
return true;
|
|
}
|
|
|
|
bool saveSpeed(float value) {
|
|
if (!settings_fs || !validSpeed(value) || !discardCorruptSavedImagesForWrite()) return false;
|
|
uint8_t image[ImageSize] = {'O', 'S', 1, 0};
|
|
memcpy(image + 4, &value, sizeof(value));
|
|
storage::writeLE32(image + 8, storage::updateCRC32(0xffffffffU, image, 8));
|
|
if (settings_fs->exists(Temp) && !settings_fs->remove(Temp)) return false;
|
|
#if defined(NRF52_PLATFORM)
|
|
File file(*settings_fs);
|
|
if (!file.open(Temp, FILE_O_WRITE)) return false;
|
|
#elif defined(STM32_PLATFORM)
|
|
File file = settings_fs->open(Temp, FILE_O_WRITE);
|
|
#else
|
|
File file = settings_fs->open(Temp, "w");
|
|
#endif
|
|
if (!file) return false;
|
|
bool ok = file.write(image, sizeof(image)) == sizeof(image);
|
|
file.flush(); file.close();
|
|
uint8_t verify[ImageSize];
|
|
if (!ok || readImage(Temp, verify) != ImageReadResult::Valid
|
|
|| memcmp(image, verify, sizeof(image))) return false;
|
|
if (!removeIfPresent(Backup)) return false;
|
|
bool had_primary = false;
|
|
if (!pathPresence(Path, had_primary)) { held = true; return false; }
|
|
if (had_primary && !settings_fs->rename(Path, Backup)) return false;
|
|
if (!settings_fs->rename(Temp, Path)) {
|
|
if (had_primary && !settings_fs->rename(Backup, Path)) held = true;
|
|
return false;
|
|
}
|
|
if (had_primary) settings_fs->remove(Backup);
|
|
factor = value; // publish only after the complete image commits
|
|
return true;
|
|
}
|
|
}
|
|
|
|
void beginSpeedConfig(FILESYSTEM* fs) {
|
|
settings_fs = fs; factor = OTA_SPEED_DEFAULT; held = false;
|
|
recoverable_corrupt_store = false;
|
|
if (!fs) return;
|
|
uint8_t image[ImageSize];
|
|
const ImageReadResult primary = readImage(Path, image);
|
|
bool loaded = primary == ImageReadResult::Valid;
|
|
ImageReadResult backup = ImageReadResult::Missing;
|
|
if (!loaded) backup = readImage(Backup, image);
|
|
if (!loaded && backup == ImageReadResult::Valid) {
|
|
// A verified backup is still authoritative when storage cannot repair the
|
|
// rename gap. Keep its pace instead of silently reverting to the faster 1x.
|
|
memcpy(&factor, image + 4, sizeof(factor));
|
|
if (primary == ImageReadResult::Unreadable
|
|
|| (primary != ImageReadResult::Missing && !removeIfPresent(Path))) {
|
|
held = true;
|
|
return;
|
|
}
|
|
if (!fs->rename(Backup, Path)) { held = true; return; }
|
|
loaded = true;
|
|
}
|
|
if (loaded) memcpy(&factor, image + 4, sizeof(factor));
|
|
else {
|
|
held = primary != ImageReadResult::Missing || backup != ImageReadResult::Missing;
|
|
// Only an explicit later setter may recreate conclusively corrupt OTA
|
|
// policy files. An unreadable file stays untouched for a clean reboot.
|
|
recoverable_corrupt_store = held
|
|
&& primary != ImageReadResult::Unreadable
|
|
&& backup != ImageReadResult::Unreadable
|
|
&& (primary == ImageReadResult::Invalid || backup == ImageReadResult::Invalid);
|
|
}
|
|
}
|
|
|
|
float speedFactor() { return factor; }
|
|
|
|
void formatSpeedFactor(float speed, char* text, size_t capacity) {
|
|
const uint32_t scaled = (uint32_t)(speed * 1000000.0f + 0.5f);
|
|
snprintf(text, capacity, "%u.%06u", (unsigned)(scaled / 1000000), (unsigned)(scaled % 1000000));
|
|
if (!capacity) return;
|
|
size_t n = strlen(text);
|
|
while (n && text[n - 1] == '0') text[--n] = 0;
|
|
if (n && text[n - 1] == '.') text[n - 1] = 0;
|
|
}
|
|
|
|
void formatSpeed(char* text, size_t capacity) {
|
|
formatSpeedFactor(factor, text, capacity);
|
|
}
|
|
|
|
float effectivePacketPace(float adaptive_speed) {
|
|
if (!validSpeed(adaptive_speed)) adaptive_speed = OTA_SPEED_DEFAULT;
|
|
return factor < adaptive_speed ? factor : adaptive_speed;
|
|
}
|
|
|
|
bool handleSpeedCommand(const char* command, char* reply, size_t capacity,
|
|
float adaptive_speed) {
|
|
const char* value = nullptr;
|
|
bool require_value = false, read_only = false;
|
|
const char* const forms[] = {"set ota.speed", "get ota.speed", "ota config speed", "ota cfg speed", "ota set speed", "ota speed"};
|
|
for (size_t i = 0; i < sizeof(forms) / sizeof(forms[0]); ++i) {
|
|
const size_t n = strlen(forms[i]);
|
|
if (!strncmp(command, forms[i], n) && (command[n] == 0 || command[n] == ' ')) {
|
|
value = command + n;
|
|
while (*value == ' ') ++value;
|
|
require_value = i == 0; read_only = i == 1;
|
|
break;
|
|
}
|
|
}
|
|
if (!value) return false;
|
|
if ((require_value && !*value) || (read_only && *value)) {
|
|
snprintf(reply, capacity, "ERR usage: set ota.speed <0.05..3> | get ota.speed");
|
|
return true;
|
|
}
|
|
if (*value) {
|
|
float parsed;
|
|
if (!cli::parseDecimalStrict(value, parsed) || !validSpeed(parsed)) {
|
|
snprintf(reply, capacity, "ERR ota.speed must be 0.05..3 (1=current speed)");
|
|
return true;
|
|
}
|
|
if (!saveSpeed(parsed)) {
|
|
snprintf(reply, capacity, "ERR ota.speed save failed; speed unchanged");
|
|
return true;
|
|
}
|
|
}
|
|
char number[16], packet[16];
|
|
formatSpeed(number, sizeof(number));
|
|
formatSpeedFactor(effectivePacketPace(adaptive_speed), packet, sizeof(packet));
|
|
if (*value) snprintf(reply, capacity, "OK ota.speed=%sx (saved) packet=%sx", number, packet);
|
|
else snprintf(reply, capacity, "> ota.speed=%sx packet=%sx", number, packet);
|
|
return true;
|
|
}
|
|
|
|
} }
|
|
#endif
|