From 8b1fd10db7295297bbd9d9ab7bc85b201a23214b Mon Sep 17 00:00:00 2001 From: Peter Woolery Date: Mon, 11 May 2026 06:59:02 -0700 Subject: [PATCH] feat(firmware): add OTA updater library skeleton with version comparison Co-Authored-By: Claude Sonnet 4.6 (1M context) --- firmware/lib/ota_updater/ota_updater.cpp | 53 ++++++++++++++++++++++++ firmware/lib/ota_updater/ota_updater.h | 27 ++++++++++++ firmware/test/test_ota/test_version.cpp | 44 ++++++++++++++++++++ 3 files changed, 124 insertions(+) create mode 100644 firmware/lib/ota_updater/ota_updater.cpp create mode 100644 firmware/lib/ota_updater/ota_updater.h create mode 100644 firmware/test/test_ota/test_version.cpp diff --git a/firmware/lib/ota_updater/ota_updater.cpp b/firmware/lib/ota_updater/ota_updater.cpp new file mode 100644 index 0000000..a53311a --- /dev/null +++ b/firmware/lib/ota_updater/ota_updater.cpp @@ -0,0 +1,53 @@ +// firmware/lib/ota_updater/ota_updater.cpp +#include "ota_updater.h" +#include +#include + +// ── version comparison ───────────────────────────────────────────────────── + +bool ota_version_newer(const char* current, const char* remote) { + int ca = 0, cb = 0, cc = 0; + int ra = 0, rb = 0, rc = 0; + if (sscanf(current, "%d.%d.%d", &ca, &cb, &cc) != 3) return false; + if (sscanf(remote, "%d.%d.%d", &ra, &rb, &rc) != 3) return false; + if (ra != ca) return ra > ca; + if (rb != cb) return rb > cb; + return rc > cc; +} + +// ── signature verification ───────────────────────────────────────────────── +// (real implementation added in Task 7) +bool ota_verify_signature_with_key(const uint8_t hash32[32], const uint8_t sig64[64], + const uint8_t pubkey65[65]) { + (void)hash32; (void)sig64; (void)pubkey65; + return false; // placeholder — filled in Task 7 +} + +// ── device-only code ─────────────────────────────────────────────────────── +#ifndef NATIVE_TEST + +#include "ota_pubkey.h" +#include "version.h" + +bool ota_verify_signature(const uint8_t hash32[32], const uint8_t sig64[64]) { + return ota_verify_signature_with_key(hash32, sig64, kOtaPublicKey); +} + +static const char* s_server_base = nullptr; +static const char* s_device_id = nullptr; +static const char* s_hmac_secret = nullptr; +static uint32_t s_interval_ms = 21600000UL; // 6 h default +static uint32_t s_last_check_ms = 0; + +void ota_updater_init(const char* server_base, const char* device_id, + const char* hmac_secret, uint32_t check_interval_ms) { + s_server_base = server_base; + s_device_id = device_id; + s_hmac_secret = hmac_secret; + s_interval_ms = check_interval_ms; + s_last_check_ms = 0; // force first check on next call +} + +bool ota_updater_check_and_apply() { return false; } // filled in Task 8 + +#endif // NATIVE_TEST diff --git a/firmware/lib/ota_updater/ota_updater.h b/firmware/lib/ota_updater/ota_updater.h new file mode 100644 index 0000000..ae36e2b --- /dev/null +++ b/firmware/lib/ota_updater/ota_updater.h @@ -0,0 +1,27 @@ +// firmware/lib/ota_updater/ota_updater.h +#pragma once +#include +#include +#include + +// One-time init. Call from setup() after WiFi is ready. +// server_base: e.g. "http://logs.research.bike:8000" +// check_interval_ms: milliseconds between polls (e.g. 6*3600*1000 = 21600000) +void ota_updater_init(const char* server_base, + const char* device_id, + const char* hmac_secret, + uint32_t check_interval_ms); + +// Polls server; downloads, verifies, and flashes if newer version available. +// Returns true if update was applied (device reboots before returning false path). +// Safe to call from any task; blocks during download. +bool ota_updater_check_and_apply(); + +// Exposed for unit testing — pass an arbitrary 65-byte uncompressed P-256 pubkey. +bool ota_version_newer(const char* current, const char* remote); +bool ota_verify_signature_with_key(const uint8_t hash32[32], const uint8_t sig64[64], + const uint8_t pubkey65[65]); + +// Production wrapper — uses the compiled-in kOtaPublicKey from ota_pubkey.h. +// Not callable from native tests (requires ota_pubkey.h / device build). +bool ota_verify_signature(const uint8_t hash32[32], const uint8_t sig64[64]); diff --git a/firmware/test/test_ota/test_version.cpp b/firmware/test/test_ota/test_version.cpp new file mode 100644 index 0000000..b5afcc2 --- /dev/null +++ b/firmware/test/test_ota/test_version.cpp @@ -0,0 +1,44 @@ +// firmware/test/test_ota/test_version.cpp +#include + +// Pull in the function under test — include .cpp directly for native builds +// so we don't need a separate compilation unit per test. +#define NATIVE_TEST +#include "../../lib/ota_updater/ota_updater.cpp" + +void setUp() {} +void tearDown() {} + +void test_remote_newer_patch() { + TEST_ASSERT_TRUE(ota_version_newer("1.0.0", "1.0.1")); +} +void test_remote_newer_minor() { + TEST_ASSERT_TRUE(ota_version_newer("1.0.9", "1.1.0")); +} +void test_remote_newer_major() { + TEST_ASSERT_TRUE(ota_version_newer("0.9.9", "1.0.0")); +} +void test_same_version() { + TEST_ASSERT_FALSE(ota_version_newer("1.2.3", "1.2.3")); +} +void test_remote_older() { + TEST_ASSERT_FALSE(ota_version_newer("1.2.3", "1.2.2")); +} +void test_malformed_current() { + TEST_ASSERT_FALSE(ota_version_newer("bad", "1.0.0")); +} +void test_malformed_remote() { + TEST_ASSERT_FALSE(ota_version_newer("1.0.0", "bad")); +} + +int main() { + UNITY_BEGIN(); + RUN_TEST(test_remote_newer_patch); + RUN_TEST(test_remote_newer_minor); + RUN_TEST(test_remote_newer_major); + RUN_TEST(test_same_version); + RUN_TEST(test_remote_older); + RUN_TEST(test_malformed_current); + RUN_TEST(test_malformed_remote); + return UNITY_END(); +}