From 3aee296cf1bc052b0bc2b61b81544966de4b22a9 Mon Sep 17 00:00:00 2001 From: HAN Date: Tue, 6 Oct 2026 23:08:31 +0700 Subject: [PATCH 01/16] Make table sizes overridable (MAX_ANON_CONTACTS, MAX_PACKET_HASHES, ADVERT_PATH_TABLE_SIZE, COMPANION_PACKET_POOL_SIZE) --- examples/companion_radio/MyMesh.cpp | 2 +- examples/companion_radio/MyMesh.h | 8 +++++++- src/helpers/BaseChatMesh.h | 4 +++- src/helpers/SimpleMeshTables.h | 4 +++- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index f32ea67aec..d17ebdba2e 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -898,7 +898,7 @@ uint32_t MyMesh::calcDirectTimeoutMillisFor(uint32_t pkt_airtime_millis, uint8_t void MyMesh::onSendTimeout() {} MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store) - : BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(16), tables), + : BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(COMPANION_PACKET_POOL_SIZE), tables), _serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _listener(NULL), _iter(0) { _iter_started = false; _cli_rescue = false; diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 52a22cb419..d5c8764e60 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -62,6 +62,10 @@ #define OFFLINE_QUEUE_SIZE 16 #endif +#ifndef COMPANION_PACKET_POOL_SIZE +#define COMPANION_PACKET_POOL_SIZE 16 +#endif + #ifndef BLE_NAME_PREFIX #define BLE_NAME_PREFIX "MeshCore-" #endif @@ -286,7 +290,9 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { AckTableEntry expected_ack_table[EXPECTED_ACK_TABLE_SIZE]; // circular table int next_ack_idx; - #define ADVERT_PATH_TABLE_SIZE 16 + #ifndef ADVERT_PATH_TABLE_SIZE + #define ADVERT_PATH_TABLE_SIZE 16 + #endif AdvertPath advert_paths[ADVERT_PATH_TABLE_SIZE]; // circular table }; diff --git a/src/helpers/BaseChatMesh.h b/src/helpers/BaseChatMesh.h index 835384b741..816fcd465d 100644 --- a/src/helpers/BaseChatMesh.h +++ b/src/helpers/BaseChatMesh.h @@ -38,7 +38,9 @@ class ContactsIterator { #define MAX_CONTACTS 32 #endif -#define MAX_ANON_CONTACTS 8 +#ifndef MAX_ANON_CONTACTS + #define MAX_ANON_CONTACTS 8 +#endif #ifndef MAX_CONNECTIONS #define MAX_CONNECTIONS 16 diff --git a/src/helpers/SimpleMeshTables.h b/src/helpers/SimpleMeshTables.h index 956f36faa6..75e25844fa 100644 --- a/src/helpers/SimpleMeshTables.h +++ b/src/helpers/SimpleMeshTables.h @@ -6,7 +6,9 @@ #include #endif -#define MAX_PACKET_HASHES (128+32) +#ifndef MAX_PACKET_HASHES + #define MAX_PACKET_HASHES (128+32) +#endif class SimpleMeshTables : public mesh::MeshTables { uint8_t _hashes[MAX_PACKET_HASHES*MAX_HASH_SIZE]; From 27ef6c3623b17c6c2a1a8fec205f88203f3caf25 Mon Sep 17 00:00:00 2001 From: HAN Date: Tue, 6 Oct 2026 23:11:59 +0700 Subject: [PATCH 02/16] Add CONTACT_OUT_PATH_SIZE: contacts can store shorter direct routes (default unchanged) Routes that do not fit are not stored (OUT_PATH_UNKNOWN) and the contact is reached by flood. Protocol frames keep 64 path bytes. With the default size the call-site helpers compile to the previous code. --- examples/companion_radio/DataStore.cpp | 4 + examples/companion_radio/MyMesh.cpp | 6 +- examples/simple_secure_chat/main.cpp | 4 + src/helpers/BaseChatMesh.cpp | 2 +- src/helpers/ContactInfo.h | 3 +- src/helpers/ContactPath.h | 69 ++++++++++++++++ test/test_contact_path/test_contact_path.cpp | 85 ++++++++++++++++++++ 7 files changed, 168 insertions(+), 5 deletions(-) create mode 100644 src/helpers/ContactPath.h create mode 100644 test/test_contact_path/test_contact_path.cpp diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index 06c56a7a44..77b9209b51 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -1,6 +1,10 @@ #include #include "DataStore.h" +#if CONTACT_OUT_PATH_SIZE < MAX_PATH_SIZE + #error "this file-based contact store writes 64-byte routes: build it with the default CONTACT_OUT_PATH_SIZE" +#endif + #if defined(EXTRAFS) || defined(QSPIFLASH) #define MAX_BLOBRECS 100 #else diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index d17ebdba2e..55aa14b727 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -176,7 +176,7 @@ void MyMesh::writeContactRespFrame(uint8_t code, const ContactInfo &contact) { out_frame[i++] = contact.type; out_frame[i++] = contact.flags; out_frame[i++] = contact.out_path_len; - memcpy(&out_frame[i], contact.out_path, MAX_PATH_SIZE); + contactPathTo64(&out_frame[i], contact.out_path, contact.out_path_len); i += MAX_PATH_SIZE; StrHelper::strzcpy((char *)&out_frame[i], contact.name, 32); i += 32; @@ -198,8 +198,8 @@ void MyMesh::updateContactFromFrame(ContactInfo &contact, uint32_t& last_mod, co i += PUB_KEY_SIZE; contact.type = frame[i++]; contact.flags = frame[i++]; - contact.out_path_len = frame[i++]; - memcpy(contact.out_path, &frame[i], MAX_PATH_SIZE); + contact.out_path_len = contactPathFrom64(contact.out_path, &frame[i + 1], frame[i]); + i++; i += MAX_PATH_SIZE; memcpy(contact.name, &frame[i], 32); i += 32; diff --git a/examples/simple_secure_chat/main.cpp b/examples/simple_secure_chat/main.cpp index 2e6ba7126d..6bed916d03 100644 --- a/examples/simple_secure_chat/main.cpp +++ b/examples/simple_secure_chat/main.cpp @@ -42,6 +42,10 @@ #include +#if CONTACT_OUT_PATH_SIZE < MAX_PATH_SIZE + #error "this file-based contact store writes 64-byte routes: build it with the default CONTACT_OUT_PATH_SIZE" +#endif + #define SEND_TIMEOUT_BASE_MILLIS 500 #define FLOOD_SEND_TIMEOUT_FACTOR 16.0f #define DIRECT_SEND_PERHOP_FACTOR 6.0f diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index 28592415ca..2d1c626991 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -353,7 +353,7 @@ bool BaseChatMesh::onPeerPathRecv(mesh::Packet* packet, int sender_idx, const ui bool BaseChatMesh::onContactPathRecv(ContactInfo& from, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) { // NOTE: default impl, we just replace the current 'out_path' regardless, whenever sender sends us a new out_path. // FUTURE: could store multiple out_paths per contact, and try to find which is the 'best'(?) - from.out_path_len = mesh::Packet::copyPath(from.out_path, out_path, out_path_len); // store a copy of path, for sendDirect() + from.out_path_len = contactPathFromPacket(from.out_path, out_path, out_path_len); // store a copy of path, for sendDirect() from.lastmod = getRTCClock()->getCurrentTime(); onContactPathUpdated(from); diff --git a/src/helpers/ContactInfo.h b/src/helpers/ContactInfo.h index 5a156c8292..5b6819cae7 100644 --- a/src/helpers/ContactInfo.h +++ b/src/helpers/ContactInfo.h @@ -2,6 +2,7 @@ #include #include +#include "ContactPath.h" #define OUT_PATH_UNKNOWN 0xFF @@ -12,7 +13,7 @@ struct ContactInfo { uint8_t flags; uint8_t out_path_len; mutable bool shared_secret_valid; // flag to indicate if shared_secret has been calculated - uint8_t out_path[MAX_PATH_SIZE]; + uint8_t out_path[CONTACT_OUT_PATH_SIZE]; uint32_t last_advert_timestamp; // by THEIR clock uint32_t lastmod; // by OUR clock int32_t gps_lat, gps_lon; // 6 dec places diff --git a/src/helpers/ContactPath.h b/src/helpers/ContactPath.h new file mode 100644 index 0000000000..998124a552 --- /dev/null +++ b/src/helpers/ContactPath.h @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include // MAX_PATH_SIZE + +// Bytes reserved for a contact's stored direct route. Boards with very little RAM can trade long direct routes +// for contact table space: a route that does not fit is not stored (OUT_PATH_UNKNOWN), and messages to that +// contact go out flood instead. The default keeps the full MAX_PATH_SIZE. +#ifndef CONTACT_OUT_PATH_SIZE + #define CONTACT_OUT_PATH_SIZE MAX_PATH_SIZE +#endif + +#ifndef OUT_PATH_UNKNOWN + #define OUT_PATH_UNKNOWN 0xFF +#endif + +// Number of bytes of an encoded path_len (upper 2 bits: hash size - 1, lower 6 bits: hop count), +// or 0xFF for the reserved hash size 4. +inline uint8_t contactPathBytes(uint8_t path_len) { + uint8_t hash_size = (path_len >> 6) + 1; + if (hash_size == 4) return 0xFF; + return (uint8_t)((path_len & 63) * hash_size); +} + +// Stores a route into a contact's out_path. Returns the stored path_len, or OUT_PATH_UNKNOWN when it does not fit. +inline uint8_t contactStorePath(uint8_t* dest, const uint8_t* src, uint8_t path_len) { + if (path_len == OUT_PATH_UNKNOWN) return OUT_PATH_UNKNOWN; + uint8_t n = contactPathBytes(path_len); + if (n > CONTACT_OUT_PATH_SIZE || n > MAX_PATH_SIZE) return OUT_PATH_UNKNOWN; + memcpy(dest, src, n); + return path_len; +} + +// Protocol frames and files always carry MAX_PATH_SIZE path bytes: copies the stored route and zeroes the rest. +inline void contactWritePath64(uint8_t* dest64, const uint8_t* stored, uint8_t path_len) { + memset(dest64, 0, MAX_PATH_SIZE); + if (path_len == OUT_PATH_UNKNOWN) return; + uint8_t n = contactPathBytes(path_len); + if (n > CONTACT_OUT_PATH_SIZE) return; // never stored that way: nothing valid to copy + memcpy(dest64, stored, n); +} + +// Call-site helpers. With the default CONTACT_OUT_PATH_SIZE they compile to exactly the operation each call site +// used before (so other boards are unchanged); with a smaller size they clamp as above. +#if CONTACT_OUT_PATH_SIZE < MAX_PATH_SIZE +inline uint8_t contactPathFromPacket(uint8_t* dest, const uint8_t* src, uint8_t path_len) { + return contactStorePath(dest, src, path_len); +} +inline uint8_t contactPathFrom64(uint8_t* dest, const uint8_t* src64, uint8_t path_len) { + return contactStorePath(dest, src64, path_len); +} +inline void contactPathTo64(uint8_t* dest64, const uint8_t* stored, uint8_t path_len) { + contactWritePath64(dest64, stored, path_len); +} +#else +#include +inline uint8_t contactPathFromPacket(uint8_t* dest, const uint8_t* src, uint8_t path_len) { + return mesh::Packet::copyPath(dest, src, path_len); +} +inline uint8_t contactPathFrom64(uint8_t* dest, const uint8_t* src64, uint8_t path_len) { + memcpy(dest, src64, MAX_PATH_SIZE); + return path_len; +} +inline void contactPathTo64(uint8_t* dest64, const uint8_t* stored, uint8_t path_len) { + (void)path_len; + memcpy(dest64, stored, MAX_PATH_SIZE); +} +#endif diff --git a/test/test_contact_path/test_contact_path.cpp b/test/test_contact_path/test_contact_path.cpp new file mode 100644 index 0000000000..421008cb55 --- /dev/null +++ b/test/test_contact_path/test_contact_path.cpp @@ -0,0 +1,85 @@ +#include +#include + +#define CONTACT_OUT_PATH_SIZE 16 +#include "helpers/ContactPath.h" + +// encoded path_len: upper 2 bits = hash size - 1, lower 6 bits = hop count +static uint8_t enc(uint8_t hash_size, uint8_t count) { return (uint8_t)(((hash_size - 1) << 6) | count); } + +TEST(ContactPath, ShortPathIsStored) { + uint8_t src[64], dst[CONTACT_OUT_PATH_SIZE + 1]; + for (int i = 0; i < 64; i++) src[i] = (uint8_t)(i + 1); + dst[CONTACT_OUT_PATH_SIZE] = 0xAA; // guard byte + EXPECT_EQ(contactStorePath(dst, src, enc(1, 5)), enc(1, 5)); + EXPECT_EQ(memcmp(dst, src, 5), 0); + EXPECT_EQ(dst[CONTACT_OUT_PATH_SIZE], 0xAA); +} + +TEST(ContactPath, ExactlyFullIsStored) { + uint8_t src[64] = {0}, dst[CONTACT_OUT_PATH_SIZE + 1]; + dst[CONTACT_OUT_PATH_SIZE] = 0xAA; + EXPECT_EQ(contactStorePath(dst, src, enc(2, 8)), enc(2, 8)); // 8 hops x 2 bytes = 16 + EXPECT_EQ(dst[CONTACT_OUT_PATH_SIZE], 0xAA); +} + +TEST(ContactPath, TooLongBecomesUnknown) { + uint8_t src[64] = {0}, dst[CONTACT_OUT_PATH_SIZE + 1]; + dst[CONTACT_OUT_PATH_SIZE] = 0xAA; + for (uint8_t hops = 17; hops <= 63; hops++) { // 1-byte hashes + EXPECT_EQ(contactStorePath(dst, src, enc(1, hops)), OUT_PATH_UNKNOWN) << (int)hops; + } + EXPECT_EQ(contactStorePath(dst, src, enc(3, 6)), OUT_PATH_UNKNOWN); // 18 bytes + EXPECT_EQ(dst[CONTACT_OUT_PATH_SIZE], 0xAA); +} + +TEST(ContactPath, ReservedHashSizeBecomesUnknown) { + uint8_t src[64] = {0}, dst[CONTACT_OUT_PATH_SIZE + 1]; + dst[CONTACT_OUT_PATH_SIZE] = 0xAA; + EXPECT_EQ(contactStorePath(dst, src, enc(4, 2)), OUT_PATH_UNKNOWN); // hash size 4 is reserved + EXPECT_EQ(dst[CONTACT_OUT_PATH_SIZE], 0xAA); +} + +TEST(ContactPath, UnknownStaysUnknown) { + uint8_t src[64] = {0}, dst[CONTACT_OUT_PATH_SIZE]; + EXPECT_EQ(contactStorePath(dst, src, OUT_PATH_UNKNOWN), OUT_PATH_UNKNOWN); +} + +TEST(ContactPath, FrameAlwaysGets64Bytes) { + uint8_t stored[CONTACT_OUT_PATH_SIZE], frame[65]; + for (int i = 0; i < CONTACT_OUT_PATH_SIZE; i++) stored[i] = (uint8_t)(0x10 + i); + memset(frame, 0xEE, sizeof(frame)); + contactWritePath64(frame, stored, enc(1, 4)); + EXPECT_EQ(memcmp(frame, stored, 4), 0); + for (int i = 4; i < 64; i++) EXPECT_EQ(frame[i], 0) << i; // nothing stale after the route + EXPECT_EQ(frame[64], 0xEE); // nothing written past 64 +} + +TEST(ContactPath, FrameForUnknownRouteIsAllZero) { + uint8_t stored[CONTACT_OUT_PATH_SIZE], frame[64]; + memset(stored, 0x55, sizeof(stored)); + memset(frame, 0xEE, sizeof(frame)); + contactWritePath64(frame, stored, OUT_PATH_UNKNOWN); + for (int i = 0; i < 64; i++) EXPECT_EQ(frame[i], 0) << i; +} + +// call-site wrappers (small mode): from a 64-byte frame/file field, from a packet route, and back to 64 bytes +TEST(ContactPath, WrappersClampInSmallMode) { + uint8_t src[64], dst[CONTACT_OUT_PATH_SIZE + 1], out[64]; + for (int i = 0; i < 64; i++) src[i] = (uint8_t)(0x30 + i); + dst[CONTACT_OUT_PATH_SIZE] = 0xAA; + EXPECT_EQ(contactPathFrom64(dst, src, enc(1, 3)), enc(1, 3)); + EXPECT_EQ(memcmp(dst, src, 3), 0); + EXPECT_EQ(contactPathFrom64(dst, src, enc(1, 40)), OUT_PATH_UNKNOWN); + EXPECT_EQ(contactPathFromPacket(dst, src, enc(2, 9)), OUT_PATH_UNKNOWN); // 18 bytes + EXPECT_EQ(contactPathFromPacket(dst, src, enc(2, 8)), enc(2, 8)); + EXPECT_EQ(dst[CONTACT_OUT_PATH_SIZE], 0xAA); + contactPathTo64(out, dst, enc(2, 8)); + EXPECT_EQ(memcmp(out, src, 16), 0); + for (int i = 16; i < 64; i++) EXPECT_EQ(out[i], 0) << i; +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} From fbd55e1b853ae4c695b3cb15b627b3affa8437cc Mon Sep 17 00:00:00 2001 From: HAN Date: Tue, 6 Oct 2026 23:13:25 +0700 Subject: [PATCH 03/16] Optional RX_DELAY_FAST_POW: rx delay without libm pow() --- examples/companion_radio/MyMesh.cpp | 7 +++++ src/Dispatcher.cpp | 7 +++++ src/helpers/FastPow.h | 38 ++++++++++++++++++++++++++++ test/test_fast_pow/test_fast_pow.cpp | 35 +++++++++++++++++++++++++ 4 files changed, 87 insertions(+) create mode 100644 src/helpers/FastPow.h create mode 100644 test/test_fast_pow/test_fast_pow.cpp diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 55aa14b727..6461a58369 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1,4 +1,7 @@ #include "MyMesh.h" +#ifdef RX_DELAY_FAST_POW + #include +#endif #include // needed for PlatformIO #ifdef ENABLE_WIFI_INTERFACE @@ -272,7 +275,11 @@ bool MyMesh::getCADEnabled() const { int MyMesh::calcRxDelay(float score, uint32_t air_time) const { if (_prefs.rx_delay_base <= 0.0f) return 0; +#ifdef RX_DELAY_FAST_POW + return (int)((fastPowf(_prefs.rx_delay_base, 0.85f - score) - 1.0f) * air_time); // no libm pow() on small MCUs +#else return (int)((pow(_prefs.rx_delay_base, 0.85f - score) - 1.0) * air_time); +#endif } uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) { diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index c0610b7f8a..ee21f06f31 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -5,6 +5,9 @@ #endif #include +#ifdef RX_DELAY_FAST_POW + #include +#endif namespace mesh { @@ -53,7 +56,11 @@ void Dispatcher::updateTxBudget() { } int Dispatcher::calcRxDelay(float score, uint32_t air_time) const { +#ifdef RX_DELAY_FAST_POW + return (int) ((fastPow10f(0.85f - score) - 1.0f) * air_time); // no libm pow()/double maths on small MCUs +#else return (int) ((pow(10, 0.85f - score) - 1.0) * air_time); +#endif } uint32_t Dispatcher::getCADFailRetryDelay() const { diff --git a/src/helpers/FastPow.h b/src/helpers/FastPow.h new file mode 100644 index 0000000000..cf37225d09 --- /dev/null +++ b/src/helpers/FastPow.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +// Single-precision powers without libm, for calcRxDelay() on boards built with RX_DELAY_FAST_POW, where libm's +// pow() and the double-precision routines it pulls in do not fit in flash. Relative error is well under 1 %. + +// 2^y: integer power of two times a 4th-order polynomial for the fractional part +inline float fastPow2f(float y) { + if (y > 30.0f) y = 30.0f; + if (y < -30.0f) y = -30.0f; + int k = (int)y; + if ((float)k > y) k--; // floor + float f = y - (float)k; // [0, 1) + float p = 1.0f + f * (0.6931472f + f * (0.2402265f + f * (0.0555041f + f * 0.0096181f))); + return k >= 0 ? p * (float)(1u << k) : p / (float)(1u << (-k)); +} + +// log2(x) for x > 0: exponent from the IEEE-754 bits, mantissa m in [1, 2) through ln(m) = 2 atanh((m-1)/(m+1)) +inline float fastLog2f(float x) { + uint32_t bits; + memcpy(&bits, &x, 4); + int e = (int)((bits >> 23) & 0xFF) - 127; + bits = (bits & 0x007FFFFFu) | 0x3F800000u; // mantissa as a float in [1, 2) + float m; + memcpy(&m, &bits, 4); + float z = (m - 1.0f) / (m + 1.0f); // [0, 1/3) + float z2 = z * z; + float ln_m = 2.0f * z * (1.0f + z2 * (0.3333333f + z2 * (0.2f + z2 * 0.1428571f))); + return (float)e + ln_m * 1.4426950f; // 1 / ln(2) +} + +// base^x for base > 0 +inline float fastPowf(float base, float x) { return fastPow2f(x * fastLog2f(base)); } + +// 10^x +inline float fastPow10f(float x) { return fastPow2f(x * 3.3219281f); } diff --git a/test/test_fast_pow/test_fast_pow.cpp b/test/test_fast_pow/test_fast_pow.cpp new file mode 100644 index 0000000000..d3a48ba11c --- /dev/null +++ b/test/test_fast_pow/test_fast_pow.cpp @@ -0,0 +1,35 @@ +#include +#include +#include "helpers/FastPow.h" + +// calcRxDelay() uses 10^(0.85 - score); score is in [0, 1] in practice, the range below is wider on purpose +TEST(FastPow10, WithinOnePercentOfPow) { + for (int i = -300; i <= 300; i++) { + float x = i / 100.0f; + float want = powf(10.0f, x); + float got = fastPow10f(x); + EXPECT_NEAR(got / want, 1.0f, 0.01f) << "x=" << x; + } +} + +TEST(FastPow10, ExactAtIntegers) { + EXPECT_NEAR(fastPow10f(0.0f), 1.0f, 0.001f); + EXPECT_NEAR(fastPow10f(1.0f), 10.0f, 0.05f); + EXPECT_NEAR(fastPow10f(-1.0f), 0.1f, 0.0005f); +} + +// MyMesh::calcRxDelay() uses rx_delay_base^(0.85 - score) with a user-set base +TEST(FastPow, AnyBaseWithinOnePercentOfPow) { + const float bases[] = {1.05f, 1.5f, 2.0f, 5.0f, 10.0f, 20.0f}; + for (float b : bases) { + for (int i = -200; i <= 200; i++) { + float x = i / 100.0f; + EXPECT_NEAR(fastPowf(b, x) / powf(b, x), 1.0f, 0.01f) << "b=" << b << " x=" << x; + } + } +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} From 35faf61228c826c366e450539a16215ccda6ff07 Mon Sep 17 00:00:00 2001 From: HAN Date: Tue, 6 Oct 2026 23:13:48 +0700 Subject: [PATCH 04/16] Optional DISABLE_CLI_RESCUE for serial-only boards without a filesystem --- examples/companion_radio/MyMesh.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 6461a58369..0cac52f17f 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2078,9 +2078,13 @@ void MyMesh::saveContacts() { } void MyMesh::enterCLIRescue() { +#ifndef DISABLE_CLI_RESCUE _cli_rescue = true; cli_command[0] = 0; Serial.println("========= CLI Rescue ========="); +#else + // rescue CLI disabled: it prints text on Serial and needs a filesystem +#endif } bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { @@ -2218,6 +2222,7 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* } void MyMesh::checkCLIRescueCmd() { +#ifndef DISABLE_CLI_RESCUE int len = strlen(cli_command); // `cli_command` must stay NUL-terminated within its bounds. If it ever isn't, // strlen() above can return >= sizeof(cli_command) and the loop below would @@ -2390,6 +2395,9 @@ void MyMesh::checkCLIRescueCmd() { cli_command[0] = 0; // reset command buffer } +#else + // rescue CLI disabled (see enterCLIRescue) +#endif } void MyMesh::checkSerialInterface() { From 9ef36c7d4756e581827d14ecfa32d812ca345c14 Mon Sep 17 00:00:00 2001 From: HAN Date: Tue, 6 Oct 2026 23:21:33 +0700 Subject: [PATCH 05/16] Optional CONFIG_SERIALIZER_NOOP for boards that persist prefs as a binary record --- platformio.ini | 20 +++++- src/helpers/ConfigSerializer.cpp | 2 + src/helpers/ConfigSerializer.h | 36 ++++++++++ src/helpers/DynamicConfigSerializer.cpp | 2 + src/helpers/DynamicConfigSerializer.h | 21 ++++++ .../test_config_serializer_noop.cpp | 66 +++++++++++++++++++ 6 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 test/test_config_serializer_noop/test_config_serializer_noop.cpp diff --git a/platformio.ini b/platformio.ini index f296f83821..5de46bb116 100644 --- a/platformio.ini +++ b/platformio.ini @@ -167,7 +167,9 @@ build_flags = -std=c++17 -I src -I test/mocks test_build_src = yes -test_ignore = test_kiss_modem +test_ignore = + test_kiss_modem + test_config_serializer_noop build_src_filter = -<*> +<../src/Utils.cpp> @@ -177,6 +179,22 @@ build_src_filter = lib_deps = google/googletest @ 1.17.0 +[env:native_noop_config] +platform = native +test_framework = googletest +build_flags = -std=c++17 + -I src + -I test/mocks + -D CONFIG_SERIALIZER_NOOP +test_build_src = yes +test_filter = test_config_serializer_noop +build_src_filter = + -<*> + +<../src/helpers/ConfigSerializer.cpp> + +<../src/helpers/DynamicConfigSerializer.cpp> +lib_deps = + google/googletest @ 1.17.0 + [env:native_kiss_modem] platform = native test_framework = googletest diff --git a/src/helpers/ConfigSerializer.cpp b/src/helpers/ConfigSerializer.cpp index daf834b2b1..814c7554f7 100644 --- a/src/helpers/ConfigSerializer.cpp +++ b/src/helpers/ConfigSerializer.cpp @@ -1,3 +1,4 @@ +#ifndef CONFIG_SERIALIZER_NOOP #include "ConfigSerializer.h" #include // atoi/atol/atof (Arduino.h pulls this in on-device, native builds do not) @@ -333,3 +334,4 @@ void ConfigSerializer::def(const char* key, ConfigSerializer& sub_obj) { } } } +#endif // CONFIG_SERIALIZER_NOOP diff --git a/src/helpers/ConfigSerializer.h b/src/helpers/ConfigSerializer.h index e55b1b120f..9d6820a793 100644 --- a/src/helpers/ConfigSerializer.h +++ b/src/helpers/ConfigSerializer.h @@ -2,6 +2,41 @@ #include +#ifdef CONFIG_SERIALIZER_NOOP +// Boards that persist their prefs as a fixed binary record (no filesystem) build this no-op variant: same +// interface, but def() does nothing and there is no text reader/writer (saves several KB of flash and RAM). +class ConfigSerializer { + bool _dirty = false; + +protected: + ConfigSerializer() { } + + void def(const char*, char*, size_t) { } + void def(const char*, void*, size_t) { } + void def(const char*, int32_t&) { } + void def(const char*, int16_t&) { } + void def(const char*, int8_t&) { } + void def(const char*, uint32_t&) { } + void def(const char*, uint16_t&) { } + void def(const char*, uint8_t&) { } + void def(const char*, float&) { } + void def(const char*, double&) { } + void def(const char*, bool&) { } + void def(const char*, ConfigSerializer&) { } + + virtual void structure() = 0; + + void markDirty() { _dirty = true; } + +public: + bool loadSerial(Stream&) { return false; } + bool saveSerial(Stream&) { return false; } + + virtual bool isDirty() const { return _dirty; } + virtual void clearDirty() { _dirty = false; } +}; +#else + #ifndef CONFIG_MAX_DEPTH #define CONFIG_MAX_DEPTH 8 #endif @@ -74,3 +109,4 @@ class ConfigSerializer { virtual bool isDirty() const { return _dirty; } virtual void clearDirty() { _dirty = false; } }; +#endif // CONFIG_SERIALIZER_NOOP diff --git a/src/helpers/DynamicConfigSerializer.cpp b/src/helpers/DynamicConfigSerializer.cpp index b9c6c267ef..0984965b07 100644 --- a/src/helpers/DynamicConfigSerializer.cpp +++ b/src/helpers/DynamicConfigSerializer.cpp @@ -1,3 +1,4 @@ +#ifndef CONFIG_SERIALIZER_NOOP #include "DynamicConfigSerializer.h" #include @@ -95,3 +96,4 @@ void DynamicConfigSerializer::structure() { setByKeyPrv(_context->getKey(getDepth()), _context->getToken()); } } +#endif // CONFIG_SERIALIZER_NOOP diff --git a/src/helpers/DynamicConfigSerializer.h b/src/helpers/DynamicConfigSerializer.h index e5dc4c7c95..e89bd550f6 100644 --- a/src/helpers/DynamicConfigSerializer.h +++ b/src/helpers/DynamicConfigSerializer.h @@ -6,6 +6,26 @@ #define MAX_DYNAMIC_CONFG 128 #endif +#ifdef CONFIG_SERIALIZER_NOOP +// No local text store: keys go to the fallback store only. +class DynamicConfigSerializer : public ConfigSerializer, public KeyValueStore { + KeyValueStore* _fallback; + +protected: + void structure() override { } + +public: + DynamicConfigSerializer(KeyValueStore* fallback = NULL) : _fallback(fallback) { } + + bool setByKey(const char* key, const char* value) override { + if (_fallback && _fallback->setByKey(key, value)) { markDirty(); return true; } + return false; + } + bool getByKey(const char* key, char* value, size_t max_len) override { + return _fallback && _fallback->getByKey(key, value, max_len); + } +}; +#else class DynamicConfigSerializer : public ConfigSerializer, public KeyValueStore { char _config[MAX_DYNAMIC_CONFG]; KeyValueStore* _fallback; @@ -21,3 +41,4 @@ class DynamicConfigSerializer : public ConfigSerializer, public KeyValueStore { bool setByKey(const char* key, const char* value) override; bool getByKey(const char* key, char* value, size_t max_len) override; }; +#endif // CONFIG_SERIALIZER_NOOP diff --git a/test/test_config_serializer_noop/test_config_serializer_noop.cpp b/test/test_config_serializer_noop/test_config_serializer_noop.cpp new file mode 100644 index 0000000000..f901f8ef65 --- /dev/null +++ b/test/test_config_serializer_noop/test_config_serializer_noop.cpp @@ -0,0 +1,66 @@ +// Runs only in [env:native_noop_config] (CONFIG_SERIALIZER_NOOP): boards that keep prefs in a binary record. +#include +#include +#include "helpers/ConfigSerializer.h" +#include "helpers/DynamicConfigSerializer.h" + +namespace { +class Prefs : public ConfigSerializer { +public: + char name[16]; + int32_t i32 = 1; int16_t i16 = 2; int8_t i8 = 3; uint32_t u32 = 4; uint16_t u16 = 5; uint8_t u8 = 6; + float f = 7.0f; double d = 8.0; bool b = true; uint8_t blob[4] = {1, 2, 3, 4}; + void touch() { markDirty(); } +protected: + void structure() override { + def("name", name, sizeof(name)); def("blob", blob, sizeof(blob)); + def("i32", i32); def("i16", i16); def("i8", i8); def("u32", u32); def("u16", u16); def("u8", u8); + def("f", f); def("d", d); def("b", b); + } +}; + +class OneKey : public KeyValueStore { +public: + char v[8] = "old"; + bool setByKey(const char* key, const char* value) override { + if (strcmp(key, "known") != 0) return false; + strncpy(v, value, sizeof(v) - 1); + return true; + } + bool getByKey(const char* key, char* value, size_t max_len) override { + if (strcmp(key, "known") != 0) return false; + strncpy(value, v, max_len); + return true; + } +}; +} + +TEST(ConfigSerializerNoop, DirtyFlagStillWorks) { + Prefs p; + EXPECT_FALSE(p.isDirty()); + p.touch(); + EXPECT_TRUE(p.isDirty()); + p.clearDirty(); + EXPECT_FALSE(p.isDirty()); +} + +TEST(DynamicConfigSerializerNoop, OnlyTheFallbackStoresValues) { + OneKey fb; + DynamicConfigSerializer d(&fb); + char out[8]; + EXPECT_TRUE(d.setByKey("known", "new")); + EXPECT_STREQ(fb.v, "new"); + EXPECT_TRUE(d.getByKey("known", out, sizeof(out) - 1)); + EXPECT_STREQ(out, "new"); + EXPECT_FALSE(d.setByKey("other", "x")); // no local text store in this build + EXPECT_FALSE(d.getByKey("other", out, sizeof(out) - 1)); +} + +TEST(DynamicConfigSerializerNoop, HasNoTextBuffer) { + EXPECT_LE(sizeof(DynamicConfigSerializer), 4 * sizeof(void*)); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} From b10e9494badff98daea624faaa446e01f9b0342f Mon Sep 17 00:00:00 2001 From: HAN Date: Tue, 6 Oct 2026 23:28:27 +0700 Subject: [PATCH 06/16] Optional ED25519_SMALL_BASE_TABLE: ~30 KB smaller fixed-base table for 128 KB parts Keeps only the first row of the precomputed base table and computes a*B by Horner (4 doublings per radix-16 digit) with the same recoding and constant-time select. RFC 8032 vectors pass with both tables. --- lib/ed25519/ge.c | 40 +++++++++++ lib/ed25519/precomp_data.h | 9 ++- platformio.ini | 20 ++++++ .../test_ed25519_vectors.cpp | 66 +++++++++++++++++++ 4 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 test/test_ed25519_vectors/test_ed25519_vectors.cpp diff --git a/lib/ed25519/ge.c b/lib/ed25519/ge.c index 87c691bff5..c9efd05378 100644 --- a/lib/ed25519/ge.c +++ b/lib/ed25519/ge.c @@ -384,6 +384,45 @@ B is the Ed25519 base point (x,4/5) with x positive. */ void ge_scalarmult_base(ge_p3 *h, const unsigned char *a) { +#ifdef ED25519_SMALL_BASE_TABLE + /* Horner over radix-16 digits with the first table row only (4 doublings per digit). + Same recoding and the same constant-time select as below. */ + signed char e[64]; + signed char carry; + ge_p1p1 r; + ge_p2 s; + ge_precomp t; + int i; + + for (i = 0; i < 32; ++i) { + e[2 * i + 0] = (a[i] >> 0) & 15; + e[2 * i + 1] = (a[i] >> 4) & 15; + } + + /* each e[i] is between 0 and 15; e[63] is between 0 and 7 */ + carry = 0; + + for (i = 0; i < 63; ++i) { + e[i] += carry; + carry = e[i] + 8; + carry >>= 4; + e[i] -= carry << 4; + } + + e[63] += carry; + /* each e[i] is between -8 and 8; a = sum e[i] * 16^i */ + ge_p3_0(h); + + for (i = 63; i >= 0; i--) { + ge_p3_dbl(&r, h); ge_p1p1_to_p2(&s, &r); /* 2h */ + ge_p2_dbl(&r, &s); ge_p1p1_to_p2(&s, &r); /* 4h */ + ge_p2_dbl(&r, &s); ge_p1p1_to_p2(&s, &r); /* 8h */ + ge_p2_dbl(&r, &s); ge_p1p1_to_p3(h, &r); /* 16h */ + select(&t, 0, e[i]); + ge_madd(&r, h, &t); + ge_p1p1_to_p3(h, &r); + } +#else signed char e[64]; signed char carry; ge_p1p1 r; @@ -431,6 +470,7 @@ void ge_scalarmult_base(ge_p3 *h, const unsigned char *a) { ge_madd(&r, h, &t); ge_p1p1_to_p3(h, &r); } +#endif } diff --git a/lib/ed25519/precomp_data.h b/lib/ed25519/precomp_data.h index ff23986c39..92df11ad7d 100644 --- a/lib/ed25519/precomp_data.h +++ b/lib/ed25519/precomp_data.h @@ -43,7 +43,12 @@ static const ge_precomp Bi[8] = { /* base[i][j] = (j+1)*256^i*B */ -static const ge_precomp base[32][8] = { +#ifdef ED25519_SMALL_BASE_TABLE + #define ED25519_BASE_ROWS 1 /* 1B..8B only: ge_scalarmult_base() uses Horner (ge.c), saves ~30 KB */ +#else + #define ED25519_BASE_ROWS 32 +#endif +static const ge_precomp base[ED25519_BASE_ROWS][8] = { { { { 25967493, -14356035, 29566456, 3660896, -12694345, 4014787, 27544626, -11754271, -6079156, 2047605 }, @@ -86,6 +91,7 @@ static const ge_precomp base[32][8] = { { 27431194, 8222322, 16448760, -3907995, -18707002, 11938355, -32961401, -2970515, 29551813, 10109425 }, }, }, +#ifndef ED25519_SMALL_BASE_TABLE { { { -13657040, -13155431, -31283750, 11777098, 21447386, 6519384, -2378284, -1627556, 10092783, -4764171 }, @@ -1388,4 +1394,5 @@ static const ge_precomp base[32][8] = { { -20430234, 14955537, -24126347, 8124619, -5369288, -5990470, 30468147, -13900640, 18423289, 4177476 }, }, }, +#endif }; diff --git a/platformio.ini b/platformio.ini index 5de46bb116..b2536c6505 100644 --- a/platformio.ini +++ b/platformio.ini @@ -166,6 +166,8 @@ test_framework = googletest build_flags = -std=c++17 -I src -I test/mocks + -I lib/ed25519 + -D ED25519_NO_SEED test_build_src = yes test_ignore = test_kiss_modem @@ -176,6 +178,24 @@ build_src_filter = +<../src/Packet.cpp> +<../src/helpers/ConfigSerializer.cpp> +<../src/helpers/DynamicConfigSerializer.cpp> + +<../lib/ed25519/*.c> + -<../lib/ed25519/seed.c> +lib_deps = + google/googletest @ 1.17.0 + +[env:native_ed25519_small] +platform = native +test_framework = googletest +build_flags = -std=c++17 + -I lib/ed25519 + -D ED25519_SMALL_BASE_TABLE + -D ED25519_NO_SEED +test_build_src = yes +test_filter = test_ed25519_vectors +build_src_filter = + -<*> + +<../lib/ed25519/*.c> + -<../lib/ed25519/seed.c> lib_deps = google/googletest @ 1.17.0 diff --git a/test/test_ed25519_vectors/test_ed25519_vectors.cpp b/test/test_ed25519_vectors/test_ed25519_vectors.cpp new file mode 100644 index 0000000000..c983829017 --- /dev/null +++ b/test/test_ed25519_vectors/test_ed25519_vectors.cpp @@ -0,0 +1,66 @@ +// Ed25519 known-answer tests (RFC 8032 section 7.1, TEST 1 and TEST 2). Run with the full fixed-base table +// ([env:native]) and with the small one ([env:native_ed25519_small], -D ED25519_SMALL_BASE_TABLE). +#include +#include +#include "ed_25519.h" +extern "C" { +#include "ge.h" +#include "precomp_data.h" +} + +static void unhex(uint8_t* out, const char* hex) { + for (size_t i = 0; hex[2 * i]; i++) { + unsigned v; + sscanf(hex + 2 * i, "%2x", &v); + out[i] = (uint8_t)v; + } +} + +struct Vec { const char *seed, *pub, *msg, *sig; }; +static const Vec VECTORS[] = { + {"9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", + "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", "", + "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b"}, + {"4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb", + "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c", "72", + "92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00"}, +}; + +TEST(Ed25519Vectors, KeypairSignVerifyMatchRfc8032) { + for (const Vec& v : VECTORS) { + uint8_t seed[32], pub_want[32], sig_want[64], msg[8], pub[32], prv[64], sig[64]; + size_t msg_len = strlen(v.msg) / 2; + unhex(seed, v.seed); unhex(pub_want, v.pub); unhex(sig_want, v.sig); unhex(msg, v.msg); + ed25519_create_keypair(pub, prv, seed); + EXPECT_EQ(memcmp(pub, pub_want, 32), 0); + uint8_t derived[32]; + ed25519_derive_pub(derived, prv); + EXPECT_EQ(memcmp(derived, pub_want, 32), 0); + ed25519_sign(sig, msg, msg_len, pub, prv); + EXPECT_EQ(memcmp(sig, sig_want, 64), 0); + EXPECT_EQ(ed25519_verify(sig, msg, msg_len, pub), 1); + sig[5] ^= 0x04; + EXPECT_EQ(ed25519_verify(sig, msg, msg_len, pub), 0); + } +} + +TEST(Ed25519Vectors, KeyExchangeIsSymmetric) { + uint8_t sa[32], sb[32], pa[32], pb[32], ka[64], kb[64], s1[32], s2[32]; + unhex(sa, VECTORS[0].seed); unhex(sb, VECTORS[1].seed); + ed25519_create_keypair(pa, ka, sa); + ed25519_create_keypair(pb, kb, sb); + ed25519_key_exchange(s1, pb, ka); + ed25519_key_exchange(s2, pa, kb); + EXPECT_EQ(memcmp(s1, s2, 32), 0); +} + +#ifdef ED25519_SMALL_BASE_TABLE +TEST(Ed25519Vectors, SmallBuildKeepsOnlyOneTableRow) { + EXPECT_EQ(ED25519_BASE_ROWS, 1); +} +#endif + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} From 27ac8863e9f13701e03e178dab7ec897929a3a60 Mon Sep 17 00:00:00 2001 From: HAN Date: Wed, 7 Oct 2026 00:06:49 +0700 Subject: [PATCH 07/16] Optional SERIAL_FRAME_STRICT: drop oversize frames and resync after a stalled frame Without it, a frame longer than MAX_FRAME_SIZE is executed truncated, and a frame cut short by the sender swallows the start of the next one. Worth enabling by default; kept behind a flag here so other boards build unchanged. --- src/helpers/ArduinoSerialInterface.cpp | 11 ++++ src/helpers/ArduinoSerialInterface.h | 3 + .../test_serial_frame_strict.cpp | 66 +++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 test/test_serial_frame_strict/test_serial_frame_strict.cpp diff --git a/src/helpers/ArduinoSerialInterface.cpp b/src/helpers/ArduinoSerialInterface.cpp index a01fa5866f..4a80e3faba 100644 --- a/src/helpers/ArduinoSerialInterface.cpp +++ b/src/helpers/ArduinoSerialInterface.cpp @@ -37,9 +37,16 @@ size_t ArduinoSerialInterface::writeFrame(const uint8_t src[], size_t len) { } size_t ArduinoSerialInterface::checkRecvFrame(uint8_t dest[]) { +#ifdef SERIAL_FRAME_STRICT + // a frame that stalls mid-way (truncated by the sender) is abandoned instead of swallowing the next frame + if (_state != RECV_STATE_IDLE && (millis() - _last_byte_ms) > 100) _state = RECV_STATE_IDLE; +#endif while (_serial->available()) { int c = _serial->read(); if (c < 0) break; +#ifdef SERIAL_FRAME_STRICT + _last_byte_ms = millis(); +#endif switch (_state) { case RECV_STATE_IDLE: @@ -62,7 +69,11 @@ size_t ArduinoSerialInterface::checkRecvFrame(uint8_t dest[]) { } rx_len++; if (rx_len >= _frame_len) { // received a complete frame? +#ifdef SERIAL_FRAME_STRICT + if (_frame_len > MAX_FRAME_SIZE) { _state = RECV_STATE_IDLE; continue; } // oversize: drop, do not execute +#else if (_frame_len > MAX_FRAME_SIZE) _frame_len = MAX_FRAME_SIZE; // truncate +#endif memcpy(dest, rx_buf, _frame_len); _state = RECV_STATE_IDLE; // reset state, for next frame return _frame_len; diff --git a/src/helpers/ArduinoSerialInterface.h b/src/helpers/ArduinoSerialInterface.h index c4086353aa..10ab0890b3 100644 --- a/src/helpers/ArduinoSerialInterface.h +++ b/src/helpers/ArduinoSerialInterface.h @@ -8,6 +8,9 @@ class ArduinoSerialInterface : public BaseSerialInterface { uint8_t _state; uint16_t _frame_len; uint16_t rx_len; +#ifdef SERIAL_FRAME_STRICT + unsigned long _last_byte_ms = 0; +#endif Stream* _serial; uint8_t rx_buf[MAX_FRAME_SIZE]; diff --git a/test/test_serial_frame_strict/test_serial_frame_strict.cpp b/test/test_serial_frame_strict/test_serial_frame_strict.cpp new file mode 100644 index 0000000000..641e2a7652 --- /dev/null +++ b/test/test_serial_frame_strict/test_serial_frame_strict.cpp @@ -0,0 +1,66 @@ +// ArduinoSerialInterface built with SERIAL_FRAME_STRICT: a stalled (truncated) frame must not swallow the next frame, +// and a frame longer than MAX_FRAME_SIZE must be dropped, not executed truncated. +#define SERIAL_FRAME_STRICT +#include +#include +#include "../../src/helpers/ArduinoSerialInterface.cpp" + +namespace { +class FakeSerial : public Stream { +public: + std::vector in; + size_t pos = 0; + int available() override { return (int)(in.size() - pos); } + int read() override { return pos < in.size() ? in[pos++] : -1; } + void push(const std::vector& b) { in.insert(in.end(), b.begin(), b.end()); } +}; + +std::vector frame(const std::vector& payload) { + std::vector f = {'<', (uint8_t)(payload.size() & 0xFF), (uint8_t)(payload.size() >> 8)}; + f.insert(f.end(), payload.begin(), payload.end()); + return f; +} +} + +TEST(SerialFrameStrict, CompleteFrameIsDelivered) { + FakeSerial s; + ArduinoSerialInterface sif; + sif.begin(s); + s.push(frame({22, 3})); + uint8_t out[MAX_FRAME_SIZE]; + EXPECT_EQ(sif.checkRecvFrame(out), 2u); + EXPECT_EQ(out[0], 22); +} + +TEST(SerialFrameStrict, StalledFrameDoesNotSwallowTheNextOne) { + FakeSerial s; + ArduinoSerialInterface sif; + sif.begin(s); + uint8_t out[MAX_FRAME_SIZE]; + g_mock_millis = 1000; + s.push({'<', 10, 0, 1, 2, 3}); // header says 10 bytes, only 3 arrive + EXPECT_EQ(sif.checkRecvFrame(out), 0u); + g_mock_millis = 1200; // > 100 ms of silence: the partial frame is abandoned + s.push(frame({22, 3})); + EXPECT_EQ(sif.checkRecvFrame(out), 2u); + EXPECT_EQ(out[0], 22); +} + +TEST(SerialFrameStrict, OversizeFrameIsDroppedNotExecuted) { + FakeSerial s; + ArduinoSerialInterface sif; + sif.begin(s); + uint8_t out[MAX_FRAME_SIZE]; + std::vector big(MAX_FRAME_SIZE + 20, 0x41); + big[0] = 22; + s.push(frame(big)); + EXPECT_EQ(sif.checkRecvFrame(out), 0u); // dropped + s.push(frame({5})); + EXPECT_EQ(sif.checkRecvFrame(out), 1u); // and the next frame still parses + EXPECT_EQ(out[0], 5); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} From c2e208a3d9a989edfdad1a0735e04cb772ad5cf3 Mon Sep 17 00:00:00 2001 From: HAN Date: Wed, 7 Oct 2026 00:06:49 +0700 Subject: [PATCH 08/16] Optional DISABLE_COMPANION_CLI: leave the text CLI out on boards short of flash Radio settings still go through the binary commands (CMD_SET_RADIO_PARAMS and friends). --- examples/companion_radio/MyMesh.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 0cac52f17f..20141fe936 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2088,6 +2088,11 @@ void MyMesh::enterCLIRescue() { } bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { +#ifdef DISABLE_COMPANION_CLI + // text CLI not built (flash): radio settings still work through CMD_SET_RADIO_PARAMS and friends + (void)command; (void)sender_timestamp; (void)reply; + return false; +#else while (*command == ' ') command++; // skip leading spaces if (strlen(command) > 4 && command[2] == '|') { // optional prefix (for companion radio CLI) @@ -2219,6 +2224,7 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, char* } return false; // not handled +#endif } void MyMesh::checkCLIRescueCmd() { From cba7d28db6ac720a54d66575275bb6e408a8fd65 Mon Sep 17 00:00:00 2001 From: HAN Date: Wed, 7 Oct 2026 00:06:50 +0700 Subject: [PATCH 09/16] Add ASR650x platform helpers (radio, RNG seed, RTC, flash-row store, crypto stack) - CubeCellRadio: mesh::Radio on the Heltec SX1262 driver; DIO1 runs callbacks in interrupt context, so main-context radio calls detach DIO1 (RadioLock) and the RX buffer is copied in a critical section - FlashRecord/FlashRecordStore: 160-byte identity + prefs record, A/B over two SFLASH user rows, CRC-32 - BigStack + IDENTITY_CRYPTO_ON_BIG_STACK: Ed25519 runs on a heap-allocated stack (the main stack is fixed at 2 KB); Sbrk lets the heap grow up to the stack region - TxGate: the node never transmits until a private key has been imported - SmallStrtod: decimal-only strtod/strtof/atof (newlib's pulls in ~7 KB) - pure helpers with host tests: NMEA parser (integer only), GPS power policy, status lines, 5x7 font, debounced button, radio airtime/score --- src/Identity.cpp | 6 + src/helpers/IdentityStore.h | 4 + src/helpers/asr650x/BigStack.cpp | 40 +++ src/helpers/asr650x/BigStack.h | 10 + src/helpers/asr650x/Button.h | 48 ++++ src/helpers/asr650x/CubeCellRTC.h | 12 + src/helpers/asr650x/CubeCellRadio.cpp | 205 ++++++++++++++++ src/helpers/asr650x/CubeCellRadio.h | 63 +++++ src/helpers/asr650x/EpochClock.h | 37 +++ src/helpers/asr650x/FlashRecord.h | 77 ++++++ src/helpers/asr650x/FlashRecordStore.h | 77 ++++++ src/helpers/asr650x/FlashRowBackend.h | 23 ++ src/helpers/asr650x/Font5x7.h | 40 +++ src/helpers/asr650x/GpsPolicy.h | 63 +++++ src/helpers/asr650x/IdentityCrypto.cpp | 45 ++++ src/helpers/asr650x/IdentityCrypto.h | 19 ++ src/helpers/asr650x/NmeaParser.h | 229 ++++++++++++++++++ src/helpers/asr650x/OledText.h | 21 ++ src/helpers/asr650x/RadioParams.h | 67 +++++ src/helpers/asr650x/Sbrk.cpp | 25 ++ src/helpers/asr650x/SmallStrtod.cpp | 10 + src/helpers/asr650x/SmallStrtod.h | 53 ++++ src/helpers/asr650x/StatusLines.h | 84 +++++++ src/helpers/asr650x/TxGate.cpp | 6 + src/helpers/asr650x/TxGate.h | 6 + .../test_asr650x_button.cpp | 58 +++++ .../test_asr650x_epoch_clock.cpp | 28 +++ .../test_asr650x_gps_policy.cpp | 116 +++++++++ test/test_asr650x_nmea/test_asr650x_nmea.cpp | 161 ++++++++++++ .../test_asr650x_oled_text.cpp | 48 ++++ .../test_asr650x_persist.cpp | 74 ++++++ .../test_asr650x_persist_store.cpp | 100 ++++++++ .../test_asr650x_radio_params.cpp | 58 +++++ .../test_asr650x_status.cpp | 81 +++++++ .../test_asr650x_strtod.cpp | 41 ++++ 35 files changed, 2035 insertions(+) create mode 100644 src/helpers/asr650x/BigStack.cpp create mode 100644 src/helpers/asr650x/BigStack.h create mode 100644 src/helpers/asr650x/Button.h create mode 100644 src/helpers/asr650x/CubeCellRTC.h create mode 100644 src/helpers/asr650x/CubeCellRadio.cpp create mode 100644 src/helpers/asr650x/CubeCellRadio.h create mode 100644 src/helpers/asr650x/EpochClock.h create mode 100644 src/helpers/asr650x/FlashRecord.h create mode 100644 src/helpers/asr650x/FlashRecordStore.h create mode 100644 src/helpers/asr650x/FlashRowBackend.h create mode 100644 src/helpers/asr650x/Font5x7.h create mode 100644 src/helpers/asr650x/GpsPolicy.h create mode 100644 src/helpers/asr650x/IdentityCrypto.cpp create mode 100644 src/helpers/asr650x/IdentityCrypto.h create mode 100644 src/helpers/asr650x/NmeaParser.h create mode 100644 src/helpers/asr650x/OledText.h create mode 100644 src/helpers/asr650x/RadioParams.h create mode 100644 src/helpers/asr650x/Sbrk.cpp create mode 100644 src/helpers/asr650x/SmallStrtod.cpp create mode 100644 src/helpers/asr650x/SmallStrtod.h create mode 100644 src/helpers/asr650x/StatusLines.h create mode 100644 src/helpers/asr650x/TxGate.cpp create mode 100644 src/helpers/asr650x/TxGate.h create mode 100644 test/test_asr650x_button/test_asr650x_button.cpp create mode 100644 test/test_asr650x_epoch_clock/test_asr650x_epoch_clock.cpp create mode 100644 test/test_asr650x_gps_policy/test_asr650x_gps_policy.cpp create mode 100644 test/test_asr650x_nmea/test_asr650x_nmea.cpp create mode 100644 test/test_asr650x_oled_text/test_asr650x_oled_text.cpp create mode 100644 test/test_asr650x_persist/test_asr650x_persist.cpp create mode 100644 test/test_asr650x_persist_store/test_asr650x_persist_store.cpp create mode 100644 test/test_asr650x_radio_params/test_asr650x_radio_params.cpp create mode 100644 test/test_asr650x_status/test_asr650x_status.cpp create mode 100644 test/test_asr650x_strtod/test_asr650x_strtod.cpp diff --git a/src/Identity.cpp b/src/Identity.cpp index 51a01ae71d..ec25756470 100644 --- a/src/Identity.cpp +++ b/src/Identity.cpp @@ -4,6 +4,10 @@ #include #include +#ifdef IDENTITY_CRYPTO_ON_BIG_STACK +#include // routes ed25519_* through a larger heap-allocated stack +#endif + #ifdef USE_CC310_HW_CRYPTO #include #include "nrf_cc310/include/crys_ec_edw_api.h" @@ -31,6 +35,8 @@ bool Identity::verify(const uint8_t* sig, const uint8_t* message, int msg_len) c (uint8_t*)pub_key, CRYS_ECEDW_MOD_SIZE_IN_BYTES, (uint8_t*)message, (size_t)msg_len, &cc310_tmp); return rc == CRYS_OK; +#elif defined(IDENTITY_CRYPTO_ON_BIG_STACK) + return asr650x_ed25519_verify(sig, this->pub_key, message, msg_len); #elif 0 // NOTE: memory corruption bug was found in this function!! return ed25519_verify(sig, message, msg_len, pub_key); diff --git a/src/helpers/IdentityStore.h b/src/helpers/IdentityStore.h index d0d7ee457e..2d9d2a43dd 100644 --- a/src/helpers/IdentityStore.h +++ b/src/helpers/IdentityStore.h @@ -1,5 +1,7 @@ #pragma once +#if !defined(ASR650X_PLATFORM) // ASR650x boards have no filesystem (identity is kept in SFLASH rows) + #if defined(ESP32) || defined(RP2040_PLATFORM) #include #define FILESYSTEM fs::FS @@ -24,3 +26,5 @@ class IdentityStore { bool save(const char *name, const mesh::LocalIdentity& id); bool save(const char *name, const mesh::LocalIdentity& id, const char display_name[]); }; + +#endif // !ASR650X_PLATFORM diff --git a/src/helpers/asr650x/BigStack.cpp b/src/helpers/asr650x/BigStack.cpp new file mode 100644 index 0000000000..bfc38e97a1 --- /dev/null +++ b/src/helpers/asr650x/BigStack.cpp @@ -0,0 +1,40 @@ +#include "BigStack.h" +#include +#include + +static uint8_t* g_stack = 0; +static size_t g_size = 0; + +bool asr650x_bigstack_init(size_t bytes) { + if (g_stack) return true; + g_stack = (uint8_t*)malloc(bytes); + if (!g_stack) return false; + g_size = bytes; + memset(g_stack, 0xA5, bytes); + return true; +} + +bool asr650x_bigstack_ready() { return g_stack != 0; } + +size_t asr650x_bigstack_highwater() { + if (!g_stack) return 0; + size_t i = 0; + while (i < g_size && g_stack[i] == 0xA5) i++; + return g_size - i; +} + +void asr650x_call_on_big_stack(void (*fn)(void*), void* ctx) { + if (!g_stack) { fn(ctx); return; } // not initialised: stay on the current stack + uint32_t top = ((uint32_t)(g_stack + g_size)) & ~7u; // 8-byte aligned top of stack + register void* r0 asm("r0") = ctx; + register void (*r1)(void*) asm("r1") = fn; + register uint32_t r2 asm("r2") = top; + asm volatile( + "mov r4, sp\n\t" + "mov sp, r2\n\t" + "blx r1\n\t" + "mov sp, r4\n\t" + : "+r"(r0), "+r"(r1), "+r"(r2) + : + : "r3", "r4", "r12", "lr", "memory", "cc"); +} diff --git a/src/helpers/asr650x/BigStack.h b/src/helpers/asr650x/BigStack.h new file mode 100644 index 0000000000..be729851a6 --- /dev/null +++ b/src/helpers/asr650x/BigStack.h @@ -0,0 +1,10 @@ +#pragma once +#include +#include + +// Runs memory-hungry crypto on a separate stack taken from the (mostly empty) heap, because the +// linker script fixes the main stack at 2 KB (M0 report, R9). +bool asr650x_bigstack_init(size_t bytes); +bool asr650x_bigstack_ready(); // false if the heap could not provide the stack +size_t asr650x_bigstack_highwater(); // bytes of the big stack ever used +void asr650x_call_on_big_stack(void (*fn)(void*), void* ctx); diff --git a/src/helpers/asr650x/Button.h b/src/helpers/asr650x/Button.h new file mode 100644 index 0000000000..c1655b3745 --- /dev/null +++ b/src/helpers/asr650x/Button.h @@ -0,0 +1,48 @@ +#pragma once +#include + +namespace asr650x { + +// Debounced press detector for the USER key (active LOW, shared with the battery divider control). +class Button { + static const uint32_t DEBOUNCE_MS = 30; + bool _down; // raw level seen at the last update + bool _armed; // a press may start (the key was seen released after the last hold-off/press) + bool _fired; + uint32_t _t_down; + uint32_t _holdoff_until; + bool _holdoff_active; + + static bool before(uint32_t a, uint32_t b) { return (int32_t)(a - b) < 0; } // wrap-safe a < b + +public: + Button() : _down(false), _armed(true), _fired(false), _t_down(0), _holdoff_until(0), _holdoff_active(false) {} + + void holdoff(uint32_t now_ms, uint32_t ms) { // e.g. right after getBatteryVoltage() released the pin + _holdoff_until = now_ms + ms; + _holdoff_active = true; + _armed = false; // anything pressed before/while held off must be released first + _down = false; + _fired = false; + } + + // true exactly once per accepted press + bool update(bool pressed, uint32_t now_ms) { + if (_holdoff_active) { + if (before(now_ms, _holdoff_until)) return false; + _holdoff_active = false; + } + if (!pressed) { + _armed = true; + _down = false; + _fired = false; + return false; + } + if (!_armed) return false; // still the same (or a held-off) press + if (!_down) { _down = true; _t_down = now_ms; return false; } + if (!_fired && (uint32_t)(now_ms - _t_down) >= DEBOUNCE_MS) { _fired = true; _armed = false; return true; } + return false; + } +}; + +} // namespace asr650x diff --git a/src/helpers/asr650x/CubeCellRTC.h b/src/helpers/asr650x/CubeCellRTC.h new file mode 100644 index 0000000000..ba463e3e92 --- /dev/null +++ b/src/helpers/asr650x/CubeCellRTC.h @@ -0,0 +1,12 @@ +#pragma once +#include +#include +#include "EpochClock.h" + +class CubeCellRTC : public mesh::RTCClock { + asr650x::EpochClock _clock; +public: + uint32_t getCurrentTime() override { return _clock.now(millis()); } + void setCurrentTime(uint32_t t) override { _clock.set(t, millis()); } + bool synced() const { return _clock.synced(); } +}; diff --git a/src/helpers/asr650x/CubeCellRadio.cpp b/src/helpers/asr650x/CubeCellRadio.cpp new file mode 100644 index 0000000000..c898147f53 --- /dev/null +++ b/src/helpers/asr650x/CubeCellRadio.cpp @@ -0,0 +1,205 @@ +#include "CubeCellRadio.h" +#include "RadioParams.h" +#include "TxGate.h" +#include "LoRaWan_102.h" // LoraWan102 library headers only: ::Radio, RadioEvents_t, MODEM_LORA (LoRaWan_APP.h would also link the LoRa/AT/display libraries) +extern "C" { // these board headers have no C++ guards; the functions are C +#include "gpio.h" // GpioSetInterrupt / GpioRemoveInterrupt / GpioRead +#include "sx126x-board.h" // extern SX126x (DIO1 pin) +} +#include + +extern "C" { +void RadioOnDioIrq(void); // radio.c: DIO1 handler (sets IrqFired and runs RadioIrqProcess) +unsigned char CyEnterCriticalSection(void); // CyLib.h +void CyExitCriticalSection(unsigned char saved); +} + +static RadioEvents_t g_events; +static CubeCellRadio* g_self = 0; + +static void cb_rx_done(uint8_t* p, uint16_t n, int16_t rssi, int8_t snr) { if (g_self) g_self->handleRxDone(p, n, rssi, snr); } +static void cb_rx_err() { if (g_self) g_self->handleRxError(); } +static void cb_tx_done() { if (g_self) g_self->handleTxDone(false); } +static void cb_tx_timeout() { if (g_self) g_self->handleTxDone(true); } + +// While main-context code talks to the SX1262 over SPI the DIO1 handler must not run (it would issue its own SPI +// commands in the middle of ours). The handler is detached for the duration; on release it is re-attached and, if +// DIO1 is already high (an edge arrived meanwhile and was not latched), serviced right away from this context. +struct RadioLock { + RadioLock() { GpioRemoveInterrupt(&SX126x.DIO1); } + ~RadioLock() { + GpioSetInterrupt(&SX126x.DIO1, RISING, IRQ_HIGH_PRIORITY, RadioOnDioIrq); + if (GpioRead(&SX126x.DIO1)) RadioOnDioIrq(); + } +}; + +struct CriticalSection { + unsigned char saved; + CriticalSection() : saved(CyEnterCriticalSection()) {} + ~CriticalSection() { CyExitCriticalSection(saved); } +}; + +CubeCellRadio::CubeCellRadio() + : _state(ST_IDLE), _rx_ready(false), _tx_done(false), _tx_timed_out(false), _rx_len(0), _last_rssi(0), _last_snr(0), + _freq(920.25f), _bw(62.5f), _sf(8), _cr(5), _pwr(14), _boost(true), _inited(false), _noise_floor(0), + _last_nf_sample(0), _n_recv(0), _n_sent(0), _n_err(0), _n_dropped(0), _n_txto(0), _n_blocked(0) {} + +void CubeCellRadio::init() { + if (_inited) return; + g_self = this; + g_events.RxDone = cb_rx_done; + g_events.RxError = cb_rx_err; + g_events.RxTimeout = cb_rx_err; + g_events.TxDone = cb_tx_done; + g_events.TxTimeout = cb_tx_timeout; + ::Radio.Init(&g_events); + _inited = true; +} + +uint32_t CubeCellRadio::random32() { + init(); + return ::Radio.Random(); +} + +uint8_t asr650x_boot_pool[4]; // copy of the seed fold (watch builds print it to compare boots) + +uint32_t CubeCellRadio::getRngSeed() { + uint32_t fold = 0; + for (int i = 0; i < 8; i++) { + uint32_t v = random32(); + fold ^= (v << i) | (v >> (32 - i)); + } + memcpy(asr650x_boot_pool, &fold, sizeof(asr650x_boot_pool)); + return fold; +} + +static uint32_t freq_hz(float mhz) { return ((uint32_t)(mhz * 1000.0f + 0.5f)) * 1000UL; } + +void CubeCellRadio::applyRx() { + ::Radio.SetPublicNetwork(false); // private sync word 0x1424 (MeshCore) + ::Radio.SetChannel(freq_hz(_freq)); + ::Radio.SetRxConfig(MODEM_LORA, asr650x::bw_index(_bw), asr650x::sf_clamp(_sf), asr650x::cr_index(_cr), 0, + asr650x::preamble_for_sf(asr650x::sf_clamp(_sf)), 0, false, 0, true, 0, 0, false, true); +} + +void CubeCellRadio::applyTx(uint32_t timeout_ms) { + ::Radio.SetPublicNetwork(false); + ::Radio.SetChannel(freq_hz(_freq)); + ::Radio.SetTxConfig(MODEM_LORA, _pwr, 0, asr650x::bw_index(_bw), asr650x::sf_clamp(_sf), asr650x::cr_index(_cr), + asr650x::preamble_for_sf(asr650x::sf_clamp(_sf)), false, true, 0, 0, false, timeout_ms); +} + +void CubeCellRadio::startRx() { + { + RadioLock lock; + applyRx(); + ::Radio.Rx(0); + } + _state = ST_RX; +} + +void CubeCellRadio::begin() { + init(); + _rx_ready = false; + _tx_done = false; + startRx(); +} + +void CubeCellRadio::loop() { + if (GpioRead(&SX126x.DIO1)) { RadioLock service; } // lost/late DIO1 edge: service it from here + if (_state == ST_IDLE && !_rx_ready) startRx(); + if (_state == ST_RX && !_rx_ready) { // slow noise-floor estimate while idle in RX + uint32_t now = millis(); + if (now - _last_nf_sample >= 100) { + _last_nf_sample = now; + int16_t r; + { + RadioLock lock; + r = ::Radio.Rssi(MODEM_LORA); + } + if (_noise_floor == 0) _noise_floor = r; + else if (r < _noise_floor + 14) _noise_floor = (int16_t)((_noise_floor * 15 + r) / 16); + } + } +} + +int CubeCellRadio::recvRaw(uint8_t* bytes, int sz) { + int len = 0; + if (_rx_ready) { + CriticalSection cs; // the callback (interrupt) may overwrite the buffer + len = _rx_len; + if (len > sz) len = sz; + if (len < 0) len = 0; + memcpy(bytes, _rx_buf, len); + _rx_ready = false; + if (len > 0) _n_recv++; + _state = ST_IDLE; // restart RX below + } + if (_state == ST_IDLE) startRx(); + return len; +} + +uint32_t CubeCellRadio::getEstAirtimeFor(int len_bytes) { + return asr650x::airtime_ms(len_bytes, _bw, _sf, _cr, asr650x::preamble_for_sf(asr650x::sf_clamp(_sf))); +} + +float CubeCellRadio::packetScore(float snr, int packet_len) { return asr650x::packet_score(snr, _sf, packet_len); } + +bool CubeCellRadio::startSendRaw(const uint8_t* bytes, int len) { + if (!asr650x_tx_allowed()) { _n_blocked++; return false; } // never transmit as the unprovisioned placeholder + if (len <= 0 || len > 255) return false; + uint32_t timeout = getEstAirtimeFor(len) * 2 + 500; // the driver's timer must outlast the real air time + _tx_done = false; + _tx_timed_out = false; + _state = ST_TX; + { + RadioLock lock; + applyTx(timeout); + ::Radio.Send((uint8_t*)bytes, (uint8_t)len); + } + return true; +} + +bool CubeCellRadio::isSendComplete() { + if (_state == ST_TX && _tx_done) { + _tx_done = false; + if (!_tx_timed_out) _n_sent++; // a timed-out send is not a completed send + return true; + } + return false; +} + +void CubeCellRadio::onSendFinished() { _state = ST_IDLE; } // loop()/recvRaw() restart RX + +void CubeCellRadio::setParams(float freq_mhz, float bw_khz, uint8_t sf, uint8_t cr) { + _freq = freq_mhz; + _bw = asr650x::bw_value(asr650x::bw_index(bw_khz)); // the bandwidth the chip really uses + _sf = asr650x::sf_clamp(sf); + _cr = (uint8_t)(asr650x::cr_index(cr) + 4); + if (_inited && _state == ST_RX) _state = ST_IDLE; // re-apply on the next loop() +} + +void CubeCellRadio::setTxPower(int8_t dbm) { _pwr = dbm < -9 ? -9 : (dbm > 22 ? 22 : dbm); } + +void CubeCellRadio::handleRxDone(uint8_t* payload, uint16_t size, int16_t rssi, int8_t snr) { + if (_state == ST_TX) return; // not expected while transmitting + if (_rx_ready) { _n_dropped++; return; } // previous packet not consumed yet: drop the new one, counted + if (size > sizeof(_rx_buf)) size = sizeof(_rx_buf); + memcpy(_rx_buf, payload, size); + _rx_len = size; + _last_rssi = rssi; + _last_snr = snr; + _rx_ready = true; + _state = ST_IDLE; +} + +void CubeCellRadio::handleRxError() { + if (_state == ST_TX) return; + _n_err++; + _state = ST_IDLE; +} + +void CubeCellRadio::handleTxDone(bool timeout) { + if (timeout) { _n_txto++; _tx_timed_out = true; } + _tx_done = true; // a timed-out send still ends the TX state +} diff --git a/src/helpers/asr650x/CubeCellRadio.h b/src/helpers/asr650x/CubeCellRadio.h new file mode 100644 index 0000000000..1d54e9ca8b --- /dev/null +++ b/src/helpers/asr650x/CubeCellRadio.h @@ -0,0 +1,63 @@ +#pragma once +#include + +// SX1262 driver for the ASR6502 on top of the Heltec radio API (::Radio.*). The Heltec header is only included in +// CubeCellRadio.cpp. State machine: RX (continuous) <-> TX; every RxDone/RxError/TxDone/TxTimeout returns to RX. +// +// Threading: the Heltec driver runs RxDone/RxError/TxDone (DIO1 interrupt) and TxTimeout (timer interrupt) INSIDE +// interrupts. Fields written by the callbacks are volatile and are read/copied inside a critical section; every +// main-context ::Radio.* call runs with the DIO1 interrupt detached (RadioLock in the .cpp). +class CubeCellRadio : public mesh::Radio { + enum { ST_IDLE = 0, ST_RX = 1, ST_TX = 2 }; + volatile uint8_t _state; + volatile bool _rx_ready, _tx_done, _tx_timed_out; + uint8_t _rx_buf[255]; + volatile uint16_t _rx_len; + volatile int16_t _last_rssi; + volatile int8_t _last_snr; + float _freq, _bw; + uint8_t _sf, _cr; + int8_t _pwr; + bool _boost, _inited; + int16_t _noise_floor; + uint32_t _last_nf_sample; + uint32_t _n_recv, _n_sent; + volatile uint32_t _n_err, _n_dropped, _n_txto; + uint32_t _n_blocked; + + void applyRx(); + void applyTx(uint32_t timeout_ms); + void startRx(); + +public: + CubeCellRadio(); + void init(); // ::Radio.Init (idempotent) + uint32_t random32(); // ::Radio.Random(); only before begin() + uint32_t getRngSeed(); // fold of 8 Radio.Random() values (wideband RSSI noise); before begin() + void begin() override; + void loop() override; + int recvRaw(uint8_t* bytes, int sz) override; + uint32_t getEstAirtimeFor(int len_bytes) override; + float packetScore(float snr, int packet_len) override; + bool startSendRaw(const uint8_t* bytes, int len) override; + bool isSendComplete() override; + void onSendFinished() override; + bool isInRecvMode() const override { return _state == ST_RX; } + int getNoiseFloor() const override { return _noise_floor; } + float getLastRSSI() const override { return _last_rssi; } + float getLastSNR() const override { return _last_snr; } + + void setParams(float freq_mhz, float bw_khz, uint8_t sf, uint8_t cr); + void setTxPower(int8_t dbm); + bool setRxBoostedGainMode(bool en) { _boost = en; return false; } // not supported: Radio.Rx() is used, not RxBoosted + bool getRxBoostedGainMode() const { return _boost; } + uint32_t getPacketsRecv() const { return _n_recv; } + uint32_t getPacketsSent() const { return _n_sent; } + uint32_t getPacketsRecvErrors() const { return _n_err + _n_dropped; } // CRC/header errors + packets lost to a busy buffer + uint32_t getPacketsBlocked() const { return _n_blocked; } + + // called from the static Heltec callbacks (interrupt context) + void handleRxDone(uint8_t* payload, uint16_t size, int16_t rssi, int8_t snr); + void handleRxError(); + void handleTxDone(bool timeout); +}; diff --git a/src/helpers/asr650x/EpochClock.h b/src/helpers/asr650x/EpochClock.h new file mode 100644 index 0000000000..b16849d349 --- /dev/null +++ b/src/helpers/asr650x/EpochClock.h @@ -0,0 +1,37 @@ +#pragma once +#include + +namespace asr650x { + +// Wall clock = epoch + accumulated monotonic milliseconds. Lost on reset by design (spec §5). +// now() must be called at least once every ~49 days so a 32-bit millis() wrap is seen once. +class EpochClock { + uint32_t _epoch; + uint32_t _last_ms; + uint64_t _rem_ms; // sub-second remainder, < 1000 after each now() + bool _synced; // true once set() was called (by the app or by GPS) + +public: + explicit EpochClock(uint32_t epoch_at_boot = 1735689600u) // 2025-01-01 until the app sets time + : _epoch(epoch_at_boot), _last_ms(0), _rem_ms(0), _synced(false) {} + + uint32_t now(uint32_t ms) { + uint32_t delta = ms - _last_ms; // unsigned arithmetic handles one wrap + _last_ms = ms; + _rem_ms += delta; + _epoch += (uint32_t)(_rem_ms / 1000); + _rem_ms %= 1000; + return _epoch; + } + + void set(uint32_t epoch, uint32_t ms) { + _epoch = epoch; + _last_ms = ms; + _rem_ms = 0; + _synced = true; + } + + bool synced() const { return _synced; } +}; + +} // namespace asr650x diff --git a/src/helpers/asr650x/FlashRecord.h b/src/helpers/asr650x/FlashRecord.h new file mode 100644 index 0000000000..9e75092fb9 --- /dev/null +++ b/src/helpers/asr650x/FlashRecord.h @@ -0,0 +1,77 @@ +#pragma once +#include +#include +#include + +// 160-byte on-flash record (EEPROM emulation, SFLASH). Explicit little-endian layout: +// 0..3 "KTM1" | 4 version(1) | 5 flags(bit0 identity, bit1 prefs) | 6..7 0 +// 8..71 prv | 72..103 pub | 104..135 name | 136..139 freq | 140..143 bw +// 144 sf | 145 cr | 146 tx_power | 147 gps_off | 148..151 seq | 152..155 gps_interval | 156..159 CRC-32 of bytes 0..155 +namespace asr650x { + +const size_t PERSIST_SIZE = 160; + +struct PersistState { + uint32_t seq; // record generation, newest valid row wins (see FlashRecordStore.h) + bool has_identity; + uint8_t prv[64]; + uint8_t pub[32]; + bool has_prefs; + char node_name[32]; + float freq, bw; + uint8_t sf, cr; + int8_t tx_power_dbm; + bool gps_off; // 1 = the user switched GPS off (0 = default: on, also for older records) + uint32_t gps_interval; // seconds between GPS refreshes, 0 = only at boot / on request +}; + +inline uint32_t crc32(const uint8_t* d, size_t n) { + uint32_t c = 0xFFFFFFFFu; + for (size_t i = 0; i < n; i++) { + c ^= d[i]; + for (int k = 0; k < 8; k++) c = (c >> 1) ^ (0xEDB88320u & (0u - (c & 1u))); + } + return ~c; +} + +inline void persist_encode(const PersistState& s, uint8_t out[PERSIST_SIZE]) { + memset(out, 0, PERSIST_SIZE); + memcpy(out, "KTM1", 4); + out[4] = 1; + out[5] = (s.has_identity ? 1 : 0) | (s.has_prefs ? 2 : 0); + if (s.has_identity) { memcpy(out + 8, s.prv, 64); memcpy(out + 72, s.pub, 32); } + if (s.has_prefs) { + memcpy(out + 104, s.node_name, 32); + memcpy(out + 136, &s.freq, 4); + memcpy(out + 140, &s.bw, 4); + out[144] = s.sf; out[145] = s.cr; out[146] = (uint8_t)s.tx_power_dbm; + } + out[147] = s.gps_off ? 1 : 0; + for (int i = 0; i < 4; i++) out[148 + i] = (uint8_t)(s.seq >> (8 * i)); + for (int i = 0; i < 4; i++) out[152 + i] = (uint8_t)(s.gps_interval >> (8 * i)); + uint32_t crc = crc32(out, 156); + for (int i = 0; i < 4; i++) out[156 + i] = (uint8_t)(crc >> (8 * i)); +} + +inline bool persist_decode(const uint8_t in[PERSIST_SIZE], PersistState& s) { + memset(&s, 0, sizeof(s)); + if (memcmp(in, "KTM1", 4) != 0 || in[4] != 1) return false; + uint32_t want = 0; + for (int i = 0; i < 4; i++) want |= (uint32_t)in[156 + i] << (8 * i); + if (crc32(in, 156) != want) return false; + s.gps_off = in[147] != 0; + for (int i = 0; i < 4; i++) s.seq |= (uint32_t)in[148 + i] << (8 * i); + for (int i = 0; i < 4; i++) s.gps_interval |= (uint32_t)in[152 + i] << (8 * i); + s.has_identity = (in[5] & 1) != 0; + s.has_prefs = (in[5] & 2) != 0; + if (s.has_identity) { memcpy(s.prv, in + 8, 64); memcpy(s.pub, in + 72, 32); } + if (s.has_prefs) { + memcpy(s.node_name, in + 104, 32); s.node_name[31] = 0; + memcpy(&s.freq, in + 136, 4); + memcpy(&s.bw, in + 140, 4); + s.sf = in[144]; s.cr = in[145]; s.tx_power_dbm = (int8_t)in[146]; + } + return true; +} + +} // namespace asr650x diff --git a/src/helpers/asr650x/FlashRecordStore.h b/src/helpers/asr650x/FlashRecordStore.h new file mode 100644 index 0000000000..301799f22c --- /dev/null +++ b/src/helpers/asr650x/FlashRecordStore.h @@ -0,0 +1,77 @@ +#pragma once +#include "FlashRecord.h" + +// Two-row A/B store for the 160-byte FlashRecord. Each commit writes the OTHER row with seq+1, so a +// power loss while writing leaves the previous record intact (the SFLASH row erase+program is not atomic). +// commit() changes the in-RAM state only after the backend confirmed the write: a rejected change can +// never leak into a later commit (review I1). Rows are 256 bytes apart (one SFLASH user row each). +// +// Backend concept: +// void read(size_t offset, uint8_t* buf, size_t n); +// bool write(size_t offset, const uint8_t* buf, size_t n); // must persist exactly the rows touched +namespace asr650x { + +const size_t ROW_STRIDE = 256; + +inline bool seq_newer(uint32_t a, uint32_t b) { return (int32_t)(a - b) > 0; } + +template +class PersistStore { + Backend& _be; + PersistState _state; + uint8_t _last[PERSIST_SIZE]; // bytes of the newest valid record (what is in flash now) + bool _have_last; + int _next_row; // row the next commit writes + +public: + explicit PersistStore(Backend& be) : _be(be), _have_last(false), _next_row(0) { + memset(&_state, 0, sizeof(_state)); + memset(_last, 0, sizeof(_last)); + } + + const PersistState& state() const { return _state; } + + void begin() { + uint8_t buf[2][PERSIST_SIZE]; + PersistState st[2]; + bool ok[2]; + for (int r = 0; r < 2; r++) { + _be.read(r * ROW_STRIDE, buf[r], PERSIST_SIZE); + ok[r] = persist_decode(buf[r], st[r]); + } + int pick = -1; + if (ok[0] && ok[1]) pick = seq_newer(st[1].seq, st[0].seq) ? 1 : 0; + else if (ok[0]) pick = 0; + else if (ok[1]) pick = 1; + if (pick < 0) { + memset(&_state, 0, sizeof(_state)); + _have_last = false; + _next_row = 0; + return; + } + _state = st[pick]; + memcpy(_last, buf[pick], PERSIST_SIZE); + _have_last = true; + _next_row = pick ^ 1; + } + + bool commit(const PersistState& candidate) { + PersistState c = candidate; + uint8_t buf[PERSIST_SIZE]; + if (_have_last) { // unchanged content -> no flash write + c.seq = _state.seq; + persist_encode(c, buf); + if (memcmp(buf, _last, PERSIST_SIZE) == 0) return true; + } + c.seq = _state.seq + 1; + persist_encode(c, buf); + if (!_be.write(_next_row * ROW_STRIDE, buf, PERSIST_SIZE)) return false; // state untouched on failure + _state = c; + memcpy(_last, buf, PERSIST_SIZE); + _have_last = true; + _next_row ^= 1; + return true; + } +}; + +} // namespace asr650x diff --git a/src/helpers/asr650x/FlashRowBackend.h b/src/helpers/asr650x/FlashRowBackend.h new file mode 100644 index 0000000000..77abcc59ff --- /dev/null +++ b/src/helpers/asr650x/FlashRowBackend.h @@ -0,0 +1,23 @@ +#pragma once +#include +#include +#include + +// Writes straight to the SFLASH user rows through the core's FLASH_update(), one record per 256-byte row. +// (The EEPROM class would rewrite every row of its RAM mirror on each commit, which defeats the A/B scheme.) +extern "C" { +int FLASH_update(uint32_t dst_addr, const void* data, uint32_t size); +int FLASH_read_at(uint32_t address, uint8_t* pData, uint32_t len_bytes); +} + +struct FlashRowBackend { + void read(size_t off, uint8_t* buf, size_t n) { FLASH_read_at(CY_SFLASH_USERBASE + off, buf, n); } + + bool write(size_t off, const uint8_t* buf, size_t n) { + if (n > asr650x::PERSIST_SIZE) return false; + if (FLASH_update(CY_SFLASH_USERBASE + off, buf, n) != 0) return false; + uint8_t chk[asr650x::PERSIST_SIZE]; + FLASH_read_at(CY_SFLASH_USERBASE + off, chk, n); + return memcmp(chk, buf, n) == 0; // FLASH_update only prints on a failed row program and still returns 0 + } +}; diff --git a/src/helpers/asr650x/Font5x7.h b/src/helpers/asr650x/Font5x7.h new file mode 100644 index 0000000000..288319cf66 --- /dev/null +++ b/src/helpers/asr650x/Font5x7.h @@ -0,0 +1,40 @@ +#pragma once +#include + +namespace asr650x { + +// 5x7 ASCII font (0x20..0x7E), 5 column bytes per character, bit 0 = top pixel row; descenders use bit 7. +static const uint8_t FONT5X7[95][5] = { + {0x00,0x00,0x00,0x00,0x00},{0x00,0x00,0x5F,0x00,0x00},{0x00,0x07,0x00,0x07,0x00},{0x14,0x7F,0x14,0x7F,0x14}, + {0x24,0x2A,0x7F,0x2A,0x12},{0x23,0x13,0x08,0x64,0x62},{0x36,0x49,0x56,0x20,0x50},{0x00,0x08,0x07,0x03,0x00}, + {0x00,0x1C,0x22,0x41,0x00},{0x00,0x41,0x22,0x1C,0x00},{0x2A,0x1C,0x7F,0x1C,0x2A},{0x08,0x08,0x3E,0x08,0x08}, + {0x00,0x80,0x70,0x30,0x00},{0x08,0x08,0x08,0x08,0x08},{0x00,0x00,0x60,0x60,0x00},{0x20,0x10,0x08,0x04,0x02}, + {0x3E,0x51,0x49,0x45,0x3E},{0x00,0x42,0x7F,0x40,0x00},{0x72,0x49,0x49,0x49,0x46},{0x21,0x41,0x49,0x4D,0x33}, + {0x18,0x14,0x12,0x7F,0x10},{0x27,0x45,0x45,0x45,0x39},{0x3C,0x4A,0x49,0x49,0x31},{0x41,0x21,0x11,0x09,0x07}, + {0x36,0x49,0x49,0x49,0x36},{0x46,0x49,0x49,0x29,0x1E},{0x00,0x00,0x14,0x00,0x00},{0x00,0x40,0x34,0x00,0x00}, + {0x00,0x08,0x14,0x22,0x41},{0x14,0x14,0x14,0x14,0x14},{0x00,0x41,0x22,0x14,0x08},{0x02,0x01,0x59,0x09,0x06}, + {0x3E,0x41,0x5D,0x59,0x4E},{0x7C,0x12,0x11,0x12,0x7C},{0x7F,0x49,0x49,0x49,0x36},{0x3E,0x41,0x41,0x41,0x22}, + {0x7F,0x41,0x41,0x41,0x3E},{0x7F,0x49,0x49,0x49,0x41},{0x7F,0x09,0x09,0x09,0x01},{0x3E,0x41,0x41,0x51,0x73}, + {0x7F,0x08,0x08,0x08,0x7F},{0x00,0x41,0x7F,0x41,0x00},{0x20,0x40,0x41,0x3F,0x01},{0x7F,0x08,0x14,0x22,0x41}, + {0x7F,0x40,0x40,0x40,0x40},{0x7F,0x02,0x1C,0x02,0x7F},{0x7F,0x04,0x08,0x10,0x7F},{0x3E,0x41,0x41,0x41,0x3E}, + {0x7F,0x09,0x09,0x09,0x06},{0x3E,0x41,0x51,0x21,0x5E},{0x7F,0x09,0x19,0x29,0x46},{0x26,0x49,0x49,0x49,0x32}, + {0x03,0x01,0x7F,0x01,0x03},{0x3F,0x40,0x40,0x40,0x3F},{0x1F,0x20,0x40,0x20,0x1F},{0x3F,0x40,0x38,0x40,0x3F}, + {0x63,0x14,0x08,0x14,0x63},{0x03,0x04,0x78,0x04,0x03},{0x61,0x59,0x49,0x4D,0x43},{0x00,0x7F,0x41,0x41,0x41}, + {0x02,0x04,0x08,0x10,0x20},{0x00,0x41,0x41,0x41,0x7F},{0x04,0x02,0x01,0x02,0x04},{0x40,0x40,0x40,0x40,0x40}, + {0x00,0x03,0x07,0x08,0x00},{0x20,0x54,0x54,0x78,0x40},{0x7F,0x28,0x44,0x44,0x38},{0x38,0x44,0x44,0x44,0x28}, + {0x38,0x44,0x44,0x28,0x7F},{0x38,0x54,0x54,0x54,0x18},{0x00,0x08,0x7E,0x09,0x02},{0x18,0xA4,0xA4,0x9C,0x78}, + {0x7F,0x08,0x04,0x04,0x78},{0x00,0x44,0x7D,0x40,0x00},{0x20,0x40,0x40,0x3D,0x00},{0x7F,0x10,0x28,0x44,0x00}, + {0x00,0x41,0x7F,0x40,0x00},{0x7C,0x04,0x78,0x04,0x78},{0x7C,0x08,0x04,0x04,0x78},{0x38,0x44,0x44,0x44,0x38}, + {0xFC,0x18,0x24,0x24,0x18},{0x18,0x24,0x24,0x18,0xFC},{0x7C,0x08,0x04,0x04,0x08},{0x48,0x54,0x54,0x54,0x24}, + {0x04,0x04,0x3F,0x44,0x24},{0x3C,0x40,0x40,0x20,0x7C},{0x1C,0x20,0x40,0x20,0x1C},{0x3C,0x40,0x30,0x40,0x3C}, + {0x44,0x28,0x10,0x28,0x44},{0x4C,0x90,0x90,0x90,0x7C},{0x44,0x64,0x54,0x4C,0x44},{0x00,0x08,0x36,0x41,0x00}, + {0x00,0x00,0x77,0x00,0x00},{0x00,0x41,0x36,0x08,0x00},{0x02,0x01,0x02,0x04,0x02} +}; + +inline const uint8_t* font5x7_glyph(char c) { + unsigned char u = (unsigned char)c; + if (u < 0x20 || u > 0x7E) u = '?'; + return FONT5X7[u - 0x20]; +} + +} // namespace asr650x diff --git a/src/helpers/asr650x/GpsPolicy.h b/src/helpers/asr650x/GpsPolicy.h new file mode 100644 index 0000000000..802d3fd004 --- /dev/null +++ b/src/helpers/asr650x/GpsPolicy.h @@ -0,0 +1,63 @@ +#pragma once +#include + +namespace asr650x { + +enum GpsState { GPS_OFF = 0, GPS_WARMING = 1, GPS_DONE = 2 }; + +// Decides when the GPS module should be powered. Pure logic: time comes in as seconds from the caller. +class GpsPolicy { + bool _enabled; + uint32_t _interval, _timeout; + GpsState _state; + uint32_t _t_start, _t_done; + bool _timed_out; + uint32_t _fixes; + + // elapsed seconds modulo 2^32 (safe across the counter wrap); a "negative" difference means the clock went + // backwards and counts as 0 + static uint32_t since(uint32_t now, uint32_t then) { + uint32_t d = now - then; + return (int32_t)d < 0 ? 0 : d; + } + +public: + GpsPolicy() : _enabled(false), _interval(0), _timeout(300), _state(GPS_OFF), _t_start(0), _t_done(0), + _timed_out(false), _fixes(0) {} + + void configure(bool enabled, uint32_t interval_s, uint32_t fix_timeout_s) { + _interval = interval_s; + _timeout = fix_timeout_s; + if (!enabled) { _enabled = false; _state = GPS_OFF; return; } + _enabled = true; + } + + void begin(uint32_t now_s) { + if (_enabled) { _state = GPS_WARMING; _t_start = now_s; _timed_out = false; } + } + + void request(uint32_t now_s) { // the app asked for a fresh position + _enabled = true; + _state = GPS_WARMING; + _t_start = now_s; + _timed_out = false; + } + + void tick(uint32_t now_s, bool fix_ok) { + if (!_enabled) return; + if (_state == GPS_WARMING) { + if (fix_ok) { _state = GPS_DONE; _t_done = now_s; _fixes++; _timed_out = false; } + else if (since(now_s, _t_start) >= _timeout) { _state = GPS_DONE; _t_done = now_s; _timed_out = true; } + } else if (_state == GPS_DONE) { + if (_interval > 0 && since(now_s, _t_done) >= _interval) { _state = GPS_WARMING; _t_start = now_s; } + } + } + + bool power_wanted() const { return _state == GPS_WARMING; } + GpsState state() const { return _state; } + bool timed_out() const { return _timed_out; } + uint32_t fix_count() const { return _fixes; } + uint32_t warming_for(uint32_t now_s) const { return _state == GPS_WARMING ? since(now_s, _t_start) : 0; } +}; + +} // namespace asr650x diff --git a/src/helpers/asr650x/IdentityCrypto.cpp b/src/helpers/asr650x/IdentityCrypto.cpp new file mode 100644 index 0000000000..daf16c809a --- /dev/null +++ b/src/helpers/asr650x/IdentityCrypto.cpp @@ -0,0 +1,45 @@ +#ifdef IDENTITY_CRYPTO_ON_BIG_STACK +#define ASR650X_IDENTITY_CRYPTO_IMPL +#include "IdentityCrypto.h" +#include "BigStack.h" +#define ED25519_NO_SEED 1 +#include +#include + +namespace { +struct Args { + unsigned char* o1; + const unsigned char *a, *b, *c; + size_t len; + bool ok; +}; +void runKeypair(void* p) { Args* x = (Args*)p; ed25519_create_keypair(x->o1, (unsigned char*)x->a, x->b); } +void runDerive(void* p) { Args* x = (Args*)p; ed25519_derive_pub(x->o1, x->a); } +void runSign(void* p) { Args* x = (Args*)p; ed25519_sign(x->o1, x->a, x->len, x->b, x->c); } +void runKeyex(void* p) { Args* x = (Args*)p; ed25519_key_exchange(x->o1, x->a, x->b); } +void runVerify(void* p) { Args* x = (Args*)p; x->ok = Ed25519::verify(x->a, x->b, x->c, x->len); } +} + +void asr650x_ed25519_create_keypair(unsigned char* pub, unsigned char* prv, const unsigned char* seed) { + Args a = { pub, prv, seed, 0, 0, false }; + asr650x_call_on_big_stack(runKeypair, &a); +} +void asr650x_ed25519_derive_pub(unsigned char* pub, const unsigned char* prv) { + Args a = { pub, prv, 0, 0, 0, false }; + asr650x_call_on_big_stack(runDerive, &a); +} +void asr650x_ed25519_sign(unsigned char* sig, const unsigned char* msg, size_t len, + const unsigned char* pub, const unsigned char* prv) { + Args a = { sig, msg, pub, prv, len, false }; + asr650x_call_on_big_stack(runSign, &a); +} +void asr650x_ed25519_key_exchange(unsigned char* secret, const unsigned char* pub, const unsigned char* prv) { + Args a = { secret, pub, prv, 0, 0, false }; + asr650x_call_on_big_stack(runKeyex, &a); +} +bool asr650x_ed25519_verify(const unsigned char* sig, const unsigned char* pub, const unsigned char* msg, size_t len) { + Args a = { 0, sig, pub, msg, len, false }; + asr650x_call_on_big_stack(runVerify, &a); + return a.ok; +} +#endif diff --git a/src/helpers/asr650x/IdentityCrypto.h b/src/helpers/asr650x/IdentityCrypto.h new file mode 100644 index 0000000000..a57f2a11f8 --- /dev/null +++ b/src/helpers/asr650x/IdentityCrypto.h @@ -0,0 +1,19 @@ +#pragma once +#include + +// Ed25519 on the big stack (BigStack.h): the software Ed25519 needs more than the ASR650x's fixed 2 KB main stack. +// Identity.cpp includes this header when IDENTITY_CRYPTO_ON_BIG_STACK is defined; the macros below then route its +// ed25519_* calls through these wrappers. +void asr650x_ed25519_create_keypair(unsigned char* pub, unsigned char* prv, const unsigned char* seed); +void asr650x_ed25519_derive_pub(unsigned char* pub, const unsigned char* prv); +void asr650x_ed25519_sign(unsigned char* sig, const unsigned char* msg, size_t len, + const unsigned char* pub, const unsigned char* prv); +void asr650x_ed25519_key_exchange(unsigned char* secret, const unsigned char* pub, const unsigned char* prv); +bool asr650x_ed25519_verify(const unsigned char* sig, const unsigned char* pub, const unsigned char* msg, size_t len); + +#ifndef ASR650X_IDENTITY_CRYPTO_IMPL + #define ed25519_create_keypair asr650x_ed25519_create_keypair + #define ed25519_derive_pub asr650x_ed25519_derive_pub + #define ed25519_sign asr650x_ed25519_sign + #define ed25519_key_exchange asr650x_ed25519_key_exchange +#endif diff --git a/src/helpers/asr650x/NmeaParser.h b/src/helpers/asr650x/NmeaParser.h new file mode 100644 index 0000000000..aa2956c88b --- /dev/null +++ b/src/helpers/asr650x/NmeaParser.h @@ -0,0 +1,229 @@ +#pragma once +#include +#include +#include + +// Integer-only NMEA 0183 reader for the Air530 (GGA + RMC). No float/double, fixed buffer, tolerant of noise. +namespace asr650x { + +enum NmeaEvent { NMEA_NONE = 0, NMEA_GGA = 1, NMEA_RMC = 2, NMEA_BAD = 3 }; + +struct GpsData { + bool pos_valid; // latest GGA/RMC carried a usable position + uint8_t fix_quality; // GGA field 6 (0 = none) + uint8_t sats; // GGA field 7 + int32_t lat_e6, lon_e6; // degrees x 1e6 + int32_t alt_dm; // decimetres + bool rmc_valid; // RMC status 'A' + uint32_t time_hhmmss; // UTC, from RMC + uint32_t date_ddmmyy; // from RMC +}; + +inline uint8_t nmea_checksum(const char* s, size_t n) { + uint8_t c = 0; + for (size_t i = 0; i < n; i++) c ^= (uint8_t)s[i]; + return c; +} + +// "$body*HH\r\n"; returns the length, or 0 if out is too small. +inline int nmea_make(char* out, size_t out_size, const char* body) { + size_t n = strlen(body); + if (out_size < n + 7) return 0; // $ + body + * + 2 hex + \r\n + NUL + static const char H[] = "0123456789ABCDEF"; + uint8_t c = nmea_checksum(body, n); + out[0] = '$'; + memcpy(out + 1, body, n); + out[1 + n] = '*'; + out[2 + n] = H[c >> 4]; + out[3 + n] = H[c & 15]; + out[4 + n] = '\r'; + out[5 + n] = '\n'; + out[6 + n] = 0; + return (int)(n + 6); +} + +inline bool nmea_is_leap(int y) { return (y % 4 == 0 && y % 100 != 0) || y % 400 == 0; } + +// UTC epoch from RMC date/time. 0 means invalid (year outside 2024..2060, bad month/day/hour/minute/second). +inline uint32_t gps_to_epoch(uint32_t date_ddmmyy, uint32_t time_hhmmss) { + int dd = (int)(date_ddmmyy / 10000), mm = (int)((date_ddmmyy / 100) % 100), yy = (int)(date_ddmmyy % 100); + int hh = (int)(time_hhmmss / 10000), mi = (int)((time_hhmmss / 100) % 100), ss = (int)(time_hhmmss % 100); + int year = 2000 + yy; + if (year < 2024 || year > 2060) return 0; + if (mm < 1 || mm > 12 || dd < 1) return 0; + static const int dim[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + int days_in_month = dim[mm - 1] + ((mm == 2 && nmea_is_leap(year)) ? 1 : 0); + if (dd > days_in_month) return 0; + if (hh > 23 || mi > 59 || ss > 59) return 0; + uint32_t days = 0; + for (int y = 1970; y < year; y++) days += nmea_is_leap(y) ? 366 : 365; + for (int m = 1; m < mm; m++) days += dim[m - 1] + ((m == 2 && nmea_is_leap(year)) ? 1 : 0); + days += (uint32_t)(dd - 1); + return days * 86400u + (uint32_t)hh * 3600u + (uint32_t)mi * 60u + (uint32_t)ss; +} + +class NmeaParser { + char _buf[84]; + uint8_t _len; + bool _active; + GpsData _d; + uint32_t _ok, _bad; + + static bool parse_uint(const char* s, uint32_t* out) { // digits only, non-empty + if (!*s) return false; + uint32_t v = 0; + for (; *s; s++) { + if (*s < '0' || *s > '9') return false; + v = v * 10 + (uint32_t)(*s - '0'); + if (v > 99999999u) return false; + } + *out = v; + return true; + } + + // "ddmm.mmmm" (or dddmm.mmmm) + hemisphere -> degrees x 1e6; false if malformed or empty + static bool parse_latlon(const char* s, char hemi, int deg_digits, int32_t* out) { + if (!*s) return false; + int n = 0; + while (s[n] >= '0' && s[n] <= '9') n++; + if (n != deg_digits + 2) return false; + uint32_t deg = 0; + for (int i = 0; i < deg_digits; i++) deg = deg * 10 + (uint32_t)(s[i] - '0'); + uint32_t whole = (uint32_t)(s[deg_digits] - '0') * 10 + (uint32_t)(s[deg_digits + 1] - '0'); + const char* p = s + n; + uint64_t frac = 0, scale = 1; // fractional minutes, up to 6 digits + if (*p == '.') { + p++; + int k = 0; + while (*p >= '0' && *p <= '9') { + if (k < 6) { frac = frac * 10 + (uint64_t)(*p - '0'); scale *= 10; k++; } + p++; + } + } + if (*p != 0) return false; + if (whole >= 60) return false; + uint64_t minutes_scaled = (uint64_t)whole * scale + frac; // minutes x scale + uint64_t frac_e6 = (minutes_scaled * 1000000u + 30u * scale) / (60u * scale); // rounded + int64_t v = (int64_t)deg * 1000000 + (int64_t)frac_e6; + if (v > (deg_digits == 2 ? 90000000LL : 180000000LL)) return false; // beyond the pole / the antimeridian + if (hemi == 'S' || hemi == 'W') v = -v; + else if (hemi != 'N' && hemi != 'E') return false; + *out = (int32_t)v; + return true; + } + + // "123.4" -> decimetres (one decimal, truncated); false if malformed + static bool parse_dm(const char* s, int32_t* out) { + if (!*s) return false; + bool neg = false; + if (*s == '-') { neg = true; s++; } + if (*s < '0' || *s > '9') return false; + int32_t v = 0; + while (*s >= '0' && *s <= '9') { v = v * 10 + (*s - '0'); s++; if (v > 1000000) return false; } + int32_t d = 0; + if (*s == '.') { s++; if (*s >= '0' && *s <= '9') d = *s - '0'; while (*s >= '0' && *s <= '9') s++; } + if (*s != 0) return false; + v = v * 10 + d; + *out = neg ? -v : v; + return true; + } + + static int hex(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + return -1; + } + + NmeaEvent parse() { + _buf[_len] = 0; + char* star = (char*)memchr(_buf, '*', _len); + if (!star || (_buf + _len) - star != 3) return NMEA_BAD; + int h1 = hex(star[1]), h2 = hex(star[2]); + if (h1 < 0 || h2 < 0) return NMEA_BAD; + if (nmea_checksum(_buf, (size_t)(star - _buf)) != (uint8_t)(h1 * 16 + h2)) return NMEA_BAD; + *star = 0; + + char* f[20]; + int nf = 0; + f[nf++] = _buf; + for (char* p = _buf; *p && nf < 20; p++) if (*p == ',') { *p = 0; f[nf++] = p + 1; } + size_t idlen = strlen(f[0]); + if (idlen < 5) { _ok++; return NMEA_NONE; } + const char* type = f[0] + idlen - 3; + + if (strcmp(type, "GGA") == 0 && nf >= 10) { + uint32_t q = 0, sats = 0; + parse_uint(f[6], &q); + parse_uint(f[7], &sats); + _d.fix_quality = (uint8_t)(q > 255 ? 255 : q); + _d.sats = (uint8_t)(sats > 255 ? 255 : sats); + int32_t lat = 0, lon = 0, alt = 0; + bool ok = _d.fix_quality > 0 && parse_latlon(f[2], f[3][0], 2, &lat) && parse_latlon(f[4], f[5][0], 3, &lon); + if (ok) { + _d.lat_e6 = lat; _d.lon_e6 = lon; + if (parse_dm(f[9], &alt)) _d.alt_dm = alt; + } + _d.pos_valid = ok; + _ok++; + return NMEA_GGA; + } + if (strcmp(type, "RMC") == 0 && nf >= 10) { + bool a = f[2][0] == 'A' && f[2][1] == 0; + _d.rmc_valid = false; + uint32_t t = 0, dt = 0; + int32_t lat = 0, lon = 0; + bool ok = a && parse_latlon(f[3], f[4][0], 2, &lat) && parse_latlon(f[5], f[6][0], 3, &lon); + // time "hhmmss[.sss]": keep the integer part + char tb[8]; + size_t tl = 0; + while (f[1][tl] && f[1][tl] != '.' && tl < 7) { tb[tl] = f[1][tl]; tl++; } + tb[tl] = 0; + bool tok = tl == 6 && parse_uint(tb, &t); + bool dok = strlen(f[9]) == 6 && parse_uint(f[9], &dt); + if (a && tok && dok) { _d.rmc_valid = true; _d.time_hhmmss = t; _d.date_ddmmyy = dt; } + if (ok) { _d.lat_e6 = lat; _d.lon_e6 = lon; } + _d.pos_valid = ok; + _ok++; + return NMEA_RMC; + } + _ok++; + return NMEA_NONE; + } + +public: + NmeaParser() : _len(0), _active(false), _ok(0), _bad(0) { memset(&_d, 0, sizeof(_d)); } + + const GpsData& data() const { return _d; } + void reset_fix() { _d.pos_valid = false; _d.rmc_valid = false; _d.fix_quality = 0; _d.sats = 0; } // after a power cycle: old data is not a fix + uint32_t sentences_ok() const { return _ok; } + uint32_t sentences_bad() const { return _bad; } + + NmeaEvent feed(uint8_t c) { + if (c == '$') { // (re)start; a '$' inside a broken sentence is a resync + if (_active && _len > 0) _bad++; + _len = 0; + _active = true; + return NMEA_NONE; + } + if (!_active) return NMEA_NONE; + if (c == '\r' || c == '\n') { + if (_len == 0) return NMEA_NONE; + NmeaEvent e = parse(); + if (e == NMEA_BAD) _bad++; + _active = false; + _len = 0; + return e; + } + if (_len >= sizeof(_buf) - 1) { // overlong: drop it + _active = false; + _len = 0; + _bad++; + return NMEA_BAD; + } + _buf[_len++] = (char)c; + return NMEA_NONE; + } +}; + +} // namespace asr650x diff --git a/src/helpers/asr650x/OledText.h b/src/helpers/asr650x/OledText.h new file mode 100644 index 0000000000..0e1a01d651 --- /dev/null +++ b/src/helpers/asr650x/OledText.h @@ -0,0 +1,21 @@ +#pragma once +#include +#include +#include "Font5x7.h" + +namespace asr650x { + +const int OLED_COLS = 128; +const int OLED_CHARS = 21; // 21 characters x 6 columns = 126 + +// One SSD1306 page (8 pixel rows, 128 columns) holding a line of text; bit 0 of each byte is the top pixel row. +inline void render_line(const char* text, uint8_t page[OLED_COLS]) { + memset(page, 0, OLED_COLS); + if (!text) return; + for (int i = 0; i < OLED_CHARS && text[i]; i++) { + const uint8_t* g = font5x7_glyph(text[i]); + memcpy(page + i * 6, g, 5); + } +} + +} // namespace asr650x diff --git a/src/helpers/asr650x/RadioParams.h b/src/helpers/asr650x/RadioParams.h new file mode 100644 index 0000000000..c1c9848ebb --- /dev/null +++ b/src/helpers/asr650x/RadioParams.h @@ -0,0 +1,67 @@ +#pragma once +#include +#include + +// Pure helpers for the SX1262 wrapper (host-testable). Bandwidth table order is the Heltec driver's +// `Bandwidths[]` (libraries/LoraWan102/src/radio/radio.c): 125, 250, 500, 62.5, 41.67, 31.25, 20.83, 15.63, 10.42, 7.81 kHz. +namespace asr650x { + +inline float bw_value(int index) { + static const float table[10] = {125.0f, 250.0f, 500.0f, 62.5f, 41.67f, 31.25f, 20.83f, 15.63f, 10.42f, 7.81f}; + if (index < 0) index = 0; + if (index > 9) index = 9; + return table[index]; +} + +// Nearest supported bandwidth by ratio (so 100 kHz maps to 125, not to 62.5). Never returns an invalid index. +inline int bw_index(float bw_khz) { + if (!(bw_khz > 0.0f)) return 9; // zero, negative or NaN -> smallest + int best = 0; + float best_ratio = 1e9f; + for (int i = 0; i < 10; i++) { + float v = bw_value(i); + float r = bw_khz > v ? bw_khz / v : v / bw_khz; // >= 1, closer to 1 is nearer + if (r < best_ratio) { best_ratio = r; best = i; } + } + return best; +} + +inline uint8_t cr_index(uint8_t cr) { // MeshCore 5..8 (4/5..4/8) -> Heltec 1..4 + if (cr < 5) cr = 5; + if (cr > 8) cr = 8; + return (uint8_t)(cr - 4); +} + +inline uint8_t sf_clamp(uint8_t sf) { return sf < 5 ? 5 : (sf > 12 ? 12 : sf); } + +inline uint16_t preamble_for_sf(uint8_t sf) { return sf <= 8 ? 32 : 16; } // same rule as MeshCore's RadioLibWrapper + +// Semtech time-on-air, explicit header, CRC on, integer microsecond arithmetic. Returns milliseconds (rounded up). +inline uint32_t airtime_ms(int len, float bw_khz, uint8_t sf, uint8_t cr, uint16_t preamble) { + if (len < 0) len = 0; + sf = sf_clamp(sf); + uint8_t crv = cr_index(cr); // 1..4 + uint32_t bw_hz = (uint32_t)(bw_khz * 1000.0f + 0.5f); + if (bw_hz == 0) bw_hz = 1; + uint32_t tsym_us = (uint32_t)(((uint64_t)1000000u << sf) / bw_hz); + int de = tsym_us >= 16380u ? 1 : 0; // low data-rate optimisation: same threshold as the Heltec driver (16.38 ms) + int num = 8 * len - 4 * (int)sf + 28 + 16; // +16 CRC, explicit header (no -20) + int den = 4 * ((int)sf - 2 * de); + int blocks = num > 0 ? (num + den - 1) / den : 0; + uint32_t npay = 8u + (uint32_t)(blocks * (crv + 4)); + uint64_t us = ((uint64_t)(preamble * 4 + 17) * tsym_us) / 4 + (uint64_t)npay * tsym_us; // (n_pre + 4.25) symbols + return (uint32_t)((us + 999u) / 1000u); +} + +// Same estimate MeshCore uses to weigh received packets (RadioLibWrapper::packetScoreInt). +inline float packet_score(float snr, int sf, int packet_len) { + static const float snr_threshold[] = {-7.5f, -10.0f, -12.5f, -15.0f, -17.5f, -20.0f}; // SF7..SF12 + if (sf < 7 || sf > 12) return 0.0f; + if (snr < snr_threshold[sf - 7]) return 0.0f; + float success = (snr - snr_threshold[sf - 7]) / 10.0f; + float collision_penalty = 1.0f - (packet_len / 256.0f); + float v = success * collision_penalty; + return v < 0.0f ? 0.0f : (v > 1.0f ? 1.0f : v); +} + +} // namespace asr650x diff --git a/src/helpers/asr650x/Sbrk.cpp b/src/helpers/asr650x/Sbrk.cpp new file mode 100644 index 0000000000..a982e5c6b8 --- /dev/null +++ b/src/helpers/asr650x/Sbrk.cpp @@ -0,0 +1,25 @@ +// Override of the core's weak _sbrk (Cm0plusStart.c). The core caps the heap at CYDEV_HEAP_SIZE (4096 B) +// although the linker leaves everything between .bss and the 2 KB stack unused. With the crypto stack +// (2.3 KB), the packet pool, the UART buffer and the flash page cache (256 B, allocated during every +// EEPROM commit) the 4 KB cap is exceeded, malloc() fails and crypto silently runs on the 2 KB main +// stack (M1 hardware run). Here the heap may grow up to the bottom of the stack region. +#include +#include + +extern "C" { +extern int end; // linker: first free byte after .bss +extern uint32_t __cy_stack_limit[]; // linker: bottom of the 2 KB stack region + +void* _sbrk(int nbytes) { + static uint8_t* heap_ptr = (uint8_t*)&end; + uint8_t* limit = (uint8_t*)__cy_stack_limit; + uint8_t* next = heap_ptr + nbytes; + if (next > limit || next < (uint8_t*)&end) { + errno = ENOMEM; + return (void*)-1; + } + void* prev = heap_ptr; + heap_ptr = next; + return prev; +} +} diff --git a/src/helpers/asr650x/SmallStrtod.cpp b/src/helpers/asr650x/SmallStrtod.cpp new file mode 100644 index 0000000000..43e90412ba --- /dev/null +++ b/src/helpers/asr650x/SmallStrtod.cpp @@ -0,0 +1,10 @@ +#ifdef ASR650X_PLATFORM +// Linked before libc: these definitions replace newlib's strtod/strtof/atof (and the ~7 KB of gdtoa they pull in). +#include "SmallStrtod.h" + +extern "C" { +double strtod(const char* s, char** end) { return asr650x_strtod(s, end); } +float strtof(const char* s, char** end) { return (float)asr650x_strtod(s, end); } +double atof(const char* s) { return asr650x_strtod(s, 0); } +} +#endif diff --git a/src/helpers/asr650x/SmallStrtod.h b/src/helpers/asr650x/SmallStrtod.h new file mode 100644 index 0000000000..047332fc94 --- /dev/null +++ b/src/helpers/asr650x/SmallStrtod.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include + +// Decimal-only strtod() (optional sign, digits, optional fraction, optional exponent; no hex/inf/nan). +// newlib's strtod() pulls in ~7 KB of flash (gdtoa); the CLI only parses plain decimal numbers. +// SmallStrtod.cpp installs it as strtod/strtof/atof on ASR650x builds. +inline double asr650x_strtod(const char* s, char** end) { + const char* p = s; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == '\f' || *p == '\v') p++; + bool neg = false; + if (*p == '+' || *p == '-') { neg = (*p == '-'); p++; } + uint64_t mant = 0; + int digits = 0, scale = 0; + bool any = false; + while (*p >= '0' && *p <= '9') { + any = true; + if (digits < 19) { mant = mant * 10 + (uint64_t)(*p - '0'); digits += (mant != 0); } + else scale++; // more digits than fit: keep the magnitude only + p++; + } + if (*p == '.') { + const char* q = p + 1; + bool frac = false; + while (*q >= '0' && *q <= '9') { + frac = true; + if (digits < 19) { mant = mant * 10 + (uint64_t)(*q - '0'); digits += (mant != 0); scale--; } + q++; + } + if (any || frac) { any = true; p = q; } + } + if (!any) { + if (end) *end = (char*)s; + return 0.0; + } + if (*p == 'e' || *p == 'E') { + const char* q = p + 1; + bool eneg = false; + if (*q == '+' || *q == '-') { eneg = (*q == '-'); q++; } + if (*q >= '0' && *q <= '9') { + int e = 0; + while (*q >= '0' && *q <= '9') { if (e < 400) e = e * 10 + (*q - '0'); q++; } + scale += eneg ? -e : e; + p = q; + } + } + double v = (double)mant; + while (scale > 0) { v *= 10.0; scale--; } + while (scale < 0) { v /= 10.0; scale++; } + if (end) *end = (char*)p; + return neg ? -v : v; +} diff --git a/src/helpers/asr650x/StatusLines.h b/src/helpers/asr650x/StatusLines.h new file mode 100644 index 0000000000..191ca883e3 --- /dev/null +++ b/src/helpers/asr650x/StatusLines.h @@ -0,0 +1,84 @@ +#pragma once +#include +#include +#include + +// NOTE: no %l length modifier anywhere: the core's wrapped snprintf printed "u" for "%lu" on the device. +namespace asr650x { + +struct StatusInput { + const char* name; + uint8_t id4[4]; // first bytes of the public key + bool time_valid; + uint32_t epoch; // UTC seconds + uint8_t gps_state; // 0 off, 1 searching, 2 fixed, 3 timeout + uint8_t sats; + bool pos_valid; + int32_t lat_e6, lon_e6; + uint16_t batt_mv; // 0 = not measured + uint8_t contacts; // contacts in the table + uint8_t max_contacts; // table size (MAX_CONTACTS) + uint32_t freq_khz; + uint8_t sf; + uint32_t warming_s; +}; + +// The text of screen line `index` (0..7), at most 21 characters (plus the NUL in out[22]). +inline void status_line(int index, const StatusInput& in, char out[22]) { + char t[48]; + t[0] = 0; + switch (index) { + case 0: { + size_t n = 0; + if (in.name) { + for (; in.name[n] && n < 21; n++) { + unsigned char c = (unsigned char)in.name[n]; + t[n] = (c >= 0x20 && c < 0x7f) ? (char)c : '?'; + } + } + t[n] = 0; + break; + } + case 1: snprintf(t, sizeof(t), "ID %02x%02x%02x%02x", (unsigned)in.id4[0], (unsigned)in.id4[1], (unsigned)in.id4[2], (unsigned)in.id4[3]); break; + case 2: + if (!in.time_valid) snprintf(t, sizeof(t), "--:--:-- no time"); + else { + uint32_t s = in.epoch % 86400u; + snprintf(t, sizeof(t), "%02u:%02u:%02u UTC", (unsigned)(s / 3600), (unsigned)((s / 60) % 60), (unsigned)(s % 60)); + } + break; + case 3: + if (in.gps_state == 1) snprintf(t, sizeof(t), "GPS search %us", (unsigned)(in.warming_s > 999 ? 999 : in.warming_s)); + else if (in.gps_state == 2) snprintf(t, sizeof(t), "GPS fix %u sats", (unsigned)in.sats); + else if (in.gps_state == 3) snprintf(t, sizeof(t), "GPS timeout"); + else snprintf(t, sizeof(t), "GPS off"); + break; + case 4: + case 5: { + if (!in.pos_valid) { snprintf(t, sizeof(t), index == 4 ? "LAT --" : "LON --"); break; } + int32_t v = index == 4 ? in.lat_e6 : in.lon_e6; + bool neg = v < 0; + uint32_t a = neg ? (uint32_t)(-(int64_t)v) : (uint32_t)v; + // sign and whole degrees are right-aligned in a fixed field so the decimals line up + char whole[12]; + snprintf(whole, sizeof(whole), "%s%u", neg ? "-" : "", (unsigned)(a / 1000000u)); + snprintf(t, sizeof(t), "%s %3s.%06u", index == 4 ? "LAT" : "LON", whole, (unsigned)(a % 1000000u)); + break; + } + case 6: { + char b[10]; + if (in.batt_mv == 0) snprintf(b, sizeof(b), "--"); + else snprintf(b, sizeof(b), "%u.%02uV", (unsigned)(in.batt_mv / 1000), (unsigned)((in.batt_mv % 1000) / 10)); + snprintf(t, sizeof(t), "BAT %s CT %u/%u", b, (unsigned)in.contacts, (unsigned)in.max_contacts); + break; + } + case 7: + snprintf(t, sizeof(t), "%u.%03uMHz SF%u", (unsigned)(in.freq_khz / 1000), (unsigned)(in.freq_khz % 1000), (unsigned)in.sf); + break; + default: break; + } + strncpy(out, t, 21); + out[21] = 0; +} + +} // namespace asr650x diff --git a/src/helpers/asr650x/TxGate.cpp b/src/helpers/asr650x/TxGate.cpp new file mode 100644 index 0000000000..8a7ac8ee7f --- /dev/null +++ b/src/helpers/asr650x/TxGate.cpp @@ -0,0 +1,6 @@ +#include "TxGate.h" + +static bool g_tx_allowed = false; + +bool asr650x_tx_allowed() { return g_tx_allowed; } +void asr650x_set_tx_allowed(bool allowed) { g_tx_allowed = allowed; } diff --git a/src/helpers/asr650x/TxGate.h b/src/helpers/asr650x/TxGate.h new file mode 100644 index 0000000000..5f419925a4 --- /dev/null +++ b/src/helpers/asr650x/TxGate.h @@ -0,0 +1,6 @@ +#pragma once + +// The companion firmware for this board does not generate its own identity: the app imports a private key. +// Until then the node runs with a placeholder identity derived from the chip ID, which must never transmit. +bool asr650x_tx_allowed(); +void asr650x_set_tx_allowed(bool allowed); diff --git a/test/test_asr650x_button/test_asr650x_button.cpp b/test/test_asr650x_button/test_asr650x_button.cpp new file mode 100644 index 0000000000..fc6d990b3b --- /dev/null +++ b/test/test_asr650x_button/test_asr650x_button.cpp @@ -0,0 +1,58 @@ +#include +#include +#include "helpers/asr650x/Button.h" + +TEST(Asr650xButton, Behaves) { + /* a clean press is reported once, after the debounce time, and not again while held */ + { + asr650x::Button b; + EXPECT_TRUE(!b.update(false, 0)); + EXPECT_TRUE(!b.update(true, 100)); + EXPECT_TRUE(!b.update(true, 120)); // 20 ms: still bouncing + EXPECT_TRUE(b.update(true, 135)); // >= 30 ms held: reported + EXPECT_TRUE(!b.update(true, 500)); // held: no repeat + EXPECT_TRUE(!b.update(false, 600)); + EXPECT_TRUE(!b.update(true, 700)); + EXPECT_TRUE(b.update(true, 731)); // second press needs the release in between + } + /* contact bounce shorter than the debounce time never fires */ + { + asr650x::Button b; + EXPECT_TRUE(!b.update(true, 0)); + EXPECT_TRUE(!b.update(false, 10)); + EXPECT_TRUE(!b.update(true, 20)); + EXPECT_TRUE(!b.update(false, 40)); + EXPECT_TRUE(!b.update(false, 1000)); + } + /* readings during the hold-off after a battery measurement are ignored, even a long LOW level */ + { + asr650x::Button b; + b.holdoff(1000, 50); + EXPECT_TRUE(!b.update(true, 1001)); + EXPECT_TRUE(!b.update(true, 1030)); + EXPECT_TRUE(!b.update(true, 1049)); + EXPECT_TRUE(!b.update(true, 1060)); // pin still LOW after the hold-off: not a new press, it must be released first + EXPECT_TRUE(!b.update(false, 1100)); + EXPECT_TRUE(!b.update(true, 1200)); + EXPECT_TRUE(b.update(true, 1235)); + } + /* a press that began before the hold-off and is still held after it must not fire */ + { + asr650x::Button b; + EXPECT_TRUE(!b.update(true, 0)); + b.holdoff(10, 50); + EXPECT_TRUE(!b.update(true, 100)); + EXPECT_TRUE(!b.update(true, 200)); + } + /* millis() wrap-around */ + { + asr650x::Button b; + EXPECT_TRUE(!b.update(true, 0xFFFFFFF0u)); + EXPECT_TRUE(b.update(true, 0x00000020u)); // 48 ms later across the wrap + } +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/test/test_asr650x_epoch_clock/test_asr650x_epoch_clock.cpp b/test/test_asr650x_epoch_clock/test_asr650x_epoch_clock.cpp new file mode 100644 index 0000000000..8866d37ada --- /dev/null +++ b/test/test_asr650x_epoch_clock/test_asr650x_epoch_clock.cpp @@ -0,0 +1,28 @@ +#include +#include +#include "helpers/asr650x/EpochClock.h" + +TEST(Asr650xEpochClock, Behaves) { + asr650x::EpochClock c(1000); + EXPECT_TRUE(c.now(0) == 1000); // starts at the boot epoch + EXPECT_TRUE(c.now(999) == 1000); // not yet a full second + EXPECT_TRUE(c.now(1000) == 1001); + EXPECT_TRUE(c.now(3500) == 1003); // remainder carried, not lost + c.set(5000, 3500); + EXPECT_TRUE(c.now(3500) == 5000); // set() is exact at that instant + EXPECT_TRUE(c.now(4499) == 5000); + EXPECT_TRUE(c.now(4500) == 5001); + c.set(2000, 4500); // going backwards is allowed + EXPECT_TRUE(c.now(5500) == 2001); + asr650x::EpochClock w(100); // millis() wrap at 2^32 + EXPECT_TRUE(w.now(0xFFFFFF00u) == 100 + 4294967u); + uint32_t before = w.now(0xFFFFFF00u); + uint32_t after = w.now(0x00000100u); // 0x200 ms later + EXPECT_TRUE(after >= before && after - before <= 1); + { asr650x::EpochClock s(1000); EXPECT_TRUE(!s.synced()); s.set(2000, 0); EXPECT_TRUE(s.synced()); } +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/test/test_asr650x_gps_policy/test_asr650x_gps_policy.cpp b/test/test_asr650x_gps_policy/test_asr650x_gps_policy.cpp new file mode 100644 index 0000000000..2a9a382c5f --- /dev/null +++ b/test/test_asr650x_gps_policy/test_asr650x_gps_policy.cpp @@ -0,0 +1,116 @@ +#include +#include +#include "helpers/asr650x/GpsPolicy.h" + +TEST(Asr650xGpsPolicy, Behaves) { + /* default: enabled, interval 0, timeout 300 s */ + { + asr650x::GpsPolicy p; + p.configure(true, 0, 300); + EXPECT_TRUE(p.state() == asr650x::GPS_OFF && !p.power_wanted()); // nothing happens before begin() + p.begin(100); + EXPECT_TRUE(p.state() == asr650x::GPS_WARMING && p.power_wanted()); + p.tick(110, false); + EXPECT_TRUE(p.state() == asr650x::GPS_WARMING && p.warming_for(110) == 10); + p.tick(130, true); // fix -> done, power off + EXPECT_TRUE(p.state() == asr650x::GPS_DONE && !p.power_wanted() && p.fix_count() == 1 && !p.timed_out()); + p.tick(100000, false); // interval 0: stays off forever + EXPECT_TRUE(p.state() == asr650x::GPS_DONE && !p.power_wanted()); + } + /* timeout without a fix */ + { + asr650x::GpsPolicy p; + p.configure(true, 0, 300); + p.begin(0); + p.tick(299, false); + EXPECT_TRUE(p.state() == asr650x::GPS_WARMING); + p.tick(300, false); + EXPECT_TRUE(p.state() == asr650x::GPS_DONE && p.timed_out() && p.fix_count() == 0 && !p.power_wanted()); + } + /* periodic refresh */ + { + asr650x::GpsPolicy p; + p.configure(true, 600, 300); + p.begin(0); + p.tick(20, true); + EXPECT_TRUE(p.state() == asr650x::GPS_DONE); + p.tick(619, false); + EXPECT_TRUE(p.state() == asr650x::GPS_DONE); + p.tick(620, false); // 600 s after entering DONE at t=20 + EXPECT_TRUE(p.state() == asr650x::GPS_WARMING && p.power_wanted()); + p.tick(640, true); + EXPECT_TRUE(p.state() == asr650x::GPS_DONE && p.fix_count() == 2); + } + /* disabled: never powers, even on tick/begin */ + { + asr650x::GpsPolicy p; + p.configure(false, 60, 300); + p.begin(0); + p.tick(1000, true); + EXPECT_TRUE(p.state() == asr650x::GPS_OFF && !p.power_wanted() && p.fix_count() == 0); + } + /* the app turns GPS on later: starts warming immediately; turning it off stops at once */ + { + asr650x::GpsPolicy p; + p.configure(false, 0, 300); + p.begin(0); + p.configure(true, 0, 300); + p.request(50); + EXPECT_TRUE(p.state() == asr650x::GPS_WARMING && p.power_wanted() && p.warming_for(60) == 10); + p.configure(false, 0, 300); + EXPECT_TRUE(p.state() == asr650x::GPS_OFF && !p.power_wanted()); + } + /* request() while already DONE re-acquires and clears the timeout flag */ + { + asr650x::GpsPolicy p; + p.configure(true, 0, 10); + p.begin(0); + p.tick(10, false); + EXPECT_TRUE(p.timed_out()); + p.request(20); + EXPECT_TRUE(p.state() == asr650x::GPS_WARMING && !p.timed_out()); + } + /* clock going backwards (RTC set by the app) must not wedge the machine */ + { + asr650x::GpsPolicy p; + p.configure(true, 100, 300); + p.begin(1000); + p.tick(500, false); // now < start: treated as 0 s elapsed, no underflow + EXPECT_TRUE(p.state() == asr650x::GPS_WARMING && p.warming_for(500) == 0); + } + /* changing the interval while DONE applies to the next cycle */ + { + asr650x::GpsPolicy p; + p.configure(true, 0, 300); + p.begin(0); + p.tick(5, true); + p.configure(true, 60, 300); + p.tick(64, false); + EXPECT_TRUE(p.state() == asr650x::GPS_DONE); + p.tick(65, false); + EXPECT_TRUE(p.state() == asr650x::GPS_WARMING); + } +} + +// the seconds counter wraps after 2^32 s: timeouts and intervals still fire across the wrap +TEST(Asr650xGpsPolicy, TimeoutAndIntervalAcrossCounterWrap) { + asr650x::GpsPolicy p; + p.configure(true, 600, 300); + p.begin(0xFFFFFF00u); + p.tick(0xFFFFFF00u + 299u, false); + EXPECT_EQ(p.state(), asr650x::GPS_WARMING); + p.tick(0xFFFFFF00u + 300u, false); // wrapped: 0x2C + EXPECT_EQ(p.state(), asr650x::GPS_DONE); + EXPECT_TRUE(p.timed_out()); + uint32_t done = 0xFFFFFF00u + 300u; + p.tick(done + 599u, false); + EXPECT_EQ(p.state(), asr650x::GPS_DONE); + p.tick(done + 600u, false); + EXPECT_EQ(p.state(), asr650x::GPS_WARMING); + EXPECT_EQ(p.warming_for(done + 610u), 10u); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/test/test_asr650x_nmea/test_asr650x_nmea.cpp b/test/test_asr650x_nmea/test_asr650x_nmea.cpp new file mode 100644 index 0000000000..263ccd4f66 --- /dev/null +++ b/test/test_asr650x_nmea/test_asr650x_nmea.cpp @@ -0,0 +1,161 @@ +#include +#include +#include +#include +#include "helpers/asr650x/NmeaParser.h" + +static asr650x::NmeaEvent feed_str(asr650x::NmeaParser& p, const std::string& s) { + asr650x::NmeaEvent last = asr650x::NMEA_NONE; + for (unsigned char c : s) { + asr650x::NmeaEvent e = p.feed(c); + if (e != asr650x::NMEA_NONE) last = e; + } + return last; +} + +TEST(Asr650xNmeaParser, Behaves) { + /* classic GGA example: 48 07.038 N, 11 31.000 E, 8 sats, 545.4 m */ + { + asr650x::NmeaParser p; + EXPECT_TRUE(feed_str(p, "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n") == asr650x::NMEA_GGA); + const asr650x::GpsData& d = p.data(); + EXPECT_TRUE(d.pos_valid && d.fix_quality == 1 && d.sats == 8); + EXPECT_TRUE(d.lat_e6 == 48117300 && d.lon_e6 == 11516667 && d.alt_dm == 5454); + EXPECT_TRUE(p.sentences_ok() == 1 && p.sentences_bad() == 0); + } + /* 5-decimal minutes, western hemisphere (Air530 style, GN talker) */ + { + asr650x::NmeaParser p; + EXPECT_TRUE(feed_str(p, "$GNGGA,092750.000,5321.68020,N,00630.33720,W,1,08,1.03,61.7,M,55.2,M,,*58\r\n") == asr650x::NMEA_GGA); + EXPECT_TRUE(p.data().lat_e6 == 53361337 && p.data().lon_e6 == -6505620 && p.data().alt_dm == 617); + } + /* southern / eastern */ + { + asr650x::NmeaParser p; + EXPECT_TRUE(feed_str(p, "$GNGGA,000000.000,3351.60000,S,15112.00000,E,1,05,1.0,10.0,M,0.0,M,,*5E\r\n") == asr650x::NMEA_GGA); + EXPECT_TRUE(p.data().lat_e6 == -33860000 && p.data().lon_e6 == 151200000 && p.data().sats == 5); + } + /* RMC with a 2026 date */ + { + asr650x::NmeaParser p; + EXPECT_TRUE(feed_str(p, "$GNRMC,092750.000,A,5321.68020,N,00630.33720,W,0.02,31.66,041026,,,A*53\r\n") == asr650x::NMEA_RMC); + EXPECT_TRUE(p.data().rmc_valid && p.data().time_hhmmss == 92750 && p.data().date_ddmmyy == 41026); + EXPECT_TRUE(p.data().pos_valid && p.data().lat_e6 == 53361337); + EXPECT_TRUE(asr650x::gps_to_epoch(p.data().date_ddmmyy, p.data().time_hhmmss) == 1791106070u); /* 2026-10-04 09:27:50 UTC */ + } + /* no fix: empty fields, quality 0 / status V: never a valid position */ + { + asr650x::NmeaParser p; + EXPECT_TRUE(feed_str(p, "$GNGGA,,,,,,0,00,99.99,,,,,,*56\r\n") == asr650x::NMEA_GGA); + EXPECT_TRUE(!p.data().pos_valid && p.data().fix_quality == 0 && p.data().sats == 0); + EXPECT_TRUE(feed_str(p, "$GNRMC,,V,,,,,,,,,,N*4D\r\n") == asr650x::NMEA_RMC); + EXPECT_TRUE(!p.data().rmc_valid && !p.data().pos_valid); + } + /* a fix followed by a no-fix sentence invalidates the position */ + { + asr650x::NmeaParser p; + feed_str(p, "$GNGGA,092750.000,5321.68020,N,00630.33720,W,1,08,1.03,61.7,M,55.2,M,,*58\r\n"); + EXPECT_TRUE(p.data().pos_valid); + feed_str(p, "$GNGGA,,,,,,0,00,99.99,,,,,,*56\r\n"); + EXPECT_TRUE(!p.data().pos_valid); + } + /* bad checksum, missing '*', truncated, no terminator, garbage, resync on '$' */ + { + asr650x::NmeaParser p; + EXPECT_TRUE(feed_str(p, "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*48\r\n") == asr650x::NMEA_BAD); + EXPECT_TRUE(!p.data().pos_valid); + EXPECT_TRUE(feed_str(p, "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,\r\n") == asr650x::NMEA_BAD); + EXPECT_TRUE(feed_str(p, "$GPGGA,1235") == asr650x::NMEA_NONE); /* truncated: nothing until a terminator */ + EXPECT_TRUE(feed_str(p, "\r\n") == asr650x::NMEA_BAD); /* the truncated sentence ends here: rejected */ + EXPECT_TRUE(feed_str(p, "\x01\x02\xff garbage \x80\r\n") == asr650x::NMEA_NONE); /* noise outside a sentence is ignored */ + /* a '$' in the middle of a broken sentence restarts it */ + EXPECT_TRUE(feed_str(p, "$GPGGA,12$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n") == asr650x::NMEA_GGA); + EXPECT_TRUE(p.data().pos_valid && p.data().lat_e6 == 48117300); + EXPECT_TRUE(p.sentences_bad() >= 2); + } + /* overlong sentence: dropped, parser recovers */ + { + asr650x::NmeaParser p; + std::string longs = "$GPGGA,"; + longs.append(300, 'A'); + longs += "*00\r\n"; + EXPECT_TRUE(feed_str(p, longs) == asr650x::NMEA_BAD); + EXPECT_TRUE(feed_str(p, "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n") == asr650x::NMEA_GGA); + } + /* other sentence types are valid but ignored */ + { + asr650x::NmeaParser p; + EXPECT_TRUE(feed_str(p, "$GPGSV,1,1,00*79\r\n") == asr650x::NMEA_NONE); + EXPECT_TRUE(p.sentences_ok() == 1); + } + /* sentences with non-numeric junk in numeric fields must not become a position */ + { + asr650x::NmeaParser p; + const char* body = "GPGGA,123519,48x7.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,"; + char line[128]; + EXPECT_TRUE(asr650x::nmea_make(line, sizeof(line), body) > 0); + feed_str(p, line); + EXPECT_TRUE(!p.data().pos_valid); + } + + /* checksum helper */ + { + char out[128]; + int n = asr650x::nmea_make(out, sizeof(out), "PCAS03,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0"); + EXPECT_TRUE(n > 0 && std::strcmp(out, "$PCAS03,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0*02\r\n") == 0); + char small[8]; + EXPECT_TRUE(asr650x::nmea_make(small, sizeof(small), "PCAS03,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0") == 0); + } + + /* epoch conversion */ + EXPECT_TRUE(asr650x::gps_to_epoch(41026, 123456) == 1791117296u); /* 2026-10-04 12:34:56 */ + EXPECT_TRUE(asr650x::gps_to_epoch(10125, 0) == 1735689600u); /* 2025-01-01 00:00:00 */ + EXPECT_TRUE(asr650x::gps_to_epoch(280226, 235959) == 1772323199u); /* 2026-02-28 23:59:59 */ + EXPECT_TRUE(asr650x::gps_to_epoch(290228, 0) == 1835395200u); /* 2028-02-29 (leap day) */ + EXPECT_TRUE(asr650x::gps_to_epoch(290227, 0) == 0); /* 2027-02-29 does not exist */ + EXPECT_TRUE(asr650x::gps_to_epoch(310226, 0) == 0); /* Feb 31 */ + EXPECT_TRUE(asr650x::gps_to_epoch(230394, 123519) == 0); /* 2094: outside the accepted window */ + EXPECT_TRUE(asr650x::gps_to_epoch(41023, 0) == 0); /* 2023: before the window */ + EXPECT_TRUE(asr650x::gps_to_epoch(41026, 250000) == 0 && asr650x::gps_to_epoch(41026, 126000) == 0 && asr650x::gps_to_epoch(41026, 120060) == 0); + EXPECT_TRUE(asr650x::gps_to_epoch(0, 0) == 0 && asr650x::gps_to_epoch(41026, 0) != 0); + /* after a GPS power cycle the previous fix must not be reported (reviewer finding: stale RMC/GGA data) */ + { + asr650x::NmeaParser p; + EXPECT_TRUE(feed_str(p, "$GNGGA,092750.000,5321.68020,N,00630.33720,W,1,08,1.03,61.7,M,55.2,M,,*58\r\n") == asr650x::NMEA_GGA); + EXPECT_TRUE(p.data().pos_valid && p.data().fix_quality == 1); + p.reset_fix(); + EXPECT_TRUE(!p.data().pos_valid && !p.data().rmc_valid && p.data().fix_quality == 0 && p.data().sats == 0); + } +} + +// a sentence with a valid checksum but an impossible position must not be taken as a fix +TEST(Asr650xNmea, OutOfRangeLatLonRejected) { + const char* bodies[] = { + "GNGGA,092750.000,9530.00000,N,00630.33720,W,1,08,1.03,61.7,M,55.2,M,,", // latitude 95 deg + "GNGGA,092750.000,9000.00100,S,00630.33720,W,1,08,1.03,61.7,M,55.2,M,,", // just over 90 deg + "GNGGA,092750.000,5321.68020,N,18530.33720,E,1,08,1.03,61.7,M,55.2,M,,", // longitude 185 deg + }; + for (const char* b : bodies) { + asr650x::NmeaParser p; + char line[100]; + int n = asr650x::nmea_make(line, sizeof(line), b); + ASSERT_GT(n, 0); + asr650x::NmeaEvent last = asr650x::NMEA_NONE; + for (int i = 0; i < n; i++) { asr650x::NmeaEvent e = p.feed((uint8_t)line[i]); if (e != asr650x::NMEA_NONE) last = e; } + EXPECT_EQ(last, asr650x::NMEA_GGA) << b; + EXPECT_FALSE(p.data().pos_valid) << b; + } + // the limits themselves are valid + asr650x::NmeaParser p; + char line[100]; + int n = asr650x::nmea_make(line, sizeof(line), "GNGGA,092750.000,9000.00000,S,18000.00000,W,1,08,1.03,61.7,M,55.2,M,,"); + for (int i = 0; i < n; i++) p.feed((uint8_t)line[i]); + EXPECT_TRUE(p.data().pos_valid); + EXPECT_EQ(p.data().lat_e6, -90000000); + EXPECT_EQ(p.data().lon_e6, -180000000); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/test/test_asr650x_oled_text/test_asr650x_oled_text.cpp b/test/test_asr650x_oled_text/test_asr650x_oled_text.cpp new file mode 100644 index 0000000000..7f77a39705 --- /dev/null +++ b/test/test_asr650x_oled_text/test_asr650x_oled_text.cpp @@ -0,0 +1,48 @@ +#include +#include +#include +#include "helpers/asr650x/OledText.h" + +TEST(Asr650xOledText, Behaves) { + /* glyph table: space is blank, every printable glyph is non-blank, unknown characters map to '?' */ + const uint8_t* sp = asr650x::font5x7_glyph(' '); + for (int i = 0; i < 5; i++) EXPECT_TRUE(sp[i] == 0); + for (int c = 0x21; c <= 0x7E; c++) { + const uint8_t* g = asr650x::font5x7_glyph((char)c); + bool any = false; + for (int i = 0; i < 5; i++) any = any || g[i] != 0; + EXPECT_TRUE(any); + } + EXPECT_TRUE(std::memcmp(asr650x::font5x7_glyph('\x01'), asr650x::font5x7_glyph('?'), 5) == 0); + EXPECT_TRUE(std::memcmp(asr650x::font5x7_glyph((char)0xC3), asr650x::font5x7_glyph('?'), 5) == 0); /* UTF-8 bytes show as '?' */ + EXPECT_TRUE(std::memcmp(asr650x::font5x7_glyph('\x7f'), asr650x::font5x7_glyph('?'), 5) == 0); + /* distinct shapes for look-alikes */ + EXPECT_TRUE(std::memcmp(asr650x::font5x7_glyph('0'), asr650x::font5x7_glyph('O'), 5) != 0); + EXPECT_TRUE(std::memcmp(asr650x::font5x7_glyph('l'), asr650x::font5x7_glyph('I'), 5) != 0); + + /* one page = 128 columns: 5 glyph columns + 1 gap per character */ + uint8_t page[128]; + asr650x::render_line("A", page); + for (int i = 0; i < 5; i++) EXPECT_TRUE(page[i] == asr650x::font5x7_glyph('A')[i]); + EXPECT_TRUE(page[5] == 0); + for (int i = 6; i < 128; i++) EXPECT_TRUE(page[i] == 0); + asr650x::render_line("AB", page); + for (int i = 0; i < 5; i++) EXPECT_TRUE(page[6 + i] == asr650x::font5x7_glyph('B')[i]); + /* empty and null text give a blank page */ + asr650x::render_line("", page); + for (int i = 0; i < 128; i++) EXPECT_TRUE(page[i] == 0); + asr650x::render_line(nullptr, page); + for (int i = 0; i < 128; i++) EXPECT_TRUE(page[i] == 0); + /* more than 21 characters: cut, last two columns stay blank */ + asr650x::render_line("ABCDEFGHIJKLMNOPQRSTUVWXYZ", page); + EXPECT_TRUE(page[126] == 0 && page[127] == 0); + for (int i = 0; i < 5; i++) EXPECT_TRUE(page[20 * 6 + i] == asr650x::font5x7_glyph('U')[i]); + /* a long run of UTF-8 does not overrun the buffer */ + asr650x::render_line("\xe1\xbb\x87\xe1\xbb\x87\xe1\xbb\x87\xe1\xbb\x87\xe1\xbb\x87\xe1\xbb\x87\xe1\xbb\x87\xe1\xbb\x87\xe1\xbb\x87", page); + EXPECT_TRUE(page[127] == 0); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/test/test_asr650x_persist/test_asr650x_persist.cpp b/test/test_asr650x_persist/test_asr650x_persist.cpp new file mode 100644 index 0000000000..5961776c3c --- /dev/null +++ b/test/test_asr650x_persist/test_asr650x_persist.cpp @@ -0,0 +1,74 @@ +#include +#include +#include +#include "helpers/asr650x/FlashRecord.h" + +static asr650x::PersistState sample() { + asr650x::PersistState s; memset(&s, 0, sizeof(s)); + s.has_identity = true; + for (int i = 0; i < 64; i++) s.prv[i] = (uint8_t)(i + 1); + for (int i = 0; i < 32; i++) s.pub[i] = (uint8_t)(0x80 + i); + s.has_prefs = true; + strcpy(s.node_name, "KTM-test"); + s.freq = 921.5f; s.bw = 62.5f; s.sf = 7; s.cr = 5; s.tx_power_dbm = 14; + return s; +} + +TEST(Asr650xFlashRecord, Behaves) { + uint8_t buf[asr650x::PERSIST_SIZE]; + EXPECT_TRUE(asr650x::crc32((const uint8_t*)"123456789", 9) == 0xCBF43926u); // standard CRC-32 check value + + asr650x::PersistState a = sample(), b; + asr650x::persist_encode(a, buf); + EXPECT_TRUE(asr650x::persist_decode(buf, b)); + EXPECT_TRUE(b.has_identity && memcmp(b.prv, a.prv, 64) == 0 && memcmp(b.pub, a.pub, 32) == 0); + EXPECT_TRUE(b.has_prefs && strcmp(b.node_name, "KTM-test") == 0); + EXPECT_TRUE(b.freq == 921.5f && b.bw == 62.5f && b.sf == 7 && b.cr == 5 && b.tx_power_dbm == 14); + + /* identity only / prefs only */ + a.has_prefs = false; asr650x::persist_encode(a, buf); + EXPECT_TRUE(asr650x::persist_decode(buf, b) && b.has_identity && !b.has_prefs); + a = sample(); a.has_identity = false; asr650x::persist_encode(a, buf); + EXPECT_TRUE(asr650x::persist_decode(buf, b) && !b.has_identity && b.has_prefs); + + /* every single-byte corruption must be rejected and reset the state to empty */ + a = sample(); asr650x::persist_encode(a, buf); + for (size_t i = 0; i < asr650x::PERSIST_SIZE; i++) { + uint8_t bad[asr650x::PERSIST_SIZE]; memcpy(bad, buf, sizeof(bad)); + bad[i] ^= 0x01; + asr650x::PersistState c = sample(); + EXPECT_TRUE(!asr650x::persist_decode(bad, c)); + EXPECT_TRUE(!c.has_identity && !c.has_prefs); + } + /* erased flash (all 0x00 / all 0xFF) is not a valid record */ + uint8_t z[asr650x::PERSIST_SIZE]; memset(z, 0x00, sizeof(z)); EXPECT_TRUE(!asr650x::persist_decode(z, b)); + memset(z, 0xFF, sizeof(z)); EXPECT_TRUE(!asr650x::persist_decode(z, b)); + /* a name without NUL terminator in the record is terminated on decode */ + a = sample(); memset(a.node_name, 'A', 32); asr650x::persist_encode(a, buf); + EXPECT_TRUE(asr650x::persist_decode(buf, b) && b.node_name[31] == 0); + /* GPS settings round-trip and are covered by the CRC */ + { + asr650x::PersistState g = sample(); + g.gps_off = true; g.gps_interval = 3600; + asr650x::persist_encode(g, buf); + EXPECT_TRUE(buf[147] == 1); + EXPECT_TRUE(buf[152] == (3600 & 0xFF) && buf[153] == ((3600 >> 8) & 0xFF) && buf[154] == 0 && buf[155] == 0); + asr650x::PersistState h; + EXPECT_TRUE(asr650x::persist_decode(buf, h) && h.gps_off && h.gps_interval == 3600); + buf[153] ^= 1; + EXPECT_TRUE(!asr650x::persist_decode(buf, h)); // corrupting the interval byte is detected + } + /* a legacy record (bytes 147 and 152..155 are zero) reads as 'GPS on, interval 0' */ + { + asr650x::PersistState legacy = sample(); + legacy.gps_off = false; legacy.gps_interval = 0; + asr650x::persist_encode(legacy, buf); + asr650x::PersistState r; + EXPECT_TRUE(asr650x::persist_decode(buf, r) && !r.gps_off && r.gps_interval == 0); + } +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/test/test_asr650x_persist_store/test_asr650x_persist_store.cpp b/test/test_asr650x_persist_store/test_asr650x_persist_store.cpp new file mode 100644 index 0000000000..a304db6b85 --- /dev/null +++ b/test/test_asr650x_persist_store/test_asr650x_persist_store.cpp @@ -0,0 +1,100 @@ +#include +#include +#include +#include "helpers/asr650x/FlashRecordStore.h" + +// RAM-backed fake of the SFLASH user rows: two 256-byte rows, a record per row. +struct FakeBackend { + uint8_t mem[512]; + bool fail_next_write; + size_t torn_bytes; // if > 0: only this many bytes of the next write land (simulated power loss) + int writes; + size_t last_off; + FakeBackend() : fail_next_write(false), torn_bytes(0), writes(0), last_off(0) { memset(mem, 0xFF, sizeof(mem)); } + void read(size_t off, uint8_t* buf, size_t n) { memcpy(buf, mem + off, n); } + bool write(size_t off, const uint8_t* buf, size_t n) { + writes++; last_off = off; + if (fail_next_write) { fail_next_write = false; return false; } + if (torn_bytes) { memcpy(mem + off, buf, torn_bytes); torn_bytes = 0; return false; } + memcpy(mem + off, buf, n); + return true; + } +}; + +static asr650x::PersistState with_identity(uint8_t fill) { + asr650x::PersistState s; memset(&s, 0, sizeof(s)); + s.has_identity = true; + memset(s.prv, fill, 64); memset(s.pub, fill ^ 0x5A, 32); + return s; +} + +TEST(Asr650xFlashRecordStore, Behaves) { + FakeBackend be; + asr650x::PersistStore st(be); + st.begin(); + EXPECT_TRUE(!st.state().has_identity && !st.state().has_prefs); + + /* identity persists across a new store instance, alternating rows */ + EXPECT_TRUE(st.commit(with_identity(1))); + EXPECT_TRUE(be.last_off == 0); + asr650x::PersistState s2 = st.state(); s2.has_prefs = true; strcpy(s2.node_name, "one"); + EXPECT_TRUE(st.commit(s2)); + EXPECT_TRUE(be.last_off == 256); + { + asr650x::PersistStore again(be); again.begin(); + EXPECT_TRUE(again.state().has_identity && again.state().prv[0] == 1); + EXPECT_TRUE(again.state().has_prefs && strcmp(again.state().node_name, "one") == 0); + } + + /* unchanged commit performs no write */ + int w = be.writes; + EXPECT_TRUE(st.commit(st.state())); + EXPECT_TRUE(be.writes == w); + + /* review I1: a failed commit must not change state, and must not leak into the next commit */ + asr650x::PersistState want_key2 = st.state(); memset(want_key2.prv, 2, 64); + be.fail_next_write = true; + EXPECT_TRUE(!st.commit(want_key2)); + EXPECT_TRUE(st.state().prv[0] == 1); + asr650x::PersistState rename = st.state(); strcpy(rename.node_name, "two"); + EXPECT_TRUE(st.commit(rename)); + { + asr650x::PersistStore again(be); again.begin(); + EXPECT_TRUE(again.state().prv[0] == 1); // the rejected key never reached flash + EXPECT_TRUE(strcmp(again.state().node_name, "two") == 0); + } + + /* review I2: power loss while writing the newer row keeps the previous record intact */ + asr650x::PersistState before = st.state(); + asr650x::PersistState next = st.state(); strcpy(next.node_name, "three"); + be.torn_bytes = 70; + EXPECT_TRUE(!st.commit(next)); + { + asr650x::PersistStore again(be); again.begin(); + EXPECT_TRUE(again.state().has_identity && memcmp(again.state().prv, before.prv, 64) == 0); + EXPECT_TRUE(strcmp(again.state().node_name, "two") == 0); + /* and the store keeps working afterwards (writes the row that was torn) */ + EXPECT_TRUE(again.commit(next)); + asr650x::PersistStore third(be); third.begin(); + EXPECT_TRUE(strcmp(third.state().node_name, "three") == 0); + } + + /* both rows corrupt -> empty */ + FakeBackend bad; + memset(bad.mem, 0xA5, sizeof(bad.mem)); + asr650x::PersistStore sb(bad); sb.begin(); + EXPECT_TRUE(!sb.state().has_identity && !sb.state().has_prefs); + EXPECT_TRUE(sb.commit(with_identity(9))); + asr650x::PersistStore sb2(bad); sb2.begin(); + EXPECT_TRUE(sb2.state().has_identity && sb2.state().prv[0] == 9); + + /* sequence number wrap: 0 is newer than 0xFFFFFFFF */ + EXPECT_TRUE(asr650x::seq_newer(0u, 0xFFFFFFFFu)); + EXPECT_TRUE(!asr650x::seq_newer(0xFFFFFFFFu, 0u)); + EXPECT_TRUE(asr650x::seq_newer(5u, 4u) && !asr650x::seq_newer(4u, 5u) && !asr650x::seq_newer(7u, 7u)); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/test/test_asr650x_radio_params/test_asr650x_radio_params.cpp b/test/test_asr650x_radio_params/test_asr650x_radio_params.cpp new file mode 100644 index 0000000000..eb579db831 --- /dev/null +++ b/test/test_asr650x_radio_params/test_asr650x_radio_params.cpp @@ -0,0 +1,58 @@ +#include +#include +#include +#include +#include "helpers/asr650x/RadioParams.h" + +// Independent reference: Semtech SX1262 datasheet time-on-air (explicit header, CRC on), in microseconds. +static double ref_airtime_us(int len, double bw_khz, int sf, int cr, int preamble) { + double tsym = (double)(1 << sf) / (bw_khz * 1000.0); // seconds + int de = (tsym * 1000.0 >= 16.38) ? 1 : 0; // low data-rate optimisation, same rule as the Heltec driver (radio.c) + double tpre = (preamble + 4.25) * tsym; + double num = 8.0 * len - 4.0 * sf + 28 + 16 /*CRC*/ - 0 /*explicit header*/; + double den = 4.0 * (sf - 2 * de); + double npay = 8 + std::max(std::ceil(num / den) * cr, 0.0); + return (tpre + npay * tsym) * 1e6; +} + +TEST(Asr650xRadioParams, Behaves) { + /* bandwidth mapping: exact, nearest, extremes */ + EXPECT_TRUE(asr650x::bw_index(62.5f) == 3 && asr650x::bw_index(125.0f) == 0 && asr650x::bw_index(250.0f) == 1 && asr650x::bw_index(500.0f) == 2); + EXPECT_TRUE(asr650x::bw_index(41.67f) == 4 && asr650x::bw_index(31.25f) == 5 && asr650x::bw_index(20.83f) == 6); + EXPECT_TRUE(asr650x::bw_index(15.63f) == 7 && asr650x::bw_index(10.42f) == 8 && asr650x::bw_index(7.81f) == 9); + EXPECT_TRUE(asr650x::bw_index(100.0f) == 0); // nearest of {62.5, 125} by ratio is 125 (100/62.5 = 1.6 > 125/100 = 1.25) + EXPECT_TRUE(asr650x::bw_index(7.0f) == 9); // below range -> smallest + EXPECT_TRUE(asr650x::bw_index(900.0f) == 2); // above range -> largest + EXPECT_TRUE(asr650x::bw_index(0.0f) == 9 && asr650x::bw_index(-5.0f) == 9); // nonsense never indexes out of the table + EXPECT_TRUE(std::fabs(asr650x::bw_value(3) - 62.5f) < 1e-3 && std::fabs(asr650x::bw_value(0) - 125.0f) < 1e-3); + + /* coding rate / sf / preamble. cr arguments use the MeshCore convention 5..8, the result is the Heltec 1..4 */ + EXPECT_TRUE(asr650x::cr_index(5) == 1 && asr650x::cr_index(8) == 4 && asr650x::cr_index(4) == 1 && asr650x::cr_index(9) == 4 && asr650x::cr_index(0) == 1); + EXPECT_TRUE(asr650x::sf_clamp(4) == 5 && asr650x::sf_clamp(13) == 12 && asr650x::sf_clamp(8) == 8); + EXPECT_TRUE(asr650x::preamble_for_sf(7) == 32 && asr650x::preamble_for_sf(8) == 32 && asr650x::preamble_for_sf(9) == 16 && asr650x::preamble_for_sf(12) == 16); + + /* airtime within 1.5 ms of the independent reference, for the production setting and a spread of others */ + struct { int len; double bw; int sf, cr, pre; } cases[] = { + {110, 62.5, 8, 5, 32}, {1, 62.5, 8, 5, 32}, {255, 62.5, 8, 5, 32}, {110, 125, 7, 5, 32}, + {110, 250, 10, 5, 16}, {50, 125, 12, 8, 16}, {255, 500, 7, 6, 32}, {20, 62.5, 11, 5, 16}, + {20, 15.63, 8, 5, 32} /* 16.378 ms symbols: the driver keeps LDRO off */ + }; + for (auto& c : cases) { + double ref = ref_airtime_us(c.len, c.bw, c.sf, c.cr, c.pre) / 1000.0; // multiplier is CR+4 = 5..8 + double got = asr650x::airtime_ms(c.len, (float)c.bw, (uint8_t)c.sf, (uint8_t)c.cr, (uint16_t)c.pre); + EXPECT_NEAR(ref, got, 1.5) << "len=" << c.len << " bw=" << c.bw << " sf=" << c.sf << " cr=" << c.cr; + } + EXPECT_TRUE(asr650x::airtime_ms(0, 62.5f, 8, 5, 32) > 0); // an empty packet still has preamble + header + + /* packet score */ + EXPECT_TRUE(asr650x::packet_score(-20.0f, 8, 100) == 0.0f); // below the SF8 threshold (-10 dB) + EXPECT_TRUE(asr650x::packet_score(0.0f, 6, 100) == 0.0f); // unsupported SF + float good = asr650x::packet_score(9.0f, 8, 110); + EXPECT_TRUE(good > 0.0f && good <= 1.0f); + EXPECT_TRUE(asr650x::packet_score(9.0f, 8, 10) > asr650x::packet_score(9.0f, 8, 200)); // longer packets score lower +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/test/test_asr650x_status/test_asr650x_status.cpp b/test/test_asr650x_status/test_asr650x_status.cpp new file mode 100644 index 0000000000..4ea4aa1de0 --- /dev/null +++ b/test/test_asr650x_status/test_asr650x_status.cpp @@ -0,0 +1,81 @@ +#include +#include +#include +#include +#include "helpers/asr650x/StatusLines.h" + +static asr650x::StatusInput base() { + asr650x::StatusInput in; + std::memset(&in, 0, sizeof(in)); + in.name = "HELTEC-M3"; + in.id4[0] = 0xab; in.id4[1] = 0x26; in.id4[2] = 0x86; in.id4[3] = 0x31; + in.freq_khz = 920250; in.sf = 8; + in.max_contacts = 12; + return in; +} +static std::string line(int i, const asr650x::StatusInput& in) { char o[22]; asr650x::status_line(i, in, o); return o; } + +TEST(Asr650xStatusLines, Behaves) { + asr650x::StatusInput in = base(); + EXPECT_TRUE(line(0, in) == "HELTEC-M3"); + EXPECT_TRUE(line(1, in) == "ID ab268631"); + EXPECT_TRUE(line(2, in) == "--:--:-- no time"); /* clock never synced */ + EXPECT_TRUE(line(3, in) == "GPS off"); + EXPECT_TRUE(line(4, in) == "LAT --"); + EXPECT_TRUE(line(5, in) == "LON --"); + EXPECT_TRUE(line(6, in) == "BAT -- CT 0/12"); /* battery not measured yet */ + EXPECT_TRUE(line(7, in) == "920.250MHz SF8"); + + in.time_valid = true; in.epoch = 1791117296u; /* 2026-10-04 12:34:56 UTC */ + EXPECT_TRUE(line(2, in) == "12:34:56 UTC"); + in.epoch = 1735689600u; /* midnight */ + EXPECT_TRUE(line(2, in) == "00:00:00 UTC"); + in.epoch = 1735689600u + 86399u; + EXPECT_TRUE(line(2, in) == "23:59:59 UTC"); + + in.gps_state = 1; in.warming_s = 45; + EXPECT_TRUE(line(3, in) == "GPS search 45s"); + in.warming_s = 100000; /* absurd: still within 21 characters */ + EXPECT_TRUE(line(3, in).size() <= 21); + in.gps_state = 2; in.sats = 8; in.pos_valid = true; + EXPECT_TRUE(line(3, in) == "GPS fix 8 sats"); + in.lat_e6 = 21028511; in.lon_e6 = 105804817; + EXPECT_TRUE(line(4, in) == "LAT 21.028511"); + EXPECT_TRUE(line(5, in) == "LON 105.804817"); + in.lat_e6 = -33860000; in.lon_e6 = -6505620; + EXPECT_TRUE(line(4, in) == "LAT -33.860000"); + EXPECT_TRUE(line(5, in) == "LON -6.505620"); + in.lat_e6 = -50000; in.lon_e6 = 5; /* -0.05 degrees keeps its sign, tiny values pad */ + EXPECT_TRUE(line(4, in) == "LAT -0.050000"); + EXPECT_TRUE(line(5, in) == "LON 0.000005"); + in.pos_valid = false; + EXPECT_TRUE(line(4, in) == "LAT --"); + in.gps_state = 3; + EXPECT_TRUE(line(3, in) == "GPS timeout"); + + in.batt_mv = 3981; in.contacts = 3; + EXPECT_TRUE(line(6, in) == "BAT 3.98V CT 3/12"); + in.batt_mv = 5000; in.contacts = 255; in.max_contacts = 255; + EXPECT_TRUE(line(6, in) == "BAT 5.00V CT 255/255"); + in.batt_mv = 65535; + EXPECT_TRUE(line(6, in).size() <= 21); + + in.freq_khz = 915000; in.sf = 12; + EXPECT_TRUE(line(7, in) == "915.000MHz SF12"); + + /* long or odd names are cut to 21 characters; control characters and a null name are safe */ + in.name = "A-VERY-LONG-NODE-NAME-THAT-DOES-NOT-FIT-ON-THE-SCREEN"; + EXPECT_TRUE(line(0, in) == "A-VERY-LONG-NODE-NAME"); /* exactly 21 characters */ + in.name = nullptr; + EXPECT_TRUE(line(0, in) == ""); + in.name = "tab\there\n"; + for (char c : line(0, in)) EXPECT_TRUE((unsigned char)c >= 0x20 && (unsigned char)c < 0x7f); + /* out-of-range line index gives an empty line */ + EXPECT_TRUE(line(8, in) == "" && line(-1, in) == ""); + for (int i = 0; i < 8; i++) EXPECT_TRUE(line(i, in).size() <= 21); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/test/test_asr650x_strtod/test_asr650x_strtod.cpp b/test/test_asr650x_strtod/test_asr650x_strtod.cpp new file mode 100644 index 0000000000..a18b85cb4a --- /dev/null +++ b/test/test_asr650x_strtod/test_asr650x_strtod.cpp @@ -0,0 +1,41 @@ +#include +#include +#include "helpers/asr650x/SmallStrtod.h" + +// The CLI parses numbers such as radio frequencies ("869.618"), bandwidths ("62.5"), dB values ("-3.5") and factors. +TEST(Asr650xStrtod, MatchesLibcOnCliStyleNumbers) { + const char* in[] = {"0", "1", "-1", "869.618", "62.5", "-3.5", "+7", "0.000001", "123456789", "1e3", "2.5E-2", + " 42", "920.25,62.5", ".5", "5.", "-0.0"}; + for (const char* s : in) { + char *e1, *e2; + double want = strtod(s, &e1); + double got = asr650x_strtod(s, &e2); + EXPECT_NEAR(got, want, 1e-9 * (want < 0 ? -want : want) + 1e-12) << s; + EXPECT_EQ(e2 - s, e1 - s) << s; + } +} + +TEST(Asr650xStrtod, NoDigitsConsumesNothing) { + const char* in[] = {"", "abc", "-", ".", "e5", "+."}; + for (const char* s : in) { + char* e; + EXPECT_EQ(asr650x_strtod(s, &e), 0.0) << s; + EXPECT_EQ(e, s) << s; + } +} + +TEST(Asr650xStrtod, ExponentWithoutDigitsIsNotConsumed) { + char* e; + const char* s = "12e"; + EXPECT_EQ(asr650x_strtod(s, &e), 12.0); + EXPECT_EQ(e - s, 2); +} + +TEST(Asr650xStrtod, NullEndPointerIsAllowed) { + EXPECT_EQ(asr650x_strtod("3.25", nullptr), 3.25); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} From e0bd82b0435ca3a8b6567540fb19523d8ad65757 Mon Sep 17 00:00:00 2001 From: HAN Date: Wed, 7 Oct 2026 00:06:50 +0700 Subject: [PATCH 10/16] Add Heltec CubeCell AB02S variant: legacy ASR6502, USB/UART companion with OLED status and GPS Legacy board: ASR6502 (Cortex-M0+), 128 KB flash, 16 KB RAM, fixed 2 KB stack. Companion over USB/UART only (no BLE, no WiFi). The node does not generate its own identity: import a private key from the app. - 8-line SSD1306 status screen on the raw PSoC I2C block (Wire's buffers do not fit in RAM), auto-off after 30 s, USER key wakes it - Air530Z GPS: powered when a fix is wanted, off after the fix, $PCAS03 keeps only GGA+RMC, sets the clock from RMC; 'gps' / 'gps_interval' through the custom-var commands - contacts in RAM (12), identity and prefs in SFLASH --- examples/companion_radio/DataStore.h | 6 + examples/companion_radio/FlashDataStore.cpp | 79 ++++++++++++ examples/companion_radio/FlashDataStore.h | 40 ++++++ examples/companion_radio/main.cpp | 5 + .../heltec_cubecell_ab02s/CubeCellBoard.cpp | 88 ++++++++++++++ .../heltec_cubecell_ab02s/CubeCellBoard.h | 30 +++++ .../heltec_cubecell_ab02s/CubeCellGPS.cpp | 91 ++++++++++++++ variants/heltec_cubecell_ab02s/CubeCellGPS.h | 38 ++++++ .../heltec_cubecell_ab02s/CubeCellSensors.cpp | 47 ++++++++ .../heltec_cubecell_ab02s/CubeCellSensors.h | 18 +++ variants/heltec_cubecell_ab02s/Oled.cpp | 114 ++++++++++++++++++ variants/heltec_cubecell_ab02s/Oled.h | 20 +++ .../heltec_cubecell_ab02s/StatusScreen.cpp | 49 ++++++++ variants/heltec_cubecell_ab02s/StatusScreen.h | 32 +++++ .../heltec_cubecell_ab02s/c_include_order.py | 8 ++ variants/heltec_cubecell_ab02s/platformio.ini | 72 +++++++++++ .../heltec_cubecell_ab02s/shims/CayenneLPP.h | 29 +++++ variants/heltec_cubecell_ab02s/shims/RTClib.h | 2 + variants/heltec_cubecell_ab02s/target.cpp | 45 +++++++ variants/heltec_cubecell_ab02s/target.h | 26 ++++ 20 files changed, 839 insertions(+) create mode 100644 examples/companion_radio/FlashDataStore.cpp create mode 100644 examples/companion_radio/FlashDataStore.h create mode 100644 variants/heltec_cubecell_ab02s/CubeCellBoard.cpp create mode 100644 variants/heltec_cubecell_ab02s/CubeCellBoard.h create mode 100644 variants/heltec_cubecell_ab02s/CubeCellGPS.cpp create mode 100644 variants/heltec_cubecell_ab02s/CubeCellGPS.h create mode 100644 variants/heltec_cubecell_ab02s/CubeCellSensors.cpp create mode 100644 variants/heltec_cubecell_ab02s/CubeCellSensors.h create mode 100644 variants/heltec_cubecell_ab02s/Oled.cpp create mode 100644 variants/heltec_cubecell_ab02s/Oled.h create mode 100644 variants/heltec_cubecell_ab02s/StatusScreen.cpp create mode 100644 variants/heltec_cubecell_ab02s/StatusScreen.h create mode 100644 variants/heltec_cubecell_ab02s/c_include_order.py create mode 100644 variants/heltec_cubecell_ab02s/platformio.ini create mode 100644 variants/heltec_cubecell_ab02s/shims/CayenneLPP.h create mode 100644 variants/heltec_cubecell_ab02s/shims/RTClib.h create mode 100644 variants/heltec_cubecell_ab02s/target.cpp create mode 100644 variants/heltec_cubecell_ab02s/target.h diff --git a/examples/companion_radio/DataStore.h b/examples/companion_radio/DataStore.h index e0a145caf1..dc52a31f3e 100644 --- a/examples/companion_radio/DataStore.h +++ b/examples/companion_radio/DataStore.h @@ -1,5 +1,9 @@ #pragma once +#if defined(ASR650X_PLATFORM) + #include "FlashDataStore.h" // no filesystem: identity + prefs in two SFLASH rows, contacts in RAM +#else + #include #include #include @@ -53,3 +57,5 @@ class DataStore { private: FILESYSTEM* _getContactsChannelsFS() const { if (_fsExtra) return _fsExtra; return _fs;}; }; + +#endif diff --git a/examples/companion_radio/FlashDataStore.cpp b/examples/companion_radio/FlashDataStore.cpp new file mode 100644 index 0000000000..fdf61c0790 --- /dev/null +++ b/examples/companion_radio/FlashDataStore.cpp @@ -0,0 +1,79 @@ +#if defined(ASR650X_PLATFORM) // the other boards use DataStore.cpp (filesystem) +#include "FlashDataStore.h" +#include "target.h" +#include + +namespace { +struct CommitArgs { asr650x::PersistStore* store; const asr650x::PersistState* cand; bool ok; }; +void run_commit(void* p) { CommitArgs* a = (CommitArgs*)p; a->ok = a->store->commit(*a->cand); } +} + +bool DataStore::commit_big(const asr650x::PersistState& c) { + CommitArgs a = { &_store, &c, false }; + asr650x_call_on_big_stack(run_commit, &a); + return a.ok; +} + +void DataStore::begin() { + _store.begin(); + asr650x_set_tx_allowed(_store.state().has_identity); // never transmit as the unprovisioned placeholder +} + +bool DataStore::formatFileSystem() { + asr650x::PersistState empty; + memset(&empty, 0, sizeof(empty)); + bool ok = commit_big(empty); + if (ok) asr650x_set_tx_allowed(false); + return ok; +} + +bool DataStore::loadMainIdentity(mesh::LocalIdentity& identity) { + const asr650x::PersistState& s = _store.state(); + if (!s.has_identity) return false; + uint8_t raw[96]; + memcpy(raw, s.prv, 64); + memcpy(raw + 64, s.pub, 32); + identity.readFrom(raw, sizeof(raw)); + return true; +} + +bool DataStore::saveMainIdentity(const mesh::LocalIdentity& identity) { + if (asr650x_identity_is_placeholder(identity)) { // never persist the placeholder ... + asr650x_set_tx_allowed(false); // ... and never transmit as it (even if a real key was loaded before) + return true; + } + mesh::LocalIdentity copy = identity; + uint8_t raw[96]; + if (copy.writeTo(raw, sizeof(raw)) != sizeof(raw)) return false; + asr650x::PersistState c = _store.state(); + c.has_identity = true; + memcpy(c.prv, raw, 64); + memcpy(c.pub, raw + 64, 32); + bool ok = commit_big(c); + if (ok) asr650x_set_tx_allowed(true); + return ok; +} + +void DataStore::loadPrefs(NodePrefs& prefs) { + const asr650x::PersistState& s = _store.state(); + if (!s.has_prefs) { prefs.gps_enabled = 1; return; } // default: GPS on + prefs.gps_enabled = s.gps_off ? 0 : 1; + prefs.gps_interval = s.gps_interval; + strncpy(prefs.node_name, s.node_name, sizeof(prefs.node_name) - 1); + prefs.node_name[sizeof(prefs.node_name) - 1] = 0; + prefs.freq = s.freq; prefs.bw = s.bw; + prefs.sf = s.sf; prefs.cr = s.cr; prefs.tx_power_dbm = s.tx_power_dbm; +} + +bool DataStore::savePrefs(NodePrefs& prefs) { + asr650x::PersistState c = _store.state(); + c.has_prefs = true; + c.gps_off = prefs.gps_enabled == 0; + c.gps_interval = prefs.gps_interval; + strncpy(c.node_name, prefs.node_name, sizeof(c.node_name) - 1); + c.node_name[sizeof(c.node_name) - 1] = 0; + c.freq = prefs.freq; c.bw = prefs.bw; + c.sf = prefs.sf; c.cr = prefs.cr; c.tx_power_dbm = prefs.tx_power_dbm; + return commit_big(c); +} +#endif // ASR650X_PLATFORM diff --git a/examples/companion_radio/FlashDataStore.h b/examples/companion_radio/FlashDataStore.h new file mode 100644 index 0000000000..82dc9ccc47 --- /dev/null +++ b/examples/companion_radio/FlashDataStore.h @@ -0,0 +1,40 @@ +#pragma once +#include +#include +#include +#include "NodePrefs.h" +#include +#include + +class DataStoreHost { +public: + virtual bool onContactLoaded(const ContactInfo& contact) = 0; + virtual bool getContactForSave(uint32_t idx, ContactInfo& contact) = 0; + virtual bool onChannelLoaded(uint8_t channel_idx, const ChannelDetails& ch) = 0; + virtual bool getChannelForSave(uint8_t channel_idx, ChannelDetails& ch) = 0; +}; + +// Same surface MyMesh uses from MeshCore's file-based DataStore. Identity + name + radio params go to +// EEPROM (CcPersist record); contacts/channels/blobs are RAM-only by design (M1 scope). +class DataStore { + FlashRowBackend _be; + asr650x::PersistStore _store; + bool commit_big(const asr650x::PersistState& c); // runs the flash commit on the crypto stack (main stack is only 2 KB) +public: + DataStore() : _store(_be) {} + void begin(); + bool formatFileSystem(); // factory reset: erases identity + prefs record + bool loadMainIdentity(mesh::LocalIdentity& identity); + bool saveMainIdentity(const mesh::LocalIdentity& identity); + void loadPrefs(NodePrefs& prefs); + bool savePrefs(NodePrefs& prefs); + void loadContacts(DataStoreHost*) {} + void saveContacts(DataStoreHost*, bool (*filter)(const ContactInfo& c) = NULL) {} + void loadChannels(DataStoreHost*) {} + void saveChannels(DataStoreHost*) {} + uint8_t getBlobByKey(const uint8_t[], int, uint8_t[]) { return 0; } + bool putBlobByKey(const uint8_t[], int, const uint8_t[], uint8_t) { return false; } + bool deleteBlobByKey(const uint8_t[], int) { return false; } + uint32_t getStorageUsedKb() const { return 0; } + uint32_t getStorageTotalKb() const { return 0; } +}; diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 839d495833..a28b9661fc 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -104,6 +104,8 @@ MultiSerialInterface interface_manager; #elif defined(ESP32) #include DataStore store(SPIFFS, rtc_clock); +#elif defined(ASR650X_PLATFORM) + DataStore store; // SFLASH rows, no filesystem #endif /* GLOBAL OBJECTS */ @@ -181,6 +183,9 @@ void setup() { SPIFFS.begin(true); store.begin(); the_mesh.begin(); +#elif defined(ASR650X_PLATFORM) + store.begin(); + the_mesh.begin(); #else #error "need to define filesystem" #endif diff --git a/variants/heltec_cubecell_ab02s/CubeCellBoard.cpp b/variants/heltec_cubecell_ab02s/CubeCellBoard.cpp new file mode 100644 index 0000000000..d5a88786a6 --- /dev/null +++ b/variants/heltec_cubecell_ab02s/CubeCellBoard.cpp @@ -0,0 +1,88 @@ +#include "target.h" +#include "Oled.h" +#include "StatusScreen.h" +#include "CubeCellGPS.h" +#include +#include + +#ifndef BIGSTACK_SIZE + #define BIGSTACK_SIZE 2048 +#endif + +static OledPanel oled; +static StatusScreen screen; +static uint32_t last_input_ms = 0; + +void CubeCellBoard::begin() { + // Ed25519 needs more than the fixed 2 KB main stack: it runs on a stack taken from the heap (BigStack.h). + // Without it, signing would overflow the main stack, so stop here (the app sees no reply). + if (!asr650x_bigstack_init(BIGSTACK_SIZE)) { + while (true) delay(1000); + } + bool ok = oled.begin(); + sensors.setOledStatus(ok ? "ok" : "none"); + screen.begin(&oled); +#ifdef ASR650X_STACK_WATCH + asr650x_stack_paint(); +#endif +} + +static void update_status_input(uint32_t now) { + asr650x::StatusInput in; + memset(&in, 0, sizeof(in)); + NodePrefs* prefs = the_mesh.getNodePrefs(); + const asr650x::GpsData& g = cubecell_gps.data(); + in.name = prefs->node_name; + memcpy(in.id4, the_mesh.self_id.pub_key, 4); + in.time_valid = rtc_clock.synced(); + in.epoch = rtc_clock.getCurrentTime(); + if (!cubecell_gps.enabled()) in.gps_state = 0; + else if (cubecell_gps.state() == asr650x::GPS_WARMING) in.gps_state = 1; + else if (g.pos_valid) in.gps_state = 2; + else if (cubecell_gps.timed_out()) in.gps_state = 3; + in.sats = g.sats; + in.pos_valid = g.pos_valid; + in.lat_e6 = g.lat_e6; + in.lon_e6 = g.lon_e6; + int n = the_mesh.getNumContacts(); + in.contacts = (uint8_t)(n < 0 ? 0 : (n > 255 ? 255 : n)); + in.max_contacts = (uint8_t)(MAX_CONTACTS > 255 ? 255 : MAX_CONTACTS); + in.freq_khz = (uint32_t)(prefs->freq * 1000.0f + 0.5f); + in.sf = prefs->sf; + in.warming_s = cubecell_gps.warming_s(); + screen.set_input(in); +} + +void CubeCellBoard::loop() { + uint32_t now = millis(); + if (now - last_input_ms >= 1000) { + last_input_ms = now; + update_status_input(now); + } + screen.loop(now); +} + +#ifdef ASR650X_STACK_WATCH +extern "C" { extern uint32_t __cy_stack_limit[]; extern uint32_t __cy_stack[]; void* sbrk(int); } + +void asr650x_stack_paint() { + uint32_t* sp; + __asm volatile("mov %0, sp" : "=r"(sp)); + for (uint32_t* p = __cy_stack_limit; p < sp - 16; p++) *p = 0xA5A5A5A5u; +} + +static uint32_t stack_used() { + uint32_t* p = __cy_stack_limit; + while (p < __cy_stack && *p == 0xA5A5A5A5u) p++; + return (uint32_t)((uint8_t*)__cy_stack - (uint8_t*)p); +} + +const char* asr650x_watch_string() { + static char name[64]; + extern char _end; + snprintf(name, sizeof(name), "CC stk=%u big=%u heap=%u p=%02x%02x%02x", (unsigned)stack_used(), + (unsigned)asr650x_bigstack_highwater(), (unsigned)((uint32_t)sbrk(0) - (uint32_t)&_end), + (unsigned)asr650x_boot_pool[0], (unsigned)asr650x_boot_pool[1], (unsigned)asr650x_boot_pool[2]); + return name; +} +#endif diff --git a/variants/heltec_cubecell_ab02s/CubeCellBoard.h b/variants/heltec_cubecell_ab02s/CubeCellBoard.h new file mode 100644 index 0000000000..b357f39fa8 --- /dev/null +++ b/variants/heltec_cubecell_ab02s/CubeCellBoard.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include + +extern "C" void CySoftwareReset(void); + +#ifdef ASR650X_STACK_WATCH +// Measurement builds only (never released): DEVICE_QUERY reports stack/heap high-water marks in the manufacturer field. +void asr650x_stack_paint(); +const char* asr650x_watch_string(); +#endif + +class CubeCellBoard : public mesh::MainBoard { +public: + void begin(); // crypto stack, OLED status screen + void loop() override; // OLED status screen (one page per call) + void attachDynamicPrefs(KeyValueStore*) { } // no board-specific prefs + uint16_t getBattMilliVolts() override { return getBatteryVoltage(); } + const char* getManufacturerName() const override { +#ifdef ASR650X_STACK_WATCH + return asr650x_watch_string(); +#else + return "Heltec CubeCell HTCC-AB02S"; +#endif + } + void reboot() override { CySoftwareReset(); } + uint8_t getStartupReason() const override { return BD_STARTUP_NORMAL; } +}; diff --git a/variants/heltec_cubecell_ab02s/CubeCellGPS.cpp b/variants/heltec_cubecell_ab02s/CubeCellGPS.cpp new file mode 100644 index 0000000000..dde3b129c7 --- /dev/null +++ b/variants/heltec_cubecell_ab02s/CubeCellGPS.cpp @@ -0,0 +1,91 @@ +#include "CubeCellGPS.h" +#include "target.h" +#include + +CubeCellGPS cubecell_gps; + +static const uint32_t FIX_TIMEOUT_S = 300; // spec: wait at most 5 minutes for a fix + +CubeCellGPS::CubeCellGPS() : _enabled(true), _interval(0), _powered(false), _began(false), _power_on_ms(0), _cfg_sent(0), _uptime_s(0), _uptime_ms_mark(0), _last_tick_ms(0) {} + +uint32_t CubeCellGPS::uptime(uint32_t now_ms) { + uint32_t d = now_ms - _uptime_ms_mark; // unsigned: correct across the millis() wrap + _uptime_s += d / 1000; + _uptime_ms_mark += (d / 1000) * 1000; + return _uptime_s; +} + +void CubeCellGPS::power(bool on, uint32_t now_ms) { + if (on == _powered) return; + digitalWrite(GPIO14, on ? LOW : HIGH); // VGPS_Control: LOW = powered + _powered = on; + if (on) { _power_on_ms = now_ms; _cfg_sent = 0; _nmea.reset_fix(); } +} + +void CubeCellGPS::begin() { + pinMode(GPIO14, OUTPUT); + digitalWrite(GPIO14, HIGH); // off + Serial1.begin(9600); // once + _began = true; + _policy.configure(_enabled, _interval, FIX_TIMEOUT_S); + _uptime_ms_mark = millis(); + _policy.begin(uptime(millis())); +} + +void CubeCellGPS::apply(bool enabled, uint32_t interval_s) { + bool turned_on = enabled && !_enabled; + _enabled = enabled; + _interval = interval_s > 86400 ? 86400 : interval_s; + _policy.configure(_enabled, _interval, FIX_TIMEOUT_S); + if (turned_on && _began) _policy.request(uptime(millis())); +} + +void CubeCellGPS::request() { + if (_enabled && _began) _policy.request(uptime(millis())); +} + +void CubeCellGPS::on_fix(bool from_rmc) { + const asr650x::GpsData& d = _nmea.data(); + if (d.pos_valid) { + sensors.node_lat = d.lat_e6 / 1000000.0; + sensors.node_lon = d.lon_e6 / 1000000.0; + sensors.node_altitude = d.alt_dm / 10.0; + } + if (from_rmc && d.rmc_valid) { // time only from the RMC just parsed, never from a stale one + uint32_t epoch = asr650x::gps_to_epoch(d.date_ddmmyy, d.time_hhmmss); + if (epoch != 0) { + uint32_t now = rtc_clock.getCurrentTime(); + uint32_t diff = epoch > now ? epoch - now : now - epoch; + if (!rtc_clock.synced() || diff >= 2) rtc_clock.setCurrentTime(epoch); + } + } +} + +void CubeCellGPS::loop(uint32_t now_ms) { + if (!_began) return; + while (Serial1.available() > 0) { // always drain: the UART buffer must not fill up + int c = Serial1.read(); + if (c < 0) break; + if (!_powered) continue; + asr650x::NmeaEvent e = _nmea.feed((uint8_t)c); + if (e == asr650x::NMEA_GGA || e == asr650x::NMEA_RMC) { + const asr650x::GpsData& d = _nmea.data(); + if (d.pos_valid) on_fix(e == asr650x::NMEA_RMC); + } + } + // Air530Z: $PCAS03 keeps only GGA and RMC (the Air530 PGKC242 command is ignored by this module). Sent at 1 s + // and again at 3 s after power-on, in case the module was not listening yet the first time. + if (_powered && _cfg_sent < 2 && now_ms - _power_on_ms >= (_cfg_sent == 0 ? 1000u : 3000u)) { + char line[64]; + int n = asr650x::nmea_make(line, sizeof(line), "PCAS03,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0"); + if (n > 0) Serial1.write((const uint8_t*)line, (size_t)n); + _cfg_sent++; + } + if (now_ms - _last_tick_ms >= 1000) { + _last_tick_ms = now_ms; + const asr650x::GpsData& d = _nmea.data(); + bool fix_ok = _powered && d.pos_valid && d.rmc_valid && asr650x::gps_to_epoch(d.date_ddmmyy, d.time_hhmmss) != 0; + _policy.tick(uptime(now_ms), fix_ok); + power(_policy.power_wanted(), now_ms); + } +} diff --git a/variants/heltec_cubecell_ab02s/CubeCellGPS.h b/variants/heltec_cubecell_ab02s/CubeCellGPS.h new file mode 100644 index 0000000000..f053d24cf1 --- /dev/null +++ b/variants/heltec_cubecell_ab02s/CubeCellGPS.h @@ -0,0 +1,38 @@ +#pragma once +#include +#include +#include + +// Air530 on Serial1 (9600), power on GPIO14 (LOW = on). Serial1.begin() is called ONCE (each call leaks a 256 B buffer); +// the module is switched with the power pin only. Never prints to Serial (the protocol port). +class CubeCellGPS { + asr650x::NmeaParser _nmea; + asr650x::GpsPolicy _policy; + bool _enabled; + uint32_t _interval; + bool _powered; + bool _began; + uint32_t _power_on_ms; + uint8_t _cfg_sent; + uint32_t _uptime_s, _uptime_ms_mark; // monotonic seconds (wraps at 2^32 s, not at the millis() wrap) + uint32_t uptime(uint32_t now_ms); + uint32_t _last_tick_ms; + void power(bool on, uint32_t now_ms); + void on_fix(bool from_rmc); +public: + CubeCellGPS(); + void begin(); + void loop(uint32_t now_ms); + void apply(bool enabled, uint32_t interval_s); + void request(); + bool enabled() const { return _enabled; } + uint32_t interval() const { return _interval; } + bool powered() const { return _powered; } + asr650x::GpsState state() const { return _policy.state(); } + bool timed_out() const { return _policy.timed_out(); } + const asr650x::GpsData& data() const { return _nmea.data(); } + uint32_t sentences_ok() const { return _nmea.sentences_ok(); } + uint32_t warming_s() const { return _policy.warming_for(_uptime_s); } +}; + +extern CubeCellGPS cubecell_gps; diff --git a/variants/heltec_cubecell_ab02s/CubeCellSensors.cpp b/variants/heltec_cubecell_ab02s/CubeCellSensors.cpp new file mode 100644 index 0000000000..a9235baa2a --- /dev/null +++ b/variants/heltec_cubecell_ab02s/CubeCellSensors.cpp @@ -0,0 +1,47 @@ +#include "CubeCellSensors.h" +#include "CubeCellGPS.h" +#include +#include +#include + +bool CubeCellSensors::begin() { cubecell_gps.begin(); return true; } +void CubeCellSensors::loop() { cubecell_gps.loop(millis()); } + +static const char* const NAMES[7] = {"gps", "gps_interval", "gps_fix", "gps_sats", "gps_power", "gps_sent", "oled"}; + +const char* CubeCellSensors::getSettingName(int i) const { return (i >= 0 && i < 7) ? NAMES[i] : NULL; } + +const char* CubeCellSensors::getSettingValue(int i) const { + char* v = const_cast(_val); + switch (i) { + case 0: snprintf(v, sizeof(_val), "%d", cubecell_gps.enabled() ? 1 : 0); break; + case 1: snprintf(v, sizeof(_val), "%u", (unsigned)cubecell_gps.interval()); break; + case 2: snprintf(v, sizeof(_val), "%d", cubecell_gps.data().pos_valid ? 1 : 0); break; + case 3: snprintf(v, sizeof(_val), "%u", (unsigned)cubecell_gps.data().sats); break; + case 4: snprintf(v, sizeof(_val), "%d", cubecell_gps.powered() ? 1 : 0); break; + case 5: snprintf(v, sizeof(_val), "%u", (unsigned)cubecell_gps.sentences_ok()); break; + case 6: return _oled; + default: return NULL; + } + return _val; +} + +bool CubeCellSensors::setSettingValue(const char* name, const char* value) { + if (strcmp(name, "gps") == 0) { + if (strcmp(value, "0") != 0 && strcmp(value, "1") != 0) return false; + cubecell_gps.apply(value[0] == '1', cubecell_gps.interval()); + return true; + } + if (strcmp(name, "gps_interval") == 0) { + if (!*value) return false; + unsigned long v = 0; + for (const char* p = value; *p; p++) { + if (*p < '0' || *p > '9') return false; + v = v * 10 + (unsigned long)(*p - '0'); + if (v > 86400UL) return false; + } + cubecell_gps.apply(cubecell_gps.enabled(), (uint32_t)v); + return true; + } + return false; // read-only or unknown key +} diff --git a/variants/heltec_cubecell_ab02s/CubeCellSensors.h b/variants/heltec_cubecell_ab02s/CubeCellSensors.h new file mode 100644 index 0000000000..40707958ca --- /dev/null +++ b/variants/heltec_cubecell_ab02s/CubeCellSensors.h @@ -0,0 +1,18 @@ +#pragma once +#include + +// GPS settings exposed through MeshCore's custom-variable commands (CMD_GET/SET_CUSTOM_VAR); read-only keys give +// the hardware tests visibility into GPS and OLED state. +class CubeCellSensors : public SensorManager { + char _val[12]; + const char* _oled; +public: + CubeCellSensors() : _oled("none") { _val[0] = 0; } + void setOledStatus(const char* s) { _oled = s; } + bool begin() override; + void loop() override; + int getNumSettings() const override { return 7; } + const char* getSettingName(int i) const override; + const char* getSettingValue(int i) const override; + bool setSettingValue(const char* name, const char* value) override; +}; diff --git a/variants/heltec_cubecell_ab02s/Oled.cpp b/variants/heltec_cubecell_ab02s/Oled.cpp new file mode 100644 index 0000000000..bc4f8c52bb --- /dev/null +++ b/variants/heltec_cubecell_ab02s/Oled.cpp @@ -0,0 +1,114 @@ +#include "Oled.h" +#include +#include +#include + +static const uint8_t ADDR = 0x3C; + +// Direct use of the PSoC I2C master block (what Wire does underneath): Wire.cpp holds 2 x 128 B buffers per instance, +// which this firmware cannot afford in RAM. Timeout 10 ms per byte, same as the core. +static const uint32_t I2C_TIMEOUT_MS = 10; + +static bool i2c_write(const uint8_t* b, uint8_t n) { + I2C_I2CMasterClearStatus(); + if (I2C_I2CMasterSendStart(ADDR, I2C_I2C_WRITE_XFER_MODE, I2C_TIMEOUT_MS) != I2C_I2C_MSTR_NO_ERROR) { + I2C_I2CMasterSendStop(I2C_TIMEOUT_MS); + return false; + } + bool ok = true; + for (uint8_t i = 0; i < n && ok; i++) ok = I2C_I2CMasterWriteByte(b[i], I2C_TIMEOUT_MS) == I2C_I2C_MSTR_NO_ERROR; + I2C_I2CMasterSendStop(I2C_TIMEOUT_MS); + return ok; +} + +bool OledPanel::cmd(uint8_t c) { + const uint8_t b[2] = { 0x80, c }; // control byte: single command + return i2c_write(b, 2); +} + +bool OledPanel::cmds(const uint8_t* c, uint8_t n) { + for (uint8_t i = 0; i < n; i++) if (!cmd(c[i])) return false; + return true; +} + +bool OledPanel::init_panel() { + static const uint8_t seq[] = { + 0xAE, // display off + 0xD5, 0xF0, // clock divide / oscillator + 0xA8, 63, // multiplex 1/64 + 0xD3, 0x00, // display offset + 0x40, // start line 0 + 0x8D, 0x14, // charge pump on + 0x20, 0x00, // horizontal addressing + 0xA1, // segment re-map + 0xC8, // COM scan direction + 0xDA, 0x12, // COM pins (128x64) + 0x81, 0xCF, // contrast + 0xD9, 0xF1, // pre-charge + 0xDB, 0x40, // VCOMH + 0xA4, // resume RAM content + 0xA6, // normal (not inverted) + 0x2E // stop scroll + }; + return cmds(seq, sizeof(seq)); +} + +bool OledPanel::begin() { + pinMode(Vext, OUTPUT); + digitalWrite(Vext, LOW); // panel power on + delay(50); + pinMode(GPIO10, OUTPUT); + digitalWrite(GPIO10, LOW); // reset pulse + delay(20); + digitalWrite(GPIO10, HIGH); + delay(20); + if (!_wire) { // once only + I2C_SCBCLK_DIV_REG = (uint32_t)(CYDEV_BCLK__HFCLK__HZ / 8 / 400000 / 3) << 8; + I2C_Start(); + _wire = true; + } + if (!i2c_write(0, 0)) { // no acknowledge: panel absent or unpowered + _present = false; + digitalWrite(Vext, HIGH); + return false; + } + _present = init_panel(); + _on = false; + if (!_present) digitalWrite(Vext, HIGH); + return _present; +} + +void OledPanel::power(bool on) { + if (!_present) return; + if (on == _on) return; + if (on) { + digitalWrite(Vext, LOW); + delay(20); + digitalWrite(GPIO10, LOW); + delay(10); + digitalWrite(GPIO10, HIGH); + delay(10); + if (!init_panel()) { _present = false; digitalWrite(Vext, HIGH); return; } + cmd(0xAF); // display on + _on = true; + } else { + cmd(0xAE); // display off + cmd(0x8D); + cmd(0x10); // charge pump off + digitalWrite(Vext, HIGH); + _on = false; + } +} + +bool OledPanel::write_page(uint8_t page, const uint8_t data[128]) { + if (!_present || !_on || page > 7) return false; + if (!cmd(0x21) || !cmd(0) || !cmd(127)) return false; // column window 0..127 + if (!cmd(0x22) || !cmd(page) || !cmd(page)) return false; // page window + uint8_t b[17]; + b[0] = 0x40; // control byte: data + for (int i = 0; i < 128; i += 16) { + memcpy(b + 1, data + i, 16); + if (!i2c_write(b, 17)) return false; + } + return true; +} diff --git a/variants/heltec_cubecell_ab02s/Oled.h b/variants/heltec_cubecell_ab02s/Oled.h new file mode 100644 index 0000000000..0abd49b727 --- /dev/null +++ b/variants/heltec_cubecell_ab02s/Oled.h @@ -0,0 +1,20 @@ +#pragma once +#include + +// Minimal SSD1306 128x64 driver over I2C (0x3C) with no frame buffer: the caller writes one 128-byte page at a time. +// Vext (P3_2, LOW = on) powers the panel; GPIO10 is its reset. If the panel does not acknowledge, begin() returns false. +class OledPanel { + bool _present; + bool _on; + bool _wire; + bool cmd(uint8_t c); + bool cmds(const uint8_t* c, uint8_t n); + bool init_panel(); +public: + OledPanel() : _present(false), _on(false), _wire(false) {} + bool begin(); // power the panel, reset it, probe 0x3C, run the init sequence (display off) + bool present() const { return _present; } + bool is_on() const { return _on; } + void power(bool on); // on: Vext + init + DISPLAYON; off: DISPLAYOFF + charge pump off + Vext off + bool write_page(uint8_t page, const uint8_t data[128]); // false on I2C error +}; diff --git a/variants/heltec_cubecell_ab02s/StatusScreen.cpp b/variants/heltec_cubecell_ab02s/StatusScreen.cpp new file mode 100644 index 0000000000..39f13e7b7f --- /dev/null +++ b/variants/heltec_cubecell_ab02s/StatusScreen.cpp @@ -0,0 +1,49 @@ +#include "StatusScreen.h" +#include +#include + +void StatusScreen::begin(OledPanel* oled) { + _oled = oled; + pinMode(USER_KEY, INPUT); // external 10k pull-up; pressed = LOW + wake(millis()); // visible at boot +} + +void StatusScreen::wake(uint32_t now_ms) { + if (!_oled || !_oled->present()) return; + _oled->power(true); + _on = _oled->is_on(); + _on_since = now_ms; + _next_page = _on ? 0 : -1; + _last_refresh = now_ms; +} + +void StatusScreen::loop(uint32_t now_ms) { + if (!_oled || !_oled->present()) return; + if (_button.update(digitalRead(USER_KEY) == LOW, now_ms)) wake(now_ms); + + if (_on && now_ms - _on_since >= ON_MS) { // auto-off + _oled->power(false); + _on = false; + _next_page = -1; + return; + } + if (!_on) return; + + if (_next_page < 0 && now_ms - _last_refresh >= 1000) { // start a refresh: battery at most every 10 s + if (now_ms - _last_batt_ms >= 10000 || _batt_mv == 0) { + _batt_mv = getBatteryVoltage(); // drives the shared USER_KEY pin LOW for a moment + _last_batt_ms = now_ms; + _button.holdoff(millis(), 50); + } + _in.batt_mv = _batt_mv; + _next_page = 0; + _last_refresh = now_ms; + } + if (_next_page >= 0) { // one page per call + char line[22]; + asr650x::status_line(_next_page, _in, line); + asr650x::render_line(line, _page); + if (!_oled->write_page((uint8_t)_next_page, _page)) { _error = true; _on = false; _oled->power(false); _next_page = -1; return; } + _next_page = (_next_page == 7) ? -1 : (int8_t)(_next_page + 1); + } +} diff --git a/variants/heltec_cubecell_ab02s/StatusScreen.h b/variants/heltec_cubecell_ab02s/StatusScreen.h new file mode 100644 index 0000000000..a5a031a71a --- /dev/null +++ b/variants/heltec_cubecell_ab02s/StatusScreen.h @@ -0,0 +1,32 @@ +#pragma once +#include +#include +#include "Oled.h" +#include +#include + +// One status screen. Auto-off after ON_MS, USER key wakes it. Redraws once a second, ONE page per loop() call +// (about 3-4 ms of I2C each) so serial frames and radio events are never starved. +class StatusScreen { + OledPanel* _oled; + asr650x::Button _button; + asr650x::StatusInput _in; + uint8_t _page[128]; + bool _on; + uint32_t _on_since, _last_refresh; + int8_t _next_page; // -1 = idle, 0..7 = pages still to draw + uint32_t _last_batt_ms; + uint16_t _batt_mv; + bool _error; +public: + static const uint32_t ON_MS = 30000; + StatusScreen() : _oled(0), _on(false), _on_since(0), _last_refresh(0), _next_page(-1), _last_batt_ms(0), _batt_mv(0), _error(false) { + memset(&_in, 0, sizeof(_in)); + } + void begin(OledPanel* oled); + void set_input(const asr650x::StatusInput& in) { _in = in; _in.batt_mv = _batt_mv; } + void wake(uint32_t now_ms); + void loop(uint32_t now_ms); + bool is_on() const { return _on; } + bool failed() const { return _error; } +}; diff --git a/variants/heltec_cubecell_ab02s/c_include_order.py b/variants/heltec_cubecell_ab02s/c_include_order.py new file mode 100644 index 0000000000..1d4da7daec --- /dev/null +++ b/variants/heltec_cubecell_ab02s/c_include_order.py @@ -0,0 +1,8 @@ +# The CubeCell framework's LoRaWAN library (which provides the SX1262 driver used here) includes "aes.h" from C files. +# On case-insensitive filesystems that name also matches rweather/Crypto's C++ "AES.h". Put the framework's C header +# first for C sources only (CFLAGS are not passed to the C++ compiler, so MeshCore keeps Crypto's AES.h). +Import("env") +import os + +fw = env.PioPlatform().get_package_dir("framework-arduinocubecell") +env.Append(CFLAGS=["-I" + os.path.join(fw, "cores", "asr650x", "lora", "system", "crypto")]) diff --git a/variants/heltec_cubecell_ab02s/platformio.ini b/variants/heltec_cubecell_ab02s/platformio.ini new file mode 100644 index 0000000000..de10084642 --- /dev/null +++ b/variants/heltec_cubecell_ab02s/platformio.ini @@ -0,0 +1,72 @@ +; Heltec CubeCell GPS-6502 (HTCC-AB02S) - LEGACY board: ASR6502 (Cortex-M0+), 128 KB flash, 16 KB RAM, fixed 2 KB stack. +; Companion over USB/UART only (no BLE, no WiFi). The node does not generate its own identity: import a private key +; from the app first; until then it answers the app but never transmits. +; Small-board options used here (all default off elsewhere): see the build_flags below. + +[heltec_cubecell_ab02s] +extends = arduino_base +platform = heltecautomation/heltec-cubecell@1.1.0 +board = cubecell_gps +extra_scripts = pre:variants/heltec_cubecell_ab02s/c_include_order.py +build_unflags = -std=gnu++98 -std=gnu++11 -std=gnu++14 -std=gnu++17 +build_flags = ${arduino_base.build_flags} + -std=gnu++11 -fno-exceptions + -D ASR650X_PLATFORM + -I src/helpers/asr650x + -I variants/heltec_cubecell_ab02s + -I variants/heltec_cubecell_ab02s/shims + -I examples/companion_radio + -D ENABLE_PRIVATE_KEY_EXPORT=0 + -D LORA_TX_POWER=14 + -D MAX_CONTACTS=12 + -D OFFLINE_QUEUE_SIZE=3 + -D MAX_GROUP_CHANNELS=1 + -D MAX_ANON_CONTACTS=2 + -D MAX_PACKET_HASHES=32 + -D ADVERT_PATH_TABLE_SIZE=4 + -D COMPANION_PACKET_POOL_SIZE=5 + -D CONTACT_OUT_PATH_SIZE=16 + -D RX_DELAY_FAST_POW + -D DISABLE_CLI_RESCUE + -D DISABLE_COMPANION_CLI + -D SERIAL_FRAME_STRICT + -D CONFIG_SERIALIZER_NOOP + -D ED25519_SMALL_BASE_TABLE + -D IDENTITY_CRYPTO_ON_BIG_STACK + -D BIGSTACK_SIZE=2048 + -D ENV_INCLUDE_GPS=1 + ; the framework printf (mpaland) is built with these: nothing here prints %f/%e/%g/%ll (floats go through StrHelper::ftoa) + -D PRINTF_DISABLE_SUPPORT_FLOAT + -D PRINTF_DISABLE_SUPPORT_EXPONENTIAL + -D PRINTF_DISABLE_SUPPORT_LONG_LONG +; only the helpers a companion needs: the others expect a filesystem +build_src_filter = + +<*.cpp> + + + + + + + + + + + + + + + + + + + + + +<../variants/heltec_cubecell_ab02s/*.cpp> +lib_deps = + rweather/Crypto @ ^0.4.0 + densaugeo/base64 @ ~1.4.0 +lib_ignore = + RadioLib + RTClib + Melopero RV3028 + CayenneLPP +upload_port = COM11 + +[env:Heltec_CubeCell_AB02S_companion_radio_usb] +extends = heltec_cubecell_ab02s +build_flags = ${heltec_cubecell_ab02s.build_flags} + -D ENABLE_USB_INTERFACE +build_src_filter = ${heltec_cubecell_ab02s.build_src_filter} + +<../examples/companion_radio/*.cpp> + -<../examples/companion_radio/DataStore.cpp> diff --git a/variants/heltec_cubecell_ab02s/shims/CayenneLPP.h b/variants/heltec_cubecell_ab02s/shims/CayenneLPP.h new file mode 100644 index 0000000000..997c201daa --- /dev/null +++ b/variants/heltec_cubecell_ab02s/shims/CayenneLPP.h @@ -0,0 +1,29 @@ +#pragma once +#include +#include + +// Minimal subset used by MyMesh (voltage = type 116, uint16 0.01 V; temperature = type 103, int16 0.1 C; +// big-endian, as in the ElectronicCats CayenneLPP library). Not verified against upstream byte-for-byte. +class CayenneLPP { + uint8_t _buf[64]; + uint8_t _cursor; +public: + explicit CayenneLPP(uint8_t) : _cursor(0) {} + void reset() { _cursor = 0; } + uint8_t getSize() const { return _cursor; } + uint8_t* getBuffer() { return _buf; } + uint8_t addVoltage(uint8_t channel, float volts) { + if (_cursor + 4 > sizeof(_buf)) return 0; + uint16_t v = (uint16_t)(volts * 100.0f + 0.5f); + _buf[_cursor++] = channel; _buf[_cursor++] = 116; + _buf[_cursor++] = v >> 8; _buf[_cursor++] = v & 0xFF; + return _cursor; + } + uint8_t addTemperature(uint8_t channel, float celsius) { + if (_cursor + 4 > sizeof(_buf)) return 0; + int16_t v = (int16_t)(celsius * 10.0f); + _buf[_cursor++] = channel; _buf[_cursor++] = 103; + _buf[_cursor++] = (uint16_t)v >> 8; _buf[_cursor++] = (uint16_t)v & 0xFF; + return _cursor; + } +}; diff --git a/variants/heltec_cubecell_ab02s/shims/RTClib.h b/variants/heltec_cubecell_ab02s/shims/RTClib.h new file mode 100644 index 0000000000..879a346820 --- /dev/null +++ b/variants/heltec_cubecell_ab02s/shims/RTClib.h @@ -0,0 +1,2 @@ +#pragma once +// MyMesh.h includes but companion_radio never uses it; intentionally empty. diff --git a/variants/heltec_cubecell_ab02s/target.cpp b/variants/heltec_cubecell_ab02s/target.cpp new file mode 100644 index 0000000000..87ec29a120 --- /dev/null +++ b/variants/heltec_cubecell_ab02s/target.cpp @@ -0,0 +1,45 @@ +#include "target.h" +#include +#include + +CubeCellBoard board; +CubeCellRadio radio_driver; +CubeCellSensors sensors; +CubeCellRTC rtc_clock; + +bool radio_init() { + radio_driver.init(); // Radio.Init first: getRngSeed() (called next by main.cpp) needs the radio + return true; +} + +namespace { +class SeedRNG : public mesh::RNG { + const uint8_t* _p; +public: + explicit SeedRNG(const uint8_t* p) : _p(p) {} + void random(uint8_t* dest, size_t sz) override { memcpy(dest, _p, sz); } +}; + +// Deterministic, device-unique, NOT secret (derived from the public chip ID): it only exists so the +// firmware can answer the app before a key is imported. It is never persisted and never transmits +// (TxGate), so the app must import a private key before the node can be used. +void placeholder_seed(uint8_t seed[32]) { + uint64_t id = getID(); + uint8_t in[8 + 21]; + memcpy(in, &id, 8); + memcpy(in + 8, "asr650x-unprovisioned", 21); + mesh::Utils::sha256(seed, 32, in, sizeof(in)); +} +} + +mesh::LocalIdentity radio_new_identity() { + uint8_t seed[32]; + placeholder_seed(seed); + SeedRNG rng(seed); + return mesh::LocalIdentity(&rng); +} + +bool asr650x_identity_is_placeholder(const mesh::Identity& id) { + mesh::LocalIdentity p = radio_new_identity(); + return id.matches(p); +} diff --git a/variants/heltec_cubecell_ab02s/target.h b/variants/heltec_cubecell_ab02s/target.h new file mode 100644 index 0000000000..6fcaf49291 --- /dev/null +++ b/variants/heltec_cubecell_ab02s/target.h @@ -0,0 +1,26 @@ +#pragma once + +// Heltec CubeCell GPS-6502 (HTCC-AB02S): legacy ASR6502 board (ARM Cortex-M0+, 128 KB flash, 16 KB RAM, fixed 2 KB +// stack), SX1262, Air530Z GPS, 0.96" SSD1306 OLED. Companion over USB/UART only (no BLE, no WiFi). + +#include +#include +#include +#include +#include +#include "CubeCellBoard.h" +#include "CubeCellSensors.h" + +#ifndef min + #define min(a, b) ((a) < (b) ? (a) : (b)) // the CubeCell core does not provide min() +#endif + +extern CubeCellBoard board; +extern CubeCellRadio radio_driver; +extern CubeCellSensors sensors; +extern CubeCellRTC rtc_clock; + +bool radio_init(); +mesh::LocalIdentity radio_new_identity(); // placeholder derived from the chip ID, NOT random (see target.cpp) +bool asr650x_identity_is_placeholder(const mesh::Identity& id); +extern uint8_t asr650x_boot_pool[4]; // fold of the boot-time Radio.Random() outputs (CubeCellRadio.cpp) From 5025a41e0166b429dba4b45821a4e337ba926dbd Mon Sep 17 00:00:00 2001 From: HAN Date: Wed, 7 Oct 2026 00:41:10 +0700 Subject: [PATCH 11/16] SERIAL_FRAME_STRICT: only abandon a stalled frame when nothing is buffered A main loop busy for more than 100 ms (e.g. Ed25519 on a small MCU) must not drop a frame whose remaining bytes are already waiting in the UART buffer. --- src/helpers/ArduinoSerialInterface.cpp | 5 +++-- .../test_serial_frame_strict.cpp | 21 ++++++++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/helpers/ArduinoSerialInterface.cpp b/src/helpers/ArduinoSerialInterface.cpp index 4a80e3faba..0761e0aad7 100644 --- a/src/helpers/ArduinoSerialInterface.cpp +++ b/src/helpers/ArduinoSerialInterface.cpp @@ -38,8 +38,9 @@ size_t ArduinoSerialInterface::writeFrame(const uint8_t src[], size_t len) { size_t ArduinoSerialInterface::checkRecvFrame(uint8_t dest[]) { #ifdef SERIAL_FRAME_STRICT - // a frame that stalls mid-way (truncated by the sender) is abandoned instead of swallowing the next frame - if (_state != RECV_STATE_IDLE && (millis() - _last_byte_ms) > 100) _state = RECV_STATE_IDLE; + // a frame that stalls mid-way (truncated by the sender) is abandoned instead of swallowing the next frame; + // only when nothing is waiting: a busy main loop must not drop a frame whose bytes are already buffered + if (_state != RECV_STATE_IDLE && !_serial->available() && (millis() - _last_byte_ms) > 100) _state = RECV_STATE_IDLE; #endif while (_serial->available()) { int c = _serial->read(); diff --git a/test/test_serial_frame_strict/test_serial_frame_strict.cpp b/test/test_serial_frame_strict/test_serial_frame_strict.cpp index 641e2a7652..ec401a2f97 100644 --- a/test/test_serial_frame_strict/test_serial_frame_strict.cpp +++ b/test/test_serial_frame_strict/test_serial_frame_strict.cpp @@ -40,7 +40,9 @@ TEST(SerialFrameStrict, StalledFrameDoesNotSwallowTheNextOne) { g_mock_millis = 1000; s.push({'<', 10, 0, 1, 2, 3}); // header says 10 bytes, only 3 arrive EXPECT_EQ(sif.checkRecvFrame(out), 0u); - g_mock_millis = 1200; // > 100 ms of silence: the partial frame is abandoned + g_mock_millis = 1150; // the main loop polls during the silence (nothing buffered): + EXPECT_EQ(sif.checkRecvFrame(out), 0u); // > 100 ms since the last byte -> the partial frame is abandoned + g_mock_millis = 1200; s.push(frame({22, 3})); EXPECT_EQ(sif.checkRecvFrame(out), 2u); EXPECT_EQ(out[0], 22); @@ -60,6 +62,23 @@ TEST(SerialFrameStrict, OversizeFrameIsDroppedNotExecuted) { EXPECT_EQ(out[0], 5); } +// the device was busy for > 100 ms while the rest of the frame was already waiting in the UART buffer: +// the frame must still be delivered (only a sender that really stopped is abandoned) +TEST(SerialFrameStrict, BusyDeviceDoesNotDropAQueuedFrame) { + FakeSerial s; + ArduinoSerialInterface sif; + sif.begin(s); + uint8_t out[MAX_FRAME_SIZE]; + g_mock_millis = 5000; + s.push({'<', 4, 0, 22, 1}); // first part read + EXPECT_EQ(sif.checkRecvFrame(out), 0u); + s.push({2, 3}); // rest arrived while the loop was busy + g_mock_millis = 5300; + EXPECT_EQ(sif.checkRecvFrame(out), 4u); + EXPECT_EQ(out[0], 22); + EXPECT_EQ(out[3], 3); +} + int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); From 19d9b30503e467cdad4d4efa92d591850ca7f1fd Mon Sep 17 00:00:00 2001 From: HAN Date: Wed, 7 Oct 2026 00:41:10 +0700 Subject: [PATCH 12/16] AB02S: keep the other companion settings across reboots (flash record v2); no hard-coded upload port Version 2 of the SFLASH record (192 bytes) adds airtime/rx/tx delay factors, multi-acks, manual add, telemetry modes, advert location policy, auto-add, path hash mode, tz offset, CAD/AGC/interference and rx gain. Version 1 records still load (the new settings then keep their defaults). --- examples/companion_radio/FlashDataStore.cpp | 37 +++++++++++++ src/helpers/asr650x/FlashRecord.h | 55 +++++++++++++++---- .../test_asr650x_persist.cpp | 42 ++++++++++++++ variants/heltec_cubecell_ab02s/platformio.ini | 1 - 4 files changed, 124 insertions(+), 11 deletions(-) diff --git a/examples/companion_radio/FlashDataStore.cpp b/examples/companion_radio/FlashDataStore.cpp index fdf61c0790..bd8089a1c1 100644 --- a/examples/companion_radio/FlashDataStore.cpp +++ b/examples/companion_radio/FlashDataStore.cpp @@ -63,6 +63,25 @@ void DataStore::loadPrefs(NodePrefs& prefs) { prefs.node_name[sizeof(prefs.node_name) - 1] = 0; prefs.freq = s.freq; prefs.bw = s.bw; prefs.sf = s.sf; prefs.cr = s.cr; prefs.tx_power_dbm = s.tx_power_dbm; + if (!s.has_ext) return; // record from an older build: keep the defaults + prefs.airtime_factor = s.airtime_factor; + prefs.rx_delay_base = s.rx_delay_base; + prefs.tx_delay_factor = s.tx_delay_factor; + prefs.direct_tx_delay_factor = s.direct_tx_delay_factor; + prefs.multi_acks = s.multi_acks; + prefs.manual_add_contacts = s.manual_add_contacts; + prefs.telemetry_mode_base = s.telemetry_modes & 0x03; + prefs.telemetry_mode_loc = (s.telemetry_modes >> 2) & 0x03; + prefs.telemetry_mode_env = (s.telemetry_modes >> 4) & 0x03; + prefs.advert_loc_policy = s.advert_loc_policy; + prefs.autoadd_config = s.autoadd_config; + prefs.autoadd_max_hops = s.autoadd_max_hops; + prefs.path_hash_mode = s.path_hash_mode; + prefs.tz_offset = s.tz_offset; + prefs.cad_enabled = s.cad_enabled; + prefs.interference_threshold = s.interference_threshold; + prefs.agc_reset_interval = s.agc_reset_interval; + prefs.rx_boosted_gain = s.rx_boosted_gain; } bool DataStore::savePrefs(NodePrefs& prefs) { @@ -74,6 +93,24 @@ bool DataStore::savePrefs(NodePrefs& prefs) { c.node_name[sizeof(c.node_name) - 1] = 0; c.freq = prefs.freq; c.bw = prefs.bw; c.sf = prefs.sf; c.cr = prefs.cr; c.tx_power_dbm = prefs.tx_power_dbm; + c.has_ext = true; + c.airtime_factor = prefs.airtime_factor; + c.rx_delay_base = prefs.rx_delay_base; + c.tx_delay_factor = prefs.tx_delay_factor; + c.direct_tx_delay_factor = prefs.direct_tx_delay_factor; + c.multi_acks = prefs.multi_acks; + c.manual_add_contacts = prefs.manual_add_contacts; + c.telemetry_modes = (uint8_t)((prefs.telemetry_mode_base & 0x03) | ((prefs.telemetry_mode_loc & 0x03) << 2) | + ((prefs.telemetry_mode_env & 0x03) << 4)); + c.advert_loc_policy = prefs.advert_loc_policy; + c.autoadd_config = prefs.autoadd_config; + c.autoadd_max_hops = prefs.autoadd_max_hops; + c.path_hash_mode = prefs.path_hash_mode; + c.tz_offset = prefs.tz_offset; + c.cad_enabled = prefs.cad_enabled; + c.interference_threshold = prefs.interference_threshold; + c.agc_reset_interval = prefs.agc_reset_interval; + c.rx_boosted_gain = prefs.rx_boosted_gain; return commit_big(c); } #endif // ASR650X_PLATFORM diff --git a/src/helpers/asr650x/FlashRecord.h b/src/helpers/asr650x/FlashRecord.h index 9e75092fb9..1cb2387deb 100644 --- a/src/helpers/asr650x/FlashRecord.h +++ b/src/helpers/asr650x/FlashRecord.h @@ -3,13 +3,19 @@ #include #include -// 160-byte on-flash record (EEPROM emulation, SFLASH). Explicit little-endian layout: -// 0..3 "KTM1" | 4 version(1) | 5 flags(bit0 identity, bit1 prefs) | 6..7 0 +// On-flash record (SFLASH user row). Explicit little-endian layout: +// 0..3 "KTM1" | 4 version | 5 flags(bit0 identity, bit1 prefs) | 6..7 0 // 8..71 prv | 72..103 pub | 104..135 name | 136..139 freq | 140..143 bw -// 144 sf | 145 cr | 146 tx_power | 147 gps_off | 148..151 seq | 152..155 gps_interval | 156..159 CRC-32 of bytes 0..155 +// 144 sf | 145 cr | 146 tx_power | 147 gps_off | 148..151 seq | 152..155 gps_interval +// version 1 (160 bytes): 156..159 CRC-32 of bytes 0..155 +// version 2 (192 bytes): 156..159 airtime_factor | 160..163 rx_delay_base | 164..167 tx_delay_factor +// | 168..171 direct_tx_delay_factor | 172 multi_acks | 173 manual_add_contacts | 174 telemetry modes +// | 175 advert_loc_policy | 176 autoadd_config | 177 autoadd_max_hops | 178 path_hash_mode | 179 tz_offset +// | 180 cad_enabled | 181 interference_threshold | 182 agc_reset_interval | 183 rx_boosted_gain | 184..187 0 +// | 188..191 CRC-32 of bytes 0..187 namespace asr650x { -const size_t PERSIST_SIZE = 160; +const size_t PERSIST_SIZE = 192; // version 2; a version 1 record uses the first 160 bytes struct PersistState { uint32_t seq; // record generation, newest valid row wins (see FlashRecordStore.h) @@ -23,6 +29,12 @@ struct PersistState { int8_t tx_power_dbm; bool gps_off; // 1 = the user switched GPS off (0 = default: on, also for older records) uint32_t gps_interval; // seconds between GPS refreshes, 0 = only at boot / on request + bool has_ext; // version 2 settings below are present (false for a version 1 record) + float airtime_factor, rx_delay_base, tx_delay_factor, direct_tx_delay_factor; + uint8_t multi_acks, manual_add_contacts, telemetry_modes, advert_loc_policy; + uint8_t autoadd_config, autoadd_max_hops, path_hash_mode; + int8_t tz_offset; + uint8_t cad_enabled, interference_threshold, agc_reset_interval, rx_boosted_gain; }; inline uint32_t crc32(const uint8_t* d, size_t n) { @@ -37,7 +49,7 @@ inline uint32_t crc32(const uint8_t* d, size_t n) { inline void persist_encode(const PersistState& s, uint8_t out[PERSIST_SIZE]) { memset(out, 0, PERSIST_SIZE); memcpy(out, "KTM1", 4); - out[4] = 1; + out[4] = 2; out[5] = (s.has_identity ? 1 : 0) | (s.has_prefs ? 2 : 0); if (s.has_identity) { memcpy(out + 8, s.prv, 64); memcpy(out + 72, s.pub, 32); } if (s.has_prefs) { @@ -49,16 +61,39 @@ inline void persist_encode(const PersistState& s, uint8_t out[PERSIST_SIZE]) { out[147] = s.gps_off ? 1 : 0; for (int i = 0; i < 4; i++) out[148 + i] = (uint8_t)(s.seq >> (8 * i)); for (int i = 0; i < 4; i++) out[152 + i] = (uint8_t)(s.gps_interval >> (8 * i)); - uint32_t crc = crc32(out, 156); - for (int i = 0; i < 4; i++) out[156 + i] = (uint8_t)(crc >> (8 * i)); + if (s.has_ext) { + memcpy(out + 156, &s.airtime_factor, 4); + memcpy(out + 160, &s.rx_delay_base, 4); + memcpy(out + 164, &s.tx_delay_factor, 4); + memcpy(out + 168, &s.direct_tx_delay_factor, 4); + out[172] = s.multi_acks; out[173] = s.manual_add_contacts; out[174] = s.telemetry_modes; + out[175] = s.advert_loc_policy; out[176] = s.autoadd_config; out[177] = s.autoadd_max_hops; + out[178] = s.path_hash_mode; out[179] = (uint8_t)s.tz_offset; out[180] = s.cad_enabled; + out[181] = s.interference_threshold; out[182] = s.agc_reset_interval; out[183] = s.rx_boosted_gain; + out[184] = 1; // marks the settings block as present + } + uint32_t crc = crc32(out, 188); + for (int i = 0; i < 4; i++) out[188 + i] = (uint8_t)(crc >> (8 * i)); } inline bool persist_decode(const uint8_t in[PERSIST_SIZE], PersistState& s) { memset(&s, 0, sizeof(s)); - if (memcmp(in, "KTM1", 4) != 0 || in[4] != 1) return false; + if (memcmp(in, "KTM1", 4) != 0 || (in[4] != 1 && in[4] != 2)) return false; + size_t crc_at = in[4] == 1 ? 156 : 188; uint32_t want = 0; - for (int i = 0; i < 4; i++) want |= (uint32_t)in[156 + i] << (8 * i); - if (crc32(in, 156) != want) return false; + for (int i = 0; i < 4; i++) want |= (uint32_t)in[crc_at + i] << (8 * i); + if (crc32(in, crc_at) != want) return false; + if (in[4] == 2 && in[184] == 1) { + s.has_ext = true; + memcpy(&s.airtime_factor, in + 156, 4); + memcpy(&s.rx_delay_base, in + 160, 4); + memcpy(&s.tx_delay_factor, in + 164, 4); + memcpy(&s.direct_tx_delay_factor, in + 168, 4); + s.multi_acks = in[172]; s.manual_add_contacts = in[173]; s.telemetry_modes = in[174]; + s.advert_loc_policy = in[175]; s.autoadd_config = in[176]; s.autoadd_max_hops = in[177]; + s.path_hash_mode = in[178]; s.tz_offset = (int8_t)in[179]; s.cad_enabled = in[180]; + s.interference_threshold = in[181]; s.agc_reset_interval = in[182]; s.rx_boosted_gain = in[183]; + } s.gps_off = in[147] != 0; for (int i = 0; i < 4; i++) s.seq |= (uint32_t)in[148 + i] << (8 * i); for (int i = 0; i < 4; i++) s.gps_interval |= (uint32_t)in[152 + i] << (8 * i); diff --git a/test/test_asr650x_persist/test_asr650x_persist.cpp b/test/test_asr650x_persist/test_asr650x_persist.cpp index 5961776c3c..6145b7b281 100644 --- a/test/test_asr650x_persist/test_asr650x_persist.cpp +++ b/test/test_asr650x_persist/test_asr650x_persist.cpp @@ -68,6 +68,48 @@ TEST(Asr650xFlashRecord, Behaves) { } } +// version 2: the other companion settings the app can change (they used to be lost on reboot) +TEST(Asr650xFlashRecord, Version2KeepsCompanionSettings) { + asr650x::PersistState a = sample(), b; + a.has_ext = true; + a.airtime_factor = 1.5f; a.rx_delay_base = 2.25f; a.tx_delay_factor = 0.5f; a.direct_tx_delay_factor = 0.25f; + a.multi_acks = 1; a.manual_add_contacts = 1; a.telemetry_modes = 0x26; a.advert_loc_policy = 1; + a.autoadd_config = 0x0E; a.autoadd_max_hops = 3; a.path_hash_mode = 2; a.tz_offset = -7; + a.cad_enabled = 1; a.interference_threshold = 14; a.agc_reset_interval = 5; a.rx_boosted_gain = 1; + uint8_t buf[asr650x::PERSIST_SIZE]; + asr650x::persist_encode(a, buf); + ASSERT_TRUE(asr650x::persist_decode(buf, b)); + EXPECT_TRUE(b.has_ext); + EXPECT_EQ(b.airtime_factor, 1.5f); EXPECT_EQ(b.rx_delay_base, 2.25f); + EXPECT_EQ(b.tx_delay_factor, 0.5f); EXPECT_EQ(b.direct_tx_delay_factor, 0.25f); + EXPECT_EQ(b.multi_acks, 1); EXPECT_EQ(b.manual_add_contacts, 1); EXPECT_EQ(b.telemetry_modes, 0x26); + EXPECT_EQ(b.advert_loc_policy, 1); EXPECT_EQ(b.autoadd_config, 0x0E); EXPECT_EQ(b.autoadd_max_hops, 3); + EXPECT_EQ(b.path_hash_mode, 2); EXPECT_EQ(b.tz_offset, -7); EXPECT_EQ(b.cad_enabled, 1); + EXPECT_EQ(b.interference_threshold, 14); EXPECT_EQ(b.agc_reset_interval, 5); EXPECT_EQ(b.rx_boosted_gain, 1); + EXPECT_EQ(b.freq, 921.5f); // version 1 fields unchanged + buf[176] ^= 0x01; // a corrupted settings byte is detected + EXPECT_FALSE(asr650x::persist_decode(buf, b)); +} + +// a record written by the first firmware (version 1, 160 bytes, CRC at 156) still loads; the new settings are absent +TEST(Asr650xFlashRecord, Version1RecordStillLoads) { + asr650x::PersistState a = sample(); + uint8_t v2[asr650x::PERSIST_SIZE]; + asr650x::persist_encode(a, v2); + uint8_t v1[asr650x::PERSIST_SIZE]; + memset(v1, 0xFF, sizeof(v1)); // bytes after the old record: erased flash + memcpy(v1, v2, 156); + v1[4] = 1; + uint32_t crc = asr650x::crc32(v1, 156); + for (int i = 0; i < 4; i++) v1[156 + i] = (uint8_t)(crc >> (8 * i)); + asr650x::PersistState b; + ASSERT_TRUE(asr650x::persist_decode(v1, b)); + EXPECT_FALSE(b.has_ext); + EXPECT_TRUE(b.has_identity && b.has_prefs); + EXPECT_EQ(memcmp(b.prv, a.prv, 64), 0); + EXPECT_STREQ(b.node_name, "KTM-test"); +} + int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); diff --git a/variants/heltec_cubecell_ab02s/platformio.ini b/variants/heltec_cubecell_ab02s/platformio.ini index de10084642..925dfbfdbf 100644 --- a/variants/heltec_cubecell_ab02s/platformio.ini +++ b/variants/heltec_cubecell_ab02s/platformio.ini @@ -61,7 +61,6 @@ lib_ignore = RTClib Melopero RV3028 CayenneLPP -upload_port = COM11 [env:Heltec_CubeCell_AB02S_companion_radio_usb] extends = heltec_cubecell_ab02s From f8d1ca81967bdc14a35abc15bbc020f522c8fcdc Mon Sep 17 00:00:00 2001 From: HAN Date: Wed, 7 Oct 2026 07:36:33 +0700 Subject: [PATCH 13/16] AB02S: keep up to 3 contacts across reboots (CONTACT_PIN_STORE) Contacts the app adds or edits (CMD_ADD_UPDATE_CONTACT) are pinned in the free SFLASH user row; removing a contact unpins it, a fourth contact stays RAM-only, factory reset clears the pins. Contacts learnt from adverts or imports stay RAM-only. The MyMesh hooks are behind CONTACT_PIN_STORE (default off). --- examples/companion_radio/FlashDataStore.cpp | 159 +++++++++++++++--- examples/companion_radio/FlashDataStore.h | 42 +++-- examples/companion_radio/MyMesh.cpp | 9 + src/helpers/asr650x/FlashRowBackend.h | 16 +- src/helpers/asr650x/PinnedContacts.h | 117 +++++++++++++ test/test_asr650x_pins/test_asr650x_pins.cpp | 126 ++++++++++++++ variants/heltec_cubecell_ab02s/platformio.ini | 1 + 7 files changed, 416 insertions(+), 54 deletions(-) create mode 100644 src/helpers/asr650x/PinnedContacts.h create mode 100644 test/test_asr650x_pins/test_asr650x_pins.cpp diff --git a/examples/companion_radio/FlashDataStore.cpp b/examples/companion_radio/FlashDataStore.cpp index bd8089a1c1..c67619ab36 100644 --- a/examples/companion_radio/FlashDataStore.cpp +++ b/examples/companion_radio/FlashDataStore.cpp @@ -1,34 +1,127 @@ -#if defined(ASR650X_PLATFORM) // the other boards use DataStore.cpp (filesystem) +#if defined(ASR650X_PLATFORM) // the other boards use DataStore.cpp (filesystem) #include "FlashDataStore.h" + #include "target.h" + #include +#include namespace { -struct CommitArgs { asr650x::PersistStore* store; const asr650x::PersistState* cand; bool ok; }; -void run_commit(void* p) { CommitArgs* a = (CommitArgs*)p; a->ok = a->store->commit(*a->cand); } +struct CommitArgs { + asr650x::PersistStore *store; + const asr650x::PersistState *cand; + bool ok; +}; +void runCommit(void *p) { + CommitArgs *a = (CommitArgs *)p; + a->ok = a->store->commit(*a->cand); } +} // namespace -bool DataStore::commit_big(const asr650x::PersistState& c) { +bool DataStore::commitBig(const asr650x::PersistState &c) { CommitArgs a = { &_store, &c, false }; - asr650x_call_on_big_stack(run_commit, &a); + asr650xCallOnBigStack(runCommit, &a); return a.ok; } void DataStore::begin() { _store.begin(); - asr650x_set_tx_allowed(_store.state().has_identity); // never transmit as the unprovisioned placeholder + asr650xSetTxAllowed(_store.state().has_identity); // never transmit as the unprovisioned placeholder +} + +// ---- pinned contacts (SFLASH row 2). Every operation runs on the crypto stack: the row, a PinSet and a +// ContactInfo together are ~600 bytes, too much for the 2 KB main stack, and none of it stays in RAM. +namespace { +const size_t PIN_ROW = 2 * asr650x::ROW_STRIDE; + +enum PinOp { PIN_LOAD, PIN_REFRESH, PIN_ADD, PIN_DEL, PIN_CLEAR }; + +struct PinArgs { + FlashRowBackend *be; + DataStoreHost *host; + bool (*filter)(const ContactInfo &); + const ContactInfo *contact; + const uint8_t *pub; + PinOp op; +}; + +void runPins(void *p) { + PinArgs *a = (PinArgs *)p; + asr650x::PinSet s; + uint8_t row[asr650x::PIN_SIZE]; + if (a->op != PIN_CLEAR) { + a->be->read(PIN_ROW, row, asr650x::PIN_SIZE); + asr650x::pinsDecode(row, s); // erased/corrupt -> empty set + } + bool changed = false; + if (a->op == PIN_LOAD) { + for (int i = 0; i < s.n; i++) { + ContactInfo ci; + memset(&ci, 0, sizeof(ci)); + ci.id = mesh::Identity(s.e[i].pub); + memcpy(ci.name, s.e[i].name, sizeof(ci.name)); + ci.type = s.e[i].type; + ci.flags = s.e[i].flags; + ci.out_path_len = OUT_PATH_UNKNOWN; // the route is learnt again (flood first) + a->host->onContactLoaded(ci); + } + return; + } + if (a->op == PIN_REFRESH) { + ContactInfo ci; + for (uint32_t idx = 0; a->host->getContactForSave(idx, ci); idx++) { + if (a->filter && !a->filter(ci)) continue; + if (asr650x::pinRefresh(s, ci.id.pub_key, ci.name, ci.type, ci.flags) == asr650x::PIN_CHANGED) + changed = true; + } + } else if (a->op == PIN_ADD) { + const ContactInfo &c = *a->contact; + changed = asr650x::pinUpsert(s, c.id.pub_key, c.name, c.type, c.flags) == + asr650x::PIN_CHANGED; // FULL: RAM only + } else if (a->op == PIN_DEL) { + changed = asr650x::pinRemove(s, a->pub); + } else { + changed = true; // clear: write the empty record + } + if (changed) { + asr650x::pinsEncode(s, row); + a->be->write(PIN_ROW, row, asr650x::PIN_SIZE); + } +} +} // namespace + +void DataStore::loadContacts(DataStoreHost *host) { + PinArgs a = { &_be, host, NULL, NULL, NULL, PIN_LOAD }; + asr650xCallOnBigStack(runPins, &a); +} + +void DataStore::saveContacts(DataStoreHost *host, bool (*filter)(const ContactInfo &c)) { + PinArgs a = { &_be, host, filter, NULL, NULL, PIN_REFRESH }; + asr650xCallOnBigStack(runPins, &a); +} + +void DataStore::pinContact(const ContactInfo &c) { + PinArgs a = { &_be, NULL, NULL, &c, NULL, PIN_ADD }; + asr650xCallOnBigStack(runPins, &a); +} + +void DataStore::unpinContact(const uint8_t *pub_key) { + PinArgs a = { &_be, NULL, NULL, NULL, pub_key, PIN_DEL }; + asr650xCallOnBigStack(runPins, &a); } bool DataStore::formatFileSystem() { asr650x::PersistState empty; memset(&empty, 0, sizeof(empty)); - bool ok = commit_big(empty); - if (ok) asr650x_set_tx_allowed(false); + bool ok = commitBig(empty); + if (ok) asr650xSetTxAllowed(false); + PinArgs a = { &_be, NULL, NULL, NULL, NULL, PIN_CLEAR }; // factory reset also forgets the pinned contacts + asr650xCallOnBigStack(runPins, &a); return ok; } -bool DataStore::loadMainIdentity(mesh::LocalIdentity& identity) { - const asr650x::PersistState& s = _store.state(); +bool DataStore::loadMainIdentity(mesh::LocalIdentity &identity) { + const asr650x::PersistState &s = _store.state(); if (!s.has_identity) return false; uint8_t raw[96]; memcpy(raw, s.prv, 64); @@ -37,9 +130,9 @@ bool DataStore::loadMainIdentity(mesh::LocalIdentity& identity) { return true; } -bool DataStore::saveMainIdentity(const mesh::LocalIdentity& identity) { - if (asr650x_identity_is_placeholder(identity)) { // never persist the placeholder ... - asr650x_set_tx_allowed(false); // ... and never transmit as it (even if a real key was loaded before) +bool DataStore::saveMainIdentity(const mesh::LocalIdentity &identity) { + if (asr650xIdentityIsPlaceholder(identity)) { // never persist the placeholder ... + asr650xSetTxAllowed(false); // ... and never transmit as it (even if a real key was loaded before) return true; } mesh::LocalIdentity copy = identity; @@ -49,21 +142,27 @@ bool DataStore::saveMainIdentity(const mesh::LocalIdentity& identity) { c.has_identity = true; memcpy(c.prv, raw, 64); memcpy(c.pub, raw + 64, 32); - bool ok = commit_big(c); - if (ok) asr650x_set_tx_allowed(true); + bool ok = commitBig(c); + if (ok) asr650xSetTxAllowed(true); return ok; } -void DataStore::loadPrefs(NodePrefs& prefs) { - const asr650x::PersistState& s = _store.state(); - if (!s.has_prefs) { prefs.gps_enabled = 1; return; } // default: GPS on +void DataStore::loadPrefs(NodePrefs &prefs) { + const asr650x::PersistState &s = _store.state(); + if (!s.has_prefs) { + prefs.gps_enabled = 1; + return; + } // default: GPS on prefs.gps_enabled = s.gps_off ? 0 : 1; prefs.gps_interval = s.gps_interval; strncpy(prefs.node_name, s.node_name, sizeof(prefs.node_name) - 1); prefs.node_name[sizeof(prefs.node_name) - 1] = 0; - prefs.freq = s.freq; prefs.bw = s.bw; - prefs.sf = s.sf; prefs.cr = s.cr; prefs.tx_power_dbm = s.tx_power_dbm; - if (!s.has_ext) return; // record from an older build: keep the defaults + prefs.freq = s.freq; + prefs.bw = s.bw; + prefs.sf = s.sf; + prefs.cr = s.cr; + prefs.tx_power_dbm = s.tx_power_dbm; + if (!s.has_ext) return; // record from an older build: keep the defaults prefs.airtime_factor = s.airtime_factor; prefs.rx_delay_base = s.rx_delay_base; prefs.tx_delay_factor = s.tx_delay_factor; @@ -84,15 +183,18 @@ void DataStore::loadPrefs(NodePrefs& prefs) { prefs.rx_boosted_gain = s.rx_boosted_gain; } -bool DataStore::savePrefs(NodePrefs& prefs) { +bool DataStore::savePrefs(NodePrefs &prefs) { asr650x::PersistState c = _store.state(); c.has_prefs = true; c.gps_off = prefs.gps_enabled == 0; c.gps_interval = prefs.gps_interval; strncpy(c.node_name, prefs.node_name, sizeof(c.node_name) - 1); c.node_name[sizeof(c.node_name) - 1] = 0; - c.freq = prefs.freq; c.bw = prefs.bw; - c.sf = prefs.sf; c.cr = prefs.cr; c.tx_power_dbm = prefs.tx_power_dbm; + c.freq = prefs.freq; + c.bw = prefs.bw; + c.sf = prefs.sf; + c.cr = prefs.cr; + c.tx_power_dbm = prefs.tx_power_dbm; c.has_ext = true; c.airtime_factor = prefs.airtime_factor; c.rx_delay_base = prefs.rx_delay_base; @@ -100,8 +202,9 @@ bool DataStore::savePrefs(NodePrefs& prefs) { c.direct_tx_delay_factor = prefs.direct_tx_delay_factor; c.multi_acks = prefs.multi_acks; c.manual_add_contacts = prefs.manual_add_contacts; - c.telemetry_modes = (uint8_t)((prefs.telemetry_mode_base & 0x03) | ((prefs.telemetry_mode_loc & 0x03) << 2) | - ((prefs.telemetry_mode_env & 0x03) << 4)); + c.telemetry_modes = + (uint8_t)((prefs.telemetry_mode_base & 0x03) | ((prefs.telemetry_mode_loc & 0x03) << 2) | + ((prefs.telemetry_mode_env & 0x03) << 4)); c.advert_loc_policy = prefs.advert_loc_policy; c.autoadd_config = prefs.autoadd_config; c.autoadd_max_hops = prefs.autoadd_max_hops; @@ -111,6 +214,6 @@ bool DataStore::savePrefs(NodePrefs& prefs) { c.interference_threshold = prefs.interference_threshold; c.agc_reset_interval = prefs.agc_reset_interval; c.rx_boosted_gain = prefs.rx_boosted_gain; - return commit_big(c); + return commitBig(c); } -#endif // ASR650X_PLATFORM +#endif // ASR650X_PLATFORM diff --git a/examples/companion_radio/FlashDataStore.h b/examples/companion_radio/FlashDataStore.h index 82dc9ccc47..2d56d827d7 100644 --- a/examples/companion_radio/FlashDataStore.h +++ b/examples/companion_radio/FlashDataStore.h @@ -1,37 +1,43 @@ #pragma once +#include "NodePrefs.h" + #include -#include #include -#include "NodePrefs.h" -#include +#include #include +#include class DataStoreHost { public: - virtual bool onContactLoaded(const ContactInfo& contact) = 0; - virtual bool getContactForSave(uint32_t idx, ContactInfo& contact) = 0; - virtual bool onChannelLoaded(uint8_t channel_idx, const ChannelDetails& ch) = 0; - virtual bool getChannelForSave(uint8_t channel_idx, ChannelDetails& ch) = 0; + virtual bool onContactLoaded(const ContactInfo &contact) = 0; + virtual bool getContactForSave(uint32_t idx, ContactInfo &contact) = 0; + virtual bool onChannelLoaded(uint8_t channel_idx, const ChannelDetails &ch) = 0; + virtual bool getChannelForSave(uint8_t channel_idx, ChannelDetails &ch) = 0; }; // Same surface MyMesh uses from MeshCore's file-based DataStore. Identity + name + radio params go to -// EEPROM (CcPersist record); contacts/channels/blobs are RAM-only by design (M1 scope). +// SFLASH rows 0/1 (FlashRecord, A/B). Up to 3 contacts added or edited by the app are pinned in SFLASH row 2 +// (PinnedContacts.h); all other contacts, channels and blobs are RAM-only. class DataStore { FlashRowBackend _be; asr650x::PersistStore _store; - bool commit_big(const asr650x::PersistState& c); // runs the flash commit on the crypto stack (main stack is only 2 KB) + bool commitBig( + const asr650x::PersistState &c); // runs the flash commit on the crypto stack (main stack is only 2 KB) public: DataStore() : _store(_be) {} void begin(); - bool formatFileSystem(); // factory reset: erases identity + prefs record - bool loadMainIdentity(mesh::LocalIdentity& identity); - bool saveMainIdentity(const mesh::LocalIdentity& identity); - void loadPrefs(NodePrefs& prefs); - bool savePrefs(NodePrefs& prefs); - void loadContacts(DataStoreHost*) {} - void saveContacts(DataStoreHost*, bool (*filter)(const ContactInfo& c) = NULL) {} - void loadChannels(DataStoreHost*) {} - void saveChannels(DataStoreHost*) {} + bool formatFileSystem(); // factory reset: erases identity + prefs record and the pinned contacts + bool loadMainIdentity(mesh::LocalIdentity &identity); + bool saveMainIdentity(const mesh::LocalIdentity &identity); + void loadPrefs(NodePrefs &prefs); + bool savePrefs(NodePrefs &prefs); + void loadContacts(DataStoreHost *host); // pinned contacts back into the table at boot + void saveContacts(DataStoreHost *host, + bool (*filter)(const ContactInfo &c) = NULL); // refreshes pinned copies + void pinContact(const ContactInfo &c); // the app added or edited this contact + void unpinContact(const uint8_t *pub_key); // the app removed it + void loadChannels(DataStoreHost *) {} + void saveChannels(DataStoreHost *) {} uint8_t getBlobByKey(const uint8_t[], int, uint8_t[]) { return 0; } bool putBlobByKey(const uint8_t[], int, const uint8_t[], uint8_t) { return false; } bool deleteBlobByKey(const uint8_t[], int) { return false; } diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 20141fe936..dd1f8aa79c 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1339,6 +1339,9 @@ void MyMesh::handleCmdFrame(size_t len) { if (recipient) { updateContactFromFrame(*recipient, last_mod, cmd_frame, len); recipient->lastmod = last_mod; +#ifdef CONTACT_PIN_STORE + _store->pinContact(*recipient); // contacts the app adds or edits survive a reboot (store-specific limit) +#endif dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); writeOKFrame(); } else { @@ -1347,6 +1350,9 @@ void MyMesh::handleCmdFrame(size_t len) { contact.lastmod = last_mod; contact.sync_since = 0; if (addContact(contact)) { +#ifdef CONTACT_PIN_STORE + _store->pinContact(contact); +#endif dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); writeOKFrame(); } else { @@ -1358,6 +1364,9 @@ void MyMesh::handleCmdFrame(size_t len) { ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE); if (recipient && removeContact(*recipient)) { _store->deleteBlobByKey(pub_key, PUB_KEY_SIZE); +#ifdef CONTACT_PIN_STORE + _store->unpinContact(pub_key); +#endif dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); writeOKFrame(); } else { diff --git a/src/helpers/asr650x/FlashRowBackend.h b/src/helpers/asr650x/FlashRowBackend.h index 77abcc59ff..c028d94255 100644 --- a/src/helpers/asr650x/FlashRowBackend.h +++ b/src/helpers/asr650x/FlashRowBackend.h @@ -1,23 +1,23 @@ #pragma once #include -#include #include +#include // Writes straight to the SFLASH user rows through the core's FLASH_update(), one record per 256-byte row. // (The EEPROM class would rewrite every row of its RAM mirror on each commit, which defeats the A/B scheme.) extern "C" { -int FLASH_update(uint32_t dst_addr, const void* data, uint32_t size); -int FLASH_read_at(uint32_t address, uint8_t* pData, uint32_t len_bytes); +int FLASH_update(uint32_t dst_addr, const void *data, uint32_t size); +int FLASH_read_at(uint32_t address, uint8_t *pData, uint32_t len_bytes); } struct FlashRowBackend { - void read(size_t off, uint8_t* buf, size_t n) { FLASH_read_at(CY_SFLASH_USERBASE + off, buf, n); } + void read(size_t off, uint8_t *buf, size_t n) { FLASH_read_at(CY_SFLASH_USERBASE + off, buf, n); } - bool write(size_t off, const uint8_t* buf, size_t n) { - if (n > asr650x::PERSIST_SIZE) return false; + bool write(size_t off, const uint8_t *buf, size_t n) { + if (n > 255) return false; // one SFLASH user row if (FLASH_update(CY_SFLASH_USERBASE + off, buf, n) != 0) return false; - uint8_t chk[asr650x::PERSIST_SIZE]; + uint8_t chk[255]; FLASH_read_at(CY_SFLASH_USERBASE + off, chk, n); - return memcmp(chk, buf, n) == 0; // FLASH_update only prints on a failed row program and still returns 0 + return memcmp(chk, buf, n) == 0; // FLASH_update only prints on a failed row program and still returns 0 } }; diff --git a/src/helpers/asr650x/PinnedContacts.h b/src/helpers/asr650x/PinnedContacts.h new file mode 100644 index 0000000000..0ad72a28fa --- /dev/null +++ b/src/helpers/asr650x/PinnedContacts.h @@ -0,0 +1,117 @@ +#pragma once + +#include "FlashRecord.h" // crc32 + +#include +#include + +// Pinned contacts: up to 3 contacts that the app added or edited survive a reboot (the rest of the contact +// table stays in RAM). Stored in ONE SFLASH user row: the core lets user code write rows 0..2, and rows 0/1 +// hold the A/B identity + prefs record, so there is no A/B copy here. A power loss while this row is written +// loses the pins (CRC fails -> empty set), never the identity. Layout (210 bytes, little-endian): 0..3 "PIN1" +// | 4 count | 5..7 0 | 8 + i*66: pub[32] name[32] type flags +// | 206..209 CRC-32 of bytes 0..205 +namespace asr650x { + +const int PIN_MAX = 3; +const size_t PIN_ENTRY = 66; +const size_t PIN_SIZE = 8 + PIN_MAX * PIN_ENTRY + 4; + +struct PinEntry { + uint8_t pub[32]; + char name[32]; // NUL-terminated, at most 31 characters + uint8_t type; + uint8_t flags; +}; + +struct PinSet { + uint8_t n; + PinEntry e[PIN_MAX]; + PinSet() : n(0) { memset(e, 0, sizeof(e)); } +}; + +enum PinResult { PIN_SAME = 0, PIN_CHANGED = 1, PIN_FULL = 2 }; + +inline int pinFind(const PinSet &s, const uint8_t pub[32]) { + for (int i = 0; i < s.n; i++) + if (memcmp(s.e[i].pub, pub, 32) == 0) return i; + return -1; +} + +inline PinResult pinSetFields(PinEntry &e, const char *name, uint8_t type, uint8_t flags) { + char nm[32]; + memset(nm, 0, sizeof(nm)); + if (name) strncpy(nm, name, 31); + if (memcmp(e.name, nm, 32) == 0 && e.type == type && e.flags == flags) return PIN_SAME; + memcpy(e.name, nm, 32); + e.type = type; + e.flags = flags; + return PIN_CHANGED; +} + +// pins (or refreshes) a contact; PIN_FULL when 3 others are pinned already +inline PinResult pinUpsert(PinSet &s, const uint8_t pub[32], const char *name, uint8_t type, uint8_t flags) { + int i = pinFind(s, pub); + if (i >= 0) return pinSetFields(s.e[i], name, type, flags); + if (s.n >= PIN_MAX) return PIN_FULL; + PinEntry &e = s.e[s.n++]; + memset(&e, 0, sizeof(e)); + memcpy(e.pub, pub, 32); + pinSetFields(e, name, type, flags); + return PIN_CHANGED; +} + +// updates the stored copy of an already pinned contact; never pins a new one +inline PinResult pinRefresh(PinSet &s, const uint8_t pub[32], const char *name, uint8_t type, uint8_t flags) { + int i = pinFind(s, pub); + return i < 0 ? PIN_SAME : pinSetFields(s.e[i], name, type, flags); +} + +inline bool pinRemove(PinSet &s, const uint8_t pub[32]) { + int i = pinFind(s, pub); + if (i < 0) return false; + for (int k = i; k + 1 < s.n; k++) + s.e[k] = s.e[k + 1]; + s.n--; + memset(&s.e[s.n], 0, sizeof(s.e[s.n])); + return true; +} + +inline void pinsEncode(const PinSet &s, uint8_t out[PIN_SIZE]) { + memset(out, 0, PIN_SIZE); + memcpy(out, "PIN1", 4); + out[4] = s.n; + for (int i = 0; i < s.n && i < PIN_MAX; i++) { + uint8_t *p = out + 8 + i * PIN_ENTRY; + memcpy(p, s.e[i].pub, 32); + memcpy(p + 32, s.e[i].name, 31); // byte 63 stays 0: always terminated + p[64] = s.e[i].type; + p[65] = s.e[i].flags; + } + uint32_t crc = crc32(out, PIN_SIZE - 4); + for (int k = 0; k < 4; k++) + out[PIN_SIZE - 4 + k] = (uint8_t)(crc >> (8 * k)); +} + +// false (and an empty set) for an erased, corrupt or foreign row +inline bool pinsDecode(const uint8_t in[PIN_SIZE], PinSet &s) { + s = PinSet(); + if (memcmp(in, "PIN1", 4) != 0) return false; + uint32_t want = 0; + for (int k = 0; k < 4; k++) + want |= (uint32_t)in[PIN_SIZE - 4 + k] << (8 * k); + if (crc32(in, PIN_SIZE - 4) != want) return false; + if (in[4] > PIN_MAX) return false; + s.n = in[4]; + for (int i = 0; i < s.n; i++) { + const uint8_t *p = in + 8 + i * PIN_ENTRY; + memcpy(s.e[i].pub, p, 32); + memcpy(s.e[i].name, p + 32, 31); + s.e[i].name[31] = 0; + s.e[i].type = p[64]; + s.e[i].flags = p[65]; + } + return true; +} + +} // namespace asr650x diff --git a/test/test_asr650x_pins/test_asr650x_pins.cpp b/test/test_asr650x_pins/test_asr650x_pins.cpp new file mode 100644 index 0000000000..8de7f48373 --- /dev/null +++ b/test/test_asr650x_pins/test_asr650x_pins.cpp @@ -0,0 +1,126 @@ +#include "helpers/asr650x/PinnedContacts.h" + +#include +#include + +static void key(uint8_t k[32], uint8_t v) { + for (int i = 0; i < 32; i++) + k[i] = (uint8_t)(v + i); +} + +TEST(Asr650xPins, ErasedOrGarbageRowIsEmpty) { + uint8_t row[asr650x::PIN_SIZE]; + asr650x::PinSet s; + memset(row, 0xFF, sizeof(row)); + EXPECT_FALSE(asr650x::pinsDecode(row, s)); + EXPECT_EQ(s.n, 0); + memset(row, 0x00, sizeof(row)); + EXPECT_FALSE(asr650x::pinsDecode(row, s)); + EXPECT_EQ(s.n, 0); +} + +TEST(Asr650xPins, AtMostThreeAndRepinOnlyRefreshes) { + uint8_t a[32], b[32], c[32], d[32]; + key(a, 0x10); + key(b, 0x40); + key(c, 0x70); + key(d, 0xA0); + asr650x::PinSet s; + EXPECT_EQ(asr650x::pinUpsert(s, a, "nRF-A", 1, 0), asr650x::PIN_CHANGED); + EXPECT_EQ(asr650x::pinUpsert(s, b, "nRF-B", 1, 0), asr650x::PIN_CHANGED); + EXPECT_EQ(asr650x::pinUpsert(s, c, "room", 3, 1), asr650x::PIN_CHANGED); + EXPECT_EQ(s.n, 3); + EXPECT_EQ(asr650x::pinUpsert(s, d, "fourth", 1, 0), asr650x::PIN_FULL); + EXPECT_EQ(s.n, 3); + EXPECT_EQ(asr650x::pinUpsert(s, b, "nRF-B", 1, 0), asr650x::PIN_SAME); + EXPECT_EQ(asr650x::pinUpsert(s, b, "nRF-B2", 1, 0), asr650x::PIN_CHANGED); + EXPECT_STREQ(s.e[1].name, "nRF-B2"); + EXPECT_LT(asr650x::pinFind(s, d), 0); + EXPECT_EQ(asr650x::pinFind(s, c), 2); +} + +TEST(Asr650xPins, RefreshNeverPinsANewContact) { + uint8_t a[32], d[32]; + key(a, 0x10); + key(d, 0xA0); + asr650x::PinSet s; + asr650x::pinUpsert(s, a, "A", 1, 0); + EXPECT_EQ(asr650x::pinRefresh(s, d, "D", 1, 0), asr650x::PIN_SAME); + EXPECT_EQ(s.n, 1); + EXPECT_EQ(asr650x::pinRefresh(s, a, "A-new", 2, 4), asr650x::PIN_CHANGED); + EXPECT_EQ(s.e[0].type, 2); + EXPECT_EQ(s.e[0].flags, 4); +} + +TEST(Asr650xPins, RemoveKeepsOrder) { + uint8_t a[32], b[32], c[32], d[32]; + key(a, 0x10); + key(b, 0x40); + key(c, 0x70); + key(d, 0xA0); + asr650x::PinSet s; + asr650x::pinUpsert(s, a, "A", 1, 0); + asr650x::pinUpsert(s, b, "B", 1, 0); + asr650x::pinUpsert(s, c, "C", 1, 0); + EXPECT_TRUE(asr650x::pinRemove(s, b)); + EXPECT_EQ(s.n, 2); + EXPECT_EQ(asr650x::pinFind(s, a), 0); + EXPECT_EQ(asr650x::pinFind(s, c), 1); + EXPECT_FALSE(asr650x::pinRemove(s, d)); + EXPECT_EQ(s.n, 2); +} + +TEST(Asr650xPins, RoundTripAndEveryByteCovered) { + uint8_t a[32], c[32]; + key(a, 0x10); + key(c, 0x70); + asr650x::PinSet s, t; + asr650x::pinUpsert(s, a, "A-VERY-LONG-NODE-NAME-THAT-DOES-NOT-FIT", 1, 2); + asr650x::pinUpsert(s, c, "C", 3, 0); + uint8_t row[asr650x::PIN_SIZE]; + asr650x::pinsEncode(s, row); + ASSERT_TRUE(asr650x::pinsDecode(row, t)); + EXPECT_EQ(t.n, 2); + EXPECT_EQ(memcmp(t.e[0].pub, a, 32), 0); + EXPECT_EQ(strlen(t.e[0].name), 31u); + EXPECT_EQ(t.e[0].type, 1); + EXPECT_EQ(t.e[0].flags, 2); + EXPECT_STREQ(t.e[1].name, "C"); + for (size_t i = 0; i < asr650x::PIN_SIZE; i++) { + uint8_t bad[asr650x::PIN_SIZE]; + memcpy(bad, row, sizeof(bad)); + bad[i] ^= 0x01; + asr650x::PinSet u; + EXPECT_FALSE(asr650x::pinsDecode(bad, u)) << i; + EXPECT_EQ(u.n, 0); + } +} + +TEST(Asr650xPins, CountAboveThreeRejectedEvenWithValidCrc) { + uint8_t a[32]; + key(a, 0x10); + asr650x::PinSet s; + asr650x::pinUpsert(s, a, "A", 1, 0); + uint8_t row[asr650x::PIN_SIZE]; + asr650x::pinsEncode(s, row); + row[4] = 4; + uint32_t crc = asr650x::crc32(row, asr650x::PIN_SIZE - 4); + for (int i = 0; i < 4; i++) + row[asr650x::PIN_SIZE - 4 + i] = (uint8_t)(crc >> (8 * i)); + asr650x::PinSet t; + EXPECT_FALSE(asr650x::pinsDecode(row, t)); + EXPECT_EQ(t.n, 0); +} + +TEST(Asr650xPins, EmptySetIsAValidRecord) { + asr650x::PinSet s, t; + uint8_t row[asr650x::PIN_SIZE]; + asr650x::pinsEncode(s, row); + EXPECT_TRUE(asr650x::pinsDecode(row, t)); + EXPECT_EQ(t.n, 0); +} + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/variants/heltec_cubecell_ab02s/platformio.ini b/variants/heltec_cubecell_ab02s/platformio.ini index 925dfbfdbf..4b25cbc6b7 100644 --- a/variants/heltec_cubecell_ab02s/platformio.ini +++ b/variants/heltec_cubecell_ab02s/platformio.ini @@ -30,6 +30,7 @@ build_flags = ${arduino_base.build_flags} -D DISABLE_CLI_RESCUE -D DISABLE_COMPANION_CLI -D SERIAL_FRAME_STRICT + -D CONTACT_PIN_STORE -D CONFIG_SERIALIZER_NOOP -D ED25519_SMALL_BASE_TABLE -D IDENTITY_CRYPTO_ON_BIG_STACK From 15ed6a29d73415169a4d327561bba223ba1c4e42 Mon Sep 17 00:00:00 2001 From: HAN Date: Wed, 7 Oct 2026 07:36:34 +0700 Subject: [PATCH 14/16] ASR650x / AB02S: upstream style (clang-format, camelCase), neutral record magic Function names follow the project's camelCase convention, new files are formatted with .clang-format, project-internal references are removed from comments and tests. The flash record is now written with the "ASR1" magic; records from early test builds ("KTM1") still load. --- src/Identity.cpp | 2 +- src/helpers/ContactPath.h | 40 +++-- src/helpers/FastPow.h | 26 +-- src/helpers/asr650x/BigStack.cpp | 44 +++-- src/helpers/asr650x/BigStack.h | 10 +- src/helpers/asr650x/Button.h | 29 +-- src/helpers/asr650x/CubeCellRTC.h | 4 +- src/helpers/asr650x/CubeCellRadio.cpp | 145 +++++++++------ src/helpers/asr650x/CubeCellRadio.h | 32 ++-- src/helpers/asr650x/EpochClock.h | 14 +- src/helpers/asr650x/FlashRecord.h | 108 +++++++---- src/helpers/asr650x/FlashRecordStore.h | 42 +++-- src/helpers/asr650x/Font5x7.h | 60 ++++--- src/helpers/asr650x/GpsPolicy.h | 46 +++-- src/helpers/asr650x/IdentityCrypto.cpp | 53 ++++-- src/helpers/asr650x/IdentityCrypto.h | 27 +-- src/helpers/asr650x/NmeaParser.h | 167 +++++++++++------ src/helpers/asr650x/OledText.h | 14 +- src/helpers/asr650x/RadioParams.h | 56 +++--- src/helpers/asr650x/Sbrk.cpp | 26 +-- src/helpers/asr650x/SmallStrtod.cpp | 15 +- src/helpers/asr650x/SmallStrtod.h | 60 +++++-- src/helpers/asr650x/StatusLines.h | 122 +++++++------ src/helpers/asr650x/TxGate.cpp | 8 +- src/helpers/asr650x/TxGate.h | 4 +- .../test_asr650x_button.cpp | 20 ++- .../test_asr650x_epoch_clock.cpp | 28 +-- .../test_asr650x_gps_policy.cpp | 49 ++--- test/test_asr650x_nmea/test_asr650x_nmea.cpp | 114 +++++++----- .../test_asr650x_oled_text.cpp | 60 ++++--- .../test_asr650x_persist.cpp | 168 ++++++++++++------ .../test_asr650x_persist_store.cpp | 80 ++++++--- .../test_asr650x_radio_params.cpp | 74 +++++--- .../test_asr650x_status.cpp | 70 +++++--- .../test_asr650x_strtod.cpp | 32 ++-- .../test_config_serializer_noop.cpp | 44 +++-- test/test_contact_path/test_contact_path.cpp | 46 +++-- .../test_ed25519_vectors.cpp | 35 ++-- test/test_fast_pow/test_fast_pow.cpp | 7 +- .../test_serial_frame_strict.cpp | 37 ++-- .../heltec_cubecell_ab02s/CubeCellBoard.cpp | 68 ++++--- .../heltec_cubecell_ab02s/CubeCellBoard.h | 17 +- .../heltec_cubecell_ab02s/CubeCellGPS.cpp | 51 +++--- variants/heltec_cubecell_ab02s/CubeCellGPS.h | 21 +-- .../heltec_cubecell_ab02s/CubeCellSensors.cpp | 58 ++++-- .../heltec_cubecell_ab02s/CubeCellSensors.h | 15 +- variants/heltec_cubecell_ab02s/Oled.cpp | 87 ++++----- variants/heltec_cubecell_ab02s/Oled.h | 18 +- .../heltec_cubecell_ab02s/StatusScreen.cpp | 31 ++-- variants/heltec_cubecell_ab02s/StatusScreen.h | 25 ++- .../heltec_cubecell_ab02s/shims/CayenneLPP.h | 15 +- variants/heltec_cubecell_ab02s/target.cpp | 20 ++- variants/heltec_cubecell_ab02s/target.h | 19 +- 53 files changed, 1507 insertions(+), 956 deletions(-) diff --git a/src/Identity.cpp b/src/Identity.cpp index ec25756470..d5a91ec567 100644 --- a/src/Identity.cpp +++ b/src/Identity.cpp @@ -36,7 +36,7 @@ bool Identity::verify(const uint8_t* sig, const uint8_t* message, int msg_len) c (uint8_t*)message, (size_t)msg_len, &cc310_tmp); return rc == CRYS_OK; #elif defined(IDENTITY_CRYPTO_ON_BIG_STACK) - return asr650x_ed25519_verify(sig, this->pub_key, message, msg_len); + return asr650xEd25519Verify(sig, this->pub_key, message, msg_len); #elif 0 // NOTE: memory corruption bug was found in this function!! return ed25519_verify(sig, message, msg_len, pub_key); diff --git a/src/helpers/ContactPath.h b/src/helpers/ContactPath.h index 998124a552..c92a571894 100644 --- a/src/helpers/ContactPath.h +++ b/src/helpers/ContactPath.h @@ -1,18 +1,18 @@ #pragma once +#include // MAX_PATH_SIZE #include #include -#include // MAX_PATH_SIZE -// Bytes reserved for a contact's stored direct route. Boards with very little RAM can trade long direct routes -// for contact table space: a route that does not fit is not stored (OUT_PATH_UNKNOWN), and messages to that -// contact go out flood instead. The default keeps the full MAX_PATH_SIZE. +// Bytes reserved for a contact's stored direct route. Boards with very little RAM can trade long direct +// routes for contact table space: a route that does not fit is not stored (OUT_PATH_UNKNOWN), and messages to +// that contact go out flood instead. The default keeps the full MAX_PATH_SIZE. #ifndef CONTACT_OUT_PATH_SIZE - #define CONTACT_OUT_PATH_SIZE MAX_PATH_SIZE +#define CONTACT_OUT_PATH_SIZE MAX_PATH_SIZE #endif #ifndef OUT_PATH_UNKNOWN - #define OUT_PATH_UNKNOWN 0xFF +#define OUT_PATH_UNKNOWN 0xFF #endif // Number of bytes of an encoded path_len (upper 2 bits: hash size - 1, lower 6 bits: hop count), @@ -23,8 +23,9 @@ inline uint8_t contactPathBytes(uint8_t path_len) { return (uint8_t)((path_len & 63) * hash_size); } -// Stores a route into a contact's out_path. Returns the stored path_len, or OUT_PATH_UNKNOWN when it does not fit. -inline uint8_t contactStorePath(uint8_t* dest, const uint8_t* src, uint8_t path_len) { +// Stores a route into a contact's out_path. Returns the stored path_len, or OUT_PATH_UNKNOWN when it does not +// fit. +inline uint8_t contactStorePath(uint8_t *dest, const uint8_t *src, uint8_t path_len) { if (path_len == OUT_PATH_UNKNOWN) return OUT_PATH_UNKNOWN; uint8_t n = contactPathBytes(path_len); if (n > CONTACT_OUT_PATH_SIZE || n > MAX_PATH_SIZE) return OUT_PATH_UNKNOWN; @@ -32,37 +33,38 @@ inline uint8_t contactStorePath(uint8_t* dest, const uint8_t* src, uint8_t path_ return path_len; } -// Protocol frames and files always carry MAX_PATH_SIZE path bytes: copies the stored route and zeroes the rest. -inline void contactWritePath64(uint8_t* dest64, const uint8_t* stored, uint8_t path_len) { +// Protocol frames and files always carry MAX_PATH_SIZE path bytes: copies the stored route and zeroes the +// rest. +inline void contactWritePath64(uint8_t *dest64, const uint8_t *stored, uint8_t path_len) { memset(dest64, 0, MAX_PATH_SIZE); if (path_len == OUT_PATH_UNKNOWN) return; uint8_t n = contactPathBytes(path_len); - if (n > CONTACT_OUT_PATH_SIZE) return; // never stored that way: nothing valid to copy + if (n > CONTACT_OUT_PATH_SIZE) return; // never stored that way: nothing valid to copy memcpy(dest64, stored, n); } -// Call-site helpers. With the default CONTACT_OUT_PATH_SIZE they compile to exactly the operation each call site -// used before (so other boards are unchanged); with a smaller size they clamp as above. +// Call-site helpers. With the default CONTACT_OUT_PATH_SIZE they compile to exactly the operation each call +// site used before (so other boards are unchanged); with a smaller size they clamp as above. #if CONTACT_OUT_PATH_SIZE < MAX_PATH_SIZE -inline uint8_t contactPathFromPacket(uint8_t* dest, const uint8_t* src, uint8_t path_len) { +inline uint8_t contactPathFromPacket(uint8_t *dest, const uint8_t *src, uint8_t path_len) { return contactStorePath(dest, src, path_len); } -inline uint8_t contactPathFrom64(uint8_t* dest, const uint8_t* src64, uint8_t path_len) { +inline uint8_t contactPathFrom64(uint8_t *dest, const uint8_t *src64, uint8_t path_len) { return contactStorePath(dest, src64, path_len); } -inline void contactPathTo64(uint8_t* dest64, const uint8_t* stored, uint8_t path_len) { +inline void contactPathTo64(uint8_t *dest64, const uint8_t *stored, uint8_t path_len) { contactWritePath64(dest64, stored, path_len); } #else #include -inline uint8_t contactPathFromPacket(uint8_t* dest, const uint8_t* src, uint8_t path_len) { +inline uint8_t contactPathFromPacket(uint8_t *dest, const uint8_t *src, uint8_t path_len) { return mesh::Packet::copyPath(dest, src, path_len); } -inline uint8_t contactPathFrom64(uint8_t* dest, const uint8_t* src64, uint8_t path_len) { +inline uint8_t contactPathFrom64(uint8_t *dest, const uint8_t *src64, uint8_t path_len) { memcpy(dest, src64, MAX_PATH_SIZE); return path_len; } -inline void contactPathTo64(uint8_t* dest64, const uint8_t* stored, uint8_t path_len) { +inline void contactPathTo64(uint8_t *dest64, const uint8_t *stored, uint8_t path_len) { (void)path_len; memcpy(dest64, stored, MAX_PATH_SIZE); } diff --git a/src/helpers/FastPow.h b/src/helpers/FastPow.h index cf37225d09..8c2d73f824 100644 --- a/src/helpers/FastPow.h +++ b/src/helpers/FastPow.h @@ -3,36 +3,42 @@ #include #include -// Single-precision powers without libm, for calcRxDelay() on boards built with RX_DELAY_FAST_POW, where libm's -// pow() and the double-precision routines it pulls in do not fit in flash. Relative error is well under 1 %. +// Single-precision powers without libm, for calcRxDelay() on boards built with RX_DELAY_FAST_POW, where +// libm's pow() and the double-precision routines it pulls in do not fit in flash. Relative error is well +// under 1 %. // 2^y: integer power of two times a 4th-order polynomial for the fractional part inline float fastPow2f(float y) { if (y > 30.0f) y = 30.0f; if (y < -30.0f) y = -30.0f; int k = (int)y; - if ((float)k > y) k--; // floor - float f = y - (float)k; // [0, 1) + if ((float)k > y) k--; // floor + float f = y - (float)k; // [0, 1) float p = 1.0f + f * (0.6931472f + f * (0.2402265f + f * (0.0555041f + f * 0.0096181f))); return k >= 0 ? p * (float)(1u << k) : p / (float)(1u << (-k)); } -// log2(x) for x > 0: exponent from the IEEE-754 bits, mantissa m in [1, 2) through ln(m) = 2 atanh((m-1)/(m+1)) +// log2(x) for x > 0: exponent from the IEEE-754 bits, mantissa m in [1, 2) through ln(m) = 2 +// atanh((m-1)/(m+1)) inline float fastLog2f(float x) { uint32_t bits; memcpy(&bits, &x, 4); int e = (int)((bits >> 23) & 0xFF) - 127; - bits = (bits & 0x007FFFFFu) | 0x3F800000u; // mantissa as a float in [1, 2) + bits = (bits & 0x007FFFFFu) | 0x3F800000u; // mantissa as a float in [1, 2) float m; memcpy(&m, &bits, 4); - float z = (m - 1.0f) / (m + 1.0f); // [0, 1/3) + float z = (m - 1.0f) / (m + 1.0f); // [0, 1/3) float z2 = z * z; float ln_m = 2.0f * z * (1.0f + z2 * (0.3333333f + z2 * (0.2f + z2 * 0.1428571f))); - return (float)e + ln_m * 1.4426950f; // 1 / ln(2) + return (float)e + ln_m * 1.4426950f; // 1 / ln(2) } // base^x for base > 0 -inline float fastPowf(float base, float x) { return fastPow2f(x * fastLog2f(base)); } +inline float fastPowf(float base, float x) { + return fastPow2f(x * fastLog2f(base)); +} // 10^x -inline float fastPow10f(float x) { return fastPow2f(x * 3.3219281f); } +inline float fastPow10f(float x) { + return fastPow2f(x * 3.3219281f); +} diff --git a/src/helpers/asr650x/BigStack.cpp b/src/helpers/asr650x/BigStack.cpp index bfc38e97a1..184b7c3888 100644 --- a/src/helpers/asr650x/BigStack.cpp +++ b/src/helpers/asr650x/BigStack.cpp @@ -1,40 +1,46 @@ #include "BigStack.h" + #include #include -static uint8_t* g_stack = 0; +static uint8_t *g_stack = 0; static size_t g_size = 0; -bool asr650x_bigstack_init(size_t bytes) { +bool asr650xBigstackInit(size_t bytes) { if (g_stack) return true; - g_stack = (uint8_t*)malloc(bytes); + g_stack = (uint8_t *)malloc(bytes); if (!g_stack) return false; g_size = bytes; memset(g_stack, 0xA5, bytes); return true; } -bool asr650x_bigstack_ready() { return g_stack != 0; } +bool asr650xBigstackReady() { + return g_stack != 0; +} -size_t asr650x_bigstack_highwater() { +size_t asr650xBigstackHighwater() { if (!g_stack) return 0; size_t i = 0; - while (i < g_size && g_stack[i] == 0xA5) i++; + while (i < g_size && g_stack[i] == 0xA5) + i++; return g_size - i; } -void asr650x_call_on_big_stack(void (*fn)(void*), void* ctx) { - if (!g_stack) { fn(ctx); return; } // not initialised: stay on the current stack - uint32_t top = ((uint32_t)(g_stack + g_size)) & ~7u; // 8-byte aligned top of stack - register void* r0 asm("r0") = ctx; - register void (*r1)(void*) asm("r1") = fn; +void asr650xCallOnBigStack(void (*fn)(void *), void *ctx) { + if (!g_stack) { + fn(ctx); + return; + } // not initialised: stay on the current stack + uint32_t top = ((uint32_t)(g_stack + g_size)) & ~7u; // 8-byte aligned top of stack + register void *r0 asm("r0") = ctx; + register void (*r1)(void *) asm("r1") = fn; register uint32_t r2 asm("r2") = top; - asm volatile( - "mov r4, sp\n\t" - "mov sp, r2\n\t" - "blx r1\n\t" - "mov sp, r4\n\t" - : "+r"(r0), "+r"(r1), "+r"(r2) - : - : "r3", "r4", "r12", "lr", "memory", "cc"); + asm volatile("mov r4, sp\n\t" + "mov sp, r2\n\t" + "blx r1\n\t" + "mov sp, r4\n\t" + : "+r"(r0), "+r"(r1), "+r"(r2) + : + : "r3", "r4", "r12", "lr", "memory", "cc"); } diff --git a/src/helpers/asr650x/BigStack.h b/src/helpers/asr650x/BigStack.h index be729851a6..e06339a08f 100644 --- a/src/helpers/asr650x/BigStack.h +++ b/src/helpers/asr650x/BigStack.h @@ -3,8 +3,8 @@ #include // Runs memory-hungry crypto on a separate stack taken from the (mostly empty) heap, because the -// linker script fixes the main stack at 2 KB (M0 report, R9). -bool asr650x_bigstack_init(size_t bytes); -bool asr650x_bigstack_ready(); // false if the heap could not provide the stack -size_t asr650x_bigstack_highwater(); // bytes of the big stack ever used -void asr650x_call_on_big_stack(void (*fn)(void*), void* ctx); +// linker script fixes the main stack at 2 KB, and Ed25519 needs ~1.8 KB on its own. +bool asr650xBigstackInit(size_t bytes); +bool asr650xBigstackReady(); // false if the heap could not provide the stack +size_t asr650xBigstackHighwater(); // bytes of the big stack ever used +void asr650xCallOnBigStack(void (*fn)(void *), void *ctx); diff --git a/src/helpers/asr650x/Button.h b/src/helpers/asr650x/Button.h index c1655b3745..bdff5632b2 100644 --- a/src/helpers/asr650x/Button.h +++ b/src/helpers/asr650x/Button.h @@ -6,22 +6,23 @@ namespace asr650x { // Debounced press detector for the USER key (active LOW, shared with the battery divider control). class Button { static const uint32_t DEBOUNCE_MS = 30; - bool _down; // raw level seen at the last update - bool _armed; // a press may start (the key was seen released after the last hold-off/press) + bool _down; // raw level seen at the last update + bool _armed; // a press may start (the key was seen released after the last hold-off/press) bool _fired; uint32_t _t_down; uint32_t _holdoff_until; bool _holdoff_active; - static bool before(uint32_t a, uint32_t b) { return (int32_t)(a - b) < 0; } // wrap-safe a < b + static bool before(uint32_t a, uint32_t b) { return (int32_t)(a - b) < 0; } // wrap-safe a < b public: - Button() : _down(false), _armed(true), _fired(false), _t_down(0), _holdoff_until(0), _holdoff_active(false) {} + Button() + : _down(false), _armed(true), _fired(false), _t_down(0), _holdoff_until(0), _holdoff_active(false) {} - void holdoff(uint32_t now_ms, uint32_t ms) { // e.g. right after getBatteryVoltage() released the pin + void holdoff(uint32_t now_ms, uint32_t ms) { // e.g. right after getBatteryVoltage() released the pin _holdoff_until = now_ms + ms; _holdoff_active = true; - _armed = false; // anything pressed before/while held off must be released first + _armed = false; // anything pressed before/while held off must be released first _down = false; _fired = false; } @@ -38,11 +39,19 @@ class Button { _fired = false; return false; } - if (!_armed) return false; // still the same (or a held-off) press - if (!_down) { _down = true; _t_down = now_ms; return false; } - if (!_fired && (uint32_t)(now_ms - _t_down) >= DEBOUNCE_MS) { _fired = true; _armed = false; return true; } + if (!_armed) return false; // still the same (or a held-off) press + if (!_down) { + _down = true; + _t_down = now_ms; + return false; + } + if (!_fired && (uint32_t)(now_ms - _t_down) >= DEBOUNCE_MS) { + _fired = true; + _armed = false; + return true; + } return false; } }; -} // namespace asr650x +} // namespace asr650x diff --git a/src/helpers/asr650x/CubeCellRTC.h b/src/helpers/asr650x/CubeCellRTC.h index ba463e3e92..b59367b15b 100644 --- a/src/helpers/asr650x/CubeCellRTC.h +++ b/src/helpers/asr650x/CubeCellRTC.h @@ -1,10 +1,12 @@ #pragma once +#include "EpochClock.h" + #include #include -#include "EpochClock.h" class CubeCellRTC : public mesh::RTCClock { asr650x::EpochClock _clock; + public: uint32_t getCurrentTime() override { return _clock.now(millis()); } void setCurrentTime(uint32_t t) override { _clock.set(t, millis()); } diff --git a/src/helpers/asr650x/CubeCellRadio.cpp b/src/helpers/asr650x/CubeCellRadio.cpp index c898147f53..49bf4a0b6e 100644 --- a/src/helpers/asr650x/CubeCellRadio.cpp +++ b/src/helpers/asr650x/CubeCellRadio.cpp @@ -1,30 +1,40 @@ #include "CubeCellRadio.h" + +#include "LoRaWan_102.h" // LoraWan102 library headers only: ::Radio, RadioEvents_t, MODEM_LORA (LoRaWan_APP.h would also link the LoRa/AT/display libraries) #include "RadioParams.h" #include "TxGate.h" -#include "LoRaWan_102.h" // LoraWan102 library headers only: ::Radio, RadioEvents_t, MODEM_LORA (LoRaWan_APP.h would also link the LoRa/AT/display libraries) -extern "C" { // these board headers have no C++ guards; the functions are C -#include "gpio.h" // GpioSetInterrupt / GpioRemoveInterrupt / GpioRead -#include "sx126x-board.h" // extern SX126x (DIO1 pin) +extern "C" { // these board headers have no C++ guards; the functions are C +#include "gpio.h" // GpioSetInterrupt / GpioRemoveInterrupt / GpioRead +#include "sx126x-board.h" // extern SX126x (DIO1 pin) } #include extern "C" { -void RadioOnDioIrq(void); // radio.c: DIO1 handler (sets IrqFired and runs RadioIrqProcess) -unsigned char CyEnterCriticalSection(void); // CyLib.h +void RadioOnDioIrq(void); // radio.c: DIO1 handler (sets IrqFired and runs RadioIrqProcess) +unsigned char CyEnterCriticalSection(void); // CyLib.h void CyExitCriticalSection(unsigned char saved); } static RadioEvents_t g_events; -static CubeCellRadio* g_self = 0; +static CubeCellRadio *g_self = 0; -static void cb_rx_done(uint8_t* p, uint16_t n, int16_t rssi, int8_t snr) { if (g_self) g_self->handleRxDone(p, n, rssi, snr); } -static void cb_rx_err() { if (g_self) g_self->handleRxError(); } -static void cb_tx_done() { if (g_self) g_self->handleTxDone(false); } -static void cb_tx_timeout() { if (g_self) g_self->handleTxDone(true); } +static void cbRxDone(uint8_t *p, uint16_t n, int16_t rssi, int8_t snr) { + if (g_self) g_self->handleRxDone(p, n, rssi, snr); +} +static void cbRxErr() { + if (g_self) g_self->handleRxError(); +} +static void cbTxDone() { + if (g_self) g_self->handleTxDone(false); +} +static void cbTxTimeout() { + if (g_self) g_self->handleTxDone(true); +} -// While main-context code talks to the SX1262 over SPI the DIO1 handler must not run (it would issue its own SPI -// commands in the middle of ours). The handler is detached for the duration; on release it is re-attached and, if -// DIO1 is already high (an edge arrived meanwhile and was not latched), serviced right away from this context. +// While main-context code talks to the SX1262 over SPI the DIO1 handler must not run (it would issue its own +// SPI commands in the middle of ours). The handler is detached for the duration; on release it is re-attached +// and, if DIO1 is already high (an edge arrived meanwhile and was not latched), serviced right away from this +// context. struct RadioLock { RadioLock() { GpioRemoveInterrupt(&SX126x.DIO1); } ~RadioLock() { @@ -40,18 +50,19 @@ struct CriticalSection { }; CubeCellRadio::CubeCellRadio() - : _state(ST_IDLE), _rx_ready(false), _tx_done(false), _tx_timed_out(false), _rx_len(0), _last_rssi(0), _last_snr(0), - _freq(920.25f), _bw(62.5f), _sf(8), _cr(5), _pwr(14), _boost(true), _inited(false), _noise_floor(0), - _last_nf_sample(0), _n_recv(0), _n_sent(0), _n_err(0), _n_dropped(0), _n_txto(0), _n_blocked(0) {} + : _state(ST_IDLE), _rx_ready(false), _tx_done(false), _tx_timed_out(false), _rx_len(0), _last_rssi(0), + _last_snr(0), _freq(920.25f), _bw(62.5f), _sf(8), _cr(5), _pwr(14), _boost(true), _inited(false), + _noise_floor(0), _last_nf_sample(0), _n_recv(0), _n_sent(0), _n_err(0), _n_dropped(0), _n_txto(0), + _n_blocked(0) {} void CubeCellRadio::init() { if (_inited) return; g_self = this; - g_events.RxDone = cb_rx_done; - g_events.RxError = cb_rx_err; - g_events.RxTimeout = cb_rx_err; - g_events.TxDone = cb_tx_done; - g_events.TxTimeout = cb_tx_timeout; + g_events.RxDone = cbRxDone; + g_events.RxError = cbRxErr; + g_events.RxTimeout = cbRxErr; + g_events.TxDone = cbTxDone; + g_events.TxTimeout = cbTxTimeout; ::Radio.Init(&g_events); _inited = true; } @@ -61,7 +72,7 @@ uint32_t CubeCellRadio::random32() { return ::Radio.Random(); } -uint8_t asr650x_boot_pool[4]; // copy of the seed fold (watch builds print it to compare boots) +uint8_t asr650x_boot_pool[4]; // copy of the seed fold (watch builds print it to compare boots) uint32_t CubeCellRadio::getRngSeed() { uint32_t fold = 0; @@ -73,20 +84,23 @@ uint32_t CubeCellRadio::getRngSeed() { return fold; } -static uint32_t freq_hz(float mhz) { return ((uint32_t)(mhz * 1000.0f + 0.5f)) * 1000UL; } +static uint32_t freqHz(float mhz) { + return ((uint32_t)(mhz * 1000.0f + 0.5f)) * 1000UL; +} void CubeCellRadio::applyRx() { - ::Radio.SetPublicNetwork(false); // private sync word 0x1424 (MeshCore) - ::Radio.SetChannel(freq_hz(_freq)); - ::Radio.SetRxConfig(MODEM_LORA, asr650x::bw_index(_bw), asr650x::sf_clamp(_sf), asr650x::cr_index(_cr), 0, - asr650x::preamble_for_sf(asr650x::sf_clamp(_sf)), 0, false, 0, true, 0, 0, false, true); + ::Radio.SetPublicNetwork(false); // private sync word 0x1424 (MeshCore) + ::Radio.SetChannel(freqHz(_freq)); + ::Radio.SetRxConfig(MODEM_LORA, asr650x::bwIndex(_bw), asr650x::sfClamp(_sf), asr650x::crIndex(_cr), 0, + asr650x::preambleForSf(asr650x::sfClamp(_sf)), 0, false, 0, true, 0, 0, false, true); } void CubeCellRadio::applyTx(uint32_t timeout_ms) { ::Radio.SetPublicNetwork(false); - ::Radio.SetChannel(freq_hz(_freq)); - ::Radio.SetTxConfig(MODEM_LORA, _pwr, 0, asr650x::bw_index(_bw), asr650x::sf_clamp(_sf), asr650x::cr_index(_cr), - asr650x::preamble_for_sf(asr650x::sf_clamp(_sf)), false, true, 0, 0, false, timeout_ms); + ::Radio.SetChannel(freqHz(_freq)); + ::Radio.SetTxConfig(MODEM_LORA, _pwr, 0, asr650x::bwIndex(_bw), asr650x::sfClamp(_sf), + asr650x::crIndex(_cr), asr650x::preambleForSf(asr650x::sfClamp(_sf)), false, true, 0, 0, + false, timeout_ms); } void CubeCellRadio::startRx() { @@ -106,9 +120,11 @@ void CubeCellRadio::begin() { } void CubeCellRadio::loop() { - if (GpioRead(&SX126x.DIO1)) { RadioLock service; } // lost/late DIO1 edge: service it from here + if (GpioRead(&SX126x.DIO1)) { + RadioLock service; + } // lost/late DIO1 edge: service it from here if (_state == ST_IDLE && !_rx_ready) startRx(); - if (_state == ST_RX && !_rx_ready) { // slow noise-floor estimate while idle in RX + if (_state == ST_RX && !_rx_ready) { // slow noise-floor estimate while idle in RX uint32_t now = millis(); if (now - _last_nf_sample >= 100) { _last_nf_sample = now; @@ -117,45 +133,52 @@ void CubeCellRadio::loop() { RadioLock lock; r = ::Radio.Rssi(MODEM_LORA); } - if (_noise_floor == 0) _noise_floor = r; - else if (r < _noise_floor + 14) _noise_floor = (int16_t)((_noise_floor * 15 + r) / 16); + if (_noise_floor == 0) + _noise_floor = r; + else if (r < _noise_floor + 14) + _noise_floor = (int16_t)((_noise_floor * 15 + r) / 16); } } } -int CubeCellRadio::recvRaw(uint8_t* bytes, int sz) { +int CubeCellRadio::recvRaw(uint8_t *bytes, int sz) { int len = 0; if (_rx_ready) { - CriticalSection cs; // the callback (interrupt) may overwrite the buffer + CriticalSection cs; // the callback (interrupt) may overwrite the buffer len = _rx_len; if (len > sz) len = sz; if (len < 0) len = 0; memcpy(bytes, _rx_buf, len); _rx_ready = false; if (len > 0) _n_recv++; - _state = ST_IDLE; // restart RX below + _state = ST_IDLE; // restart RX below } if (_state == ST_IDLE) startRx(); return len; } uint32_t CubeCellRadio::getEstAirtimeFor(int len_bytes) { - return asr650x::airtime_ms(len_bytes, _bw, _sf, _cr, asr650x::preamble_for_sf(asr650x::sf_clamp(_sf))); + return asr650x::airtimeMs(len_bytes, _bw, _sf, _cr, asr650x::preambleForSf(asr650x::sfClamp(_sf))); } -float CubeCellRadio::packetScore(float snr, int packet_len) { return asr650x::packet_score(snr, _sf, packet_len); } +float CubeCellRadio::packetScore(float snr, int packet_len) { + return asr650x::packetScore(snr, _sf, packet_len); +} -bool CubeCellRadio::startSendRaw(const uint8_t* bytes, int len) { - if (!asr650x_tx_allowed()) { _n_blocked++; return false; } // never transmit as the unprovisioned placeholder +bool CubeCellRadio::startSendRaw(const uint8_t *bytes, int len) { + if (!asr650xTxAllowed()) { + _n_blocked++; + return false; + } // never transmit as the unprovisioned placeholder if (len <= 0 || len > 255) return false; - uint32_t timeout = getEstAirtimeFor(len) * 2 + 500; // the driver's timer must outlast the real air time + uint32_t timeout = getEstAirtimeFor(len) * 2 + 500; // the driver's timer must outlast the real air time _tx_done = false; _tx_timed_out = false; _state = ST_TX; { RadioLock lock; applyTx(timeout); - ::Radio.Send((uint8_t*)bytes, (uint8_t)len); + ::Radio.Send((uint8_t *)bytes, (uint8_t)len); } return true; } @@ -163,27 +186,34 @@ bool CubeCellRadio::startSendRaw(const uint8_t* bytes, int len) { bool CubeCellRadio::isSendComplete() { if (_state == ST_TX && _tx_done) { _tx_done = false; - if (!_tx_timed_out) _n_sent++; // a timed-out send is not a completed send + if (!_tx_timed_out) _n_sent++; // a timed-out send is not a completed send return true; } return false; } -void CubeCellRadio::onSendFinished() { _state = ST_IDLE; } // loop()/recvRaw() restart RX +void CubeCellRadio::onSendFinished() { + _state = ST_IDLE; +} // loop()/recvRaw() restart RX void CubeCellRadio::setParams(float freq_mhz, float bw_khz, uint8_t sf, uint8_t cr) { _freq = freq_mhz; - _bw = asr650x::bw_value(asr650x::bw_index(bw_khz)); // the bandwidth the chip really uses - _sf = asr650x::sf_clamp(sf); - _cr = (uint8_t)(asr650x::cr_index(cr) + 4); - if (_inited && _state == ST_RX) _state = ST_IDLE; // re-apply on the next loop() + _bw = asr650x::bwValue(asr650x::bwIndex(bw_khz)); // the bandwidth the chip really uses + _sf = asr650x::sfClamp(sf); + _cr = (uint8_t)(asr650x::crIndex(cr) + 4); + if (_inited && _state == ST_RX) _state = ST_IDLE; // re-apply on the next loop() } -void CubeCellRadio::setTxPower(int8_t dbm) { _pwr = dbm < -9 ? -9 : (dbm > 22 ? 22 : dbm); } +void CubeCellRadio::setTxPower(int8_t dbm) { + _pwr = dbm < -9 ? -9 : (dbm > 22 ? 22 : dbm); +} -void CubeCellRadio::handleRxDone(uint8_t* payload, uint16_t size, int16_t rssi, int8_t snr) { - if (_state == ST_TX) return; // not expected while transmitting - if (_rx_ready) { _n_dropped++; return; } // previous packet not consumed yet: drop the new one, counted +void CubeCellRadio::handleRxDone(uint8_t *payload, uint16_t size, int16_t rssi, int8_t snr) { + if (_state == ST_TX) return; // not expected while transmitting + if (_rx_ready) { + _n_dropped++; + return; + } // previous packet not consumed yet: drop the new one, counted if (size > sizeof(_rx_buf)) size = sizeof(_rx_buf); memcpy(_rx_buf, payload, size); _rx_len = size; @@ -200,6 +230,9 @@ void CubeCellRadio::handleRxError() { } void CubeCellRadio::handleTxDone(bool timeout) { - if (timeout) { _n_txto++; _tx_timed_out = true; } - _tx_done = true; // a timed-out send still ends the TX state + if (timeout) { + _n_txto++; + _tx_timed_out = true; + } + _tx_done = true; // a timed-out send still ends the TX state } diff --git a/src/helpers/asr650x/CubeCellRadio.h b/src/helpers/asr650x/CubeCellRadio.h index 1d54e9ca8b..4e5a0011c8 100644 --- a/src/helpers/asr650x/CubeCellRadio.h +++ b/src/helpers/asr650x/CubeCellRadio.h @@ -1,12 +1,13 @@ #pragma once #include -// SX1262 driver for the ASR6502 on top of the Heltec radio API (::Radio.*). The Heltec header is only included in -// CubeCellRadio.cpp. State machine: RX (continuous) <-> TX; every RxDone/RxError/TxDone/TxTimeout returns to RX. +// SX1262 driver for the ASR6502 on top of the Heltec radio API (::Radio.*). The Heltec header is only +// included in CubeCellRadio.cpp. State machine: RX (continuous) <-> TX; every RxDone/RxError/TxDone/TxTimeout +// returns to RX. // -// Threading: the Heltec driver runs RxDone/RxError/TxDone (DIO1 interrupt) and TxTimeout (timer interrupt) INSIDE -// interrupts. Fields written by the callbacks are volatile and are read/copied inside a critical section; every -// main-context ::Radio.* call runs with the DIO1 interrupt detached (RadioLock in the .cpp). +// Threading: the Heltec driver runs RxDone/RxError/TxDone (DIO1 interrupt) and TxTimeout (timer interrupt) +// INSIDE interrupts. Fields written by the callbacks are volatile and are read/copied inside a critical +// section; every main-context ::Radio.* call runs with the DIO1 interrupt detached (RadioLock in the .cpp). class CubeCellRadio : public mesh::Radio { enum { ST_IDLE = 0, ST_RX = 1, ST_TX = 2 }; volatile uint8_t _state; @@ -31,15 +32,15 @@ class CubeCellRadio : public mesh::Radio { public: CubeCellRadio(); - void init(); // ::Radio.Init (idempotent) - uint32_t random32(); // ::Radio.Random(); only before begin() - uint32_t getRngSeed(); // fold of 8 Radio.Random() values (wideband RSSI noise); before begin() + void init(); // ::Radio.Init (idempotent) + uint32_t random32(); // ::Radio.Random(); only before begin() + uint32_t getRngSeed(); // fold of 8 Radio.Random() values (wideband RSSI noise); before begin() void begin() override; void loop() override; - int recvRaw(uint8_t* bytes, int sz) override; + int recvRaw(uint8_t *bytes, int sz) override; uint32_t getEstAirtimeFor(int len_bytes) override; float packetScore(float snr, int packet_len) override; - bool startSendRaw(const uint8_t* bytes, int len) override; + bool startSendRaw(const uint8_t *bytes, int len) override; bool isSendComplete() override; void onSendFinished() override; bool isInRecvMode() const override { return _state == ST_RX; } @@ -49,15 +50,20 @@ class CubeCellRadio : public mesh::Radio { void setParams(float freq_mhz, float bw_khz, uint8_t sf, uint8_t cr); void setTxPower(int8_t dbm); - bool setRxBoostedGainMode(bool en) { _boost = en; return false; } // not supported: Radio.Rx() is used, not RxBoosted + bool setRxBoostedGainMode(bool en) { + _boost = en; + return false; + } // not supported: Radio.Rx() is used, not RxBoosted bool getRxBoostedGainMode() const { return _boost; } uint32_t getPacketsRecv() const { return _n_recv; } uint32_t getPacketsSent() const { return _n_sent; } - uint32_t getPacketsRecvErrors() const { return _n_err + _n_dropped; } // CRC/header errors + packets lost to a busy buffer + uint32_t getPacketsRecvErrors() const { + return _n_err + _n_dropped; + } // CRC/header errors + packets lost to a busy buffer uint32_t getPacketsBlocked() const { return _n_blocked; } // called from the static Heltec callbacks (interrupt context) - void handleRxDone(uint8_t* payload, uint16_t size, int16_t rssi, int8_t snr); + void handleRxDone(uint8_t *payload, uint16_t size, int16_t rssi, int8_t snr); void handleRxError(); void handleTxDone(bool timeout); }; diff --git a/src/helpers/asr650x/EpochClock.h b/src/helpers/asr650x/EpochClock.h index b16849d349..2472e07b15 100644 --- a/src/helpers/asr650x/EpochClock.h +++ b/src/helpers/asr650x/EpochClock.h @@ -3,20 +3,20 @@ namespace asr650x { -// Wall clock = epoch + accumulated monotonic milliseconds. Lost on reset by design (spec §5). +// Wall clock = epoch + accumulated monotonic milliseconds. Lost on reset (the app or GPS sets it again). // now() must be called at least once every ~49 days so a 32-bit millis() wrap is seen once. class EpochClock { uint32_t _epoch; uint32_t _last_ms; - uint64_t _rem_ms; // sub-second remainder, < 1000 after each now() - bool _synced; // true once set() was called (by the app or by GPS) + uint64_t _rem_ms; // sub-second remainder, < 1000 after each now() + bool _synced; // true once set() was called (by the app or by GPS) public: - explicit EpochClock(uint32_t epoch_at_boot = 1735689600u) // 2025-01-01 until the app sets time - : _epoch(epoch_at_boot), _last_ms(0), _rem_ms(0), _synced(false) {} + explicit EpochClock(uint32_t epoch_at_boot = 1735689600u) // 2025-01-01 until the app sets time + : _epoch(epoch_at_boot), _last_ms(0), _rem_ms(0), _synced(false) {} uint32_t now(uint32_t ms) { - uint32_t delta = ms - _last_ms; // unsigned arithmetic handles one wrap + uint32_t delta = ms - _last_ms; // unsigned arithmetic handles one wrap _last_ms = ms; _rem_ms += delta; _epoch += (uint32_t)(_rem_ms / 1000); @@ -34,4 +34,4 @@ class EpochClock { bool synced() const { return _synced; } }; -} // namespace asr650x +} // namespace asr650x diff --git a/src/helpers/asr650x/FlashRecord.h b/src/helpers/asr650x/FlashRecord.h index 1cb2387deb..208254e0ba 100644 --- a/src/helpers/asr650x/FlashRecord.h +++ b/src/helpers/asr650x/FlashRecord.h @@ -4,21 +4,21 @@ #include // On-flash record (SFLASH user row). Explicit little-endian layout: -// 0..3 "KTM1" | 4 version | 5 flags(bit0 identity, bit1 prefs) | 6..7 0 -// 8..71 prv | 72..103 pub | 104..135 name | 136..139 freq | 140..143 bw -// 144 sf | 145 cr | 146 tx_power | 147 gps_off | 148..151 seq | 152..155 gps_interval +// 0..3 "ASR1" ("KTM1" in records from early builds, still accepted) | 4 version | 5 flags(bit0 identity, +// bit1 prefs) | 6..7 0 8..71 prv | 72..103 pub | 104..135 name | 136..139 freq | 140..143 bw 144 sf | 145 +// cr | 146 tx_power | 147 gps_off | 148..151 seq | 152..155 gps_interval // version 1 (160 bytes): 156..159 CRC-32 of bytes 0..155 // version 2 (192 bytes): 156..159 airtime_factor | 160..163 rx_delay_base | 164..167 tx_delay_factor // | 168..171 direct_tx_delay_factor | 172 multi_acks | 173 manual_add_contacts | 174 telemetry modes // | 175 advert_loc_policy | 176 autoadd_config | 177 autoadd_max_hops | 178 path_hash_mode | 179 tz_offset -// | 180 cad_enabled | 181 interference_threshold | 182 agc_reset_interval | 183 rx_boosted_gain | 184..187 0 -// | 188..191 CRC-32 of bytes 0..187 +// | 180 cad_enabled | 181 interference_threshold | 182 agc_reset_interval | 183 rx_boosted_gain | 184..187 +// 0 | 188..191 CRC-32 of bytes 0..187 namespace asr650x { -const size_t PERSIST_SIZE = 192; // version 2; a version 1 record uses the first 160 bytes +const size_t PERSIST_SIZE = 192; // version 2; a version 1 record uses the first 160 bytes struct PersistState { - uint32_t seq; // record generation, newest valid row wins (see FlashRecordStore.h) + uint32_t seq; // record generation, newest valid row wins (see FlashRecordStore.h) bool has_identity; uint8_t prv[64]; uint8_t pub[32]; @@ -27,9 +27,9 @@ struct PersistState { float freq, bw; uint8_t sf, cr; int8_t tx_power_dbm; - bool gps_off; // 1 = the user switched GPS off (0 = default: on, also for older records) - uint32_t gps_interval; // seconds between GPS refreshes, 0 = only at boot / on request - bool has_ext; // version 2 settings below are present (false for a version 1 record) + bool gps_off; // 1 = the user switched GPS off (0 = default: on, also for older records) + uint32_t gps_interval; // seconds between GPS refreshes, 0 = only at boot / on request + bool has_ext; // version 2 settings below are present (false for a version 1 record) float airtime_factor, rx_delay_base, tx_delay_factor, direct_tx_delay_factor; uint8_t multi_acks, manual_add_contacts, telemetry_modes, advert_loc_policy; uint8_t autoadd_config, autoadd_max_hops, path_hash_mode; @@ -37,51 +37,69 @@ struct PersistState { uint8_t cad_enabled, interference_threshold, agc_reset_interval, rx_boosted_gain; }; -inline uint32_t crc32(const uint8_t* d, size_t n) { +inline uint32_t crc32(const uint8_t *d, size_t n) { uint32_t c = 0xFFFFFFFFu; for (size_t i = 0; i < n; i++) { c ^= d[i]; - for (int k = 0; k < 8; k++) c = (c >> 1) ^ (0xEDB88320u & (0u - (c & 1u))); + for (int k = 0; k < 8; k++) + c = (c >> 1) ^ (0xEDB88320u & (0u - (c & 1u))); } return ~c; } -inline void persist_encode(const PersistState& s, uint8_t out[PERSIST_SIZE]) { +inline void persistEncode(const PersistState &s, uint8_t out[PERSIST_SIZE]) { memset(out, 0, PERSIST_SIZE); - memcpy(out, "KTM1", 4); + memcpy(out, "ASR1", 4); out[4] = 2; out[5] = (s.has_identity ? 1 : 0) | (s.has_prefs ? 2 : 0); - if (s.has_identity) { memcpy(out + 8, s.prv, 64); memcpy(out + 72, s.pub, 32); } + if (s.has_identity) { + memcpy(out + 8, s.prv, 64); + memcpy(out + 72, s.pub, 32); + } if (s.has_prefs) { memcpy(out + 104, s.node_name, 32); memcpy(out + 136, &s.freq, 4); memcpy(out + 140, &s.bw, 4); - out[144] = s.sf; out[145] = s.cr; out[146] = (uint8_t)s.tx_power_dbm; + out[144] = s.sf; + out[145] = s.cr; + out[146] = (uint8_t)s.tx_power_dbm; } out[147] = s.gps_off ? 1 : 0; - for (int i = 0; i < 4; i++) out[148 + i] = (uint8_t)(s.seq >> (8 * i)); - for (int i = 0; i < 4; i++) out[152 + i] = (uint8_t)(s.gps_interval >> (8 * i)); + for (int i = 0; i < 4; i++) + out[148 + i] = (uint8_t)(s.seq >> (8 * i)); + for (int i = 0; i < 4; i++) + out[152 + i] = (uint8_t)(s.gps_interval >> (8 * i)); if (s.has_ext) { memcpy(out + 156, &s.airtime_factor, 4); memcpy(out + 160, &s.rx_delay_base, 4); memcpy(out + 164, &s.tx_delay_factor, 4); memcpy(out + 168, &s.direct_tx_delay_factor, 4); - out[172] = s.multi_acks; out[173] = s.manual_add_contacts; out[174] = s.telemetry_modes; - out[175] = s.advert_loc_policy; out[176] = s.autoadd_config; out[177] = s.autoadd_max_hops; - out[178] = s.path_hash_mode; out[179] = (uint8_t)s.tz_offset; out[180] = s.cad_enabled; - out[181] = s.interference_threshold; out[182] = s.agc_reset_interval; out[183] = s.rx_boosted_gain; - out[184] = 1; // marks the settings block as present + out[172] = s.multi_acks; + out[173] = s.manual_add_contacts; + out[174] = s.telemetry_modes; + out[175] = s.advert_loc_policy; + out[176] = s.autoadd_config; + out[177] = s.autoadd_max_hops; + out[178] = s.path_hash_mode; + out[179] = (uint8_t)s.tz_offset; + out[180] = s.cad_enabled; + out[181] = s.interference_threshold; + out[182] = s.agc_reset_interval; + out[183] = s.rx_boosted_gain; + out[184] = 1; // marks the settings block as present } uint32_t crc = crc32(out, 188); - for (int i = 0; i < 4; i++) out[188 + i] = (uint8_t)(crc >> (8 * i)); + for (int i = 0; i < 4; i++) + out[188 + i] = (uint8_t)(crc >> (8 * i)); } -inline bool persist_decode(const uint8_t in[PERSIST_SIZE], PersistState& s) { +inline bool persistDecode(const uint8_t in[PERSIST_SIZE], PersistState &s) { memset(&s, 0, sizeof(s)); - if (memcmp(in, "KTM1", 4) != 0 || (in[4] != 1 && in[4] != 2)) return false; + if ((memcmp(in, "ASR1", 4) != 0 && memcmp(in, "KTM1", 4) != 0) || (in[4] != 1 && in[4] != 2)) return false; size_t crc_at = in[4] == 1 ? 156 : 188; uint32_t want = 0; - for (int i = 0; i < 4; i++) want |= (uint32_t)in[crc_at + i] << (8 * i); + for (int i = 0; i < 4; i++) + want |= (uint32_t)in[crc_at + i] << (8 * i); if (crc32(in, crc_at) != want) return false; if (in[4] == 2 && in[184] == 1) { s.has_ext = true; @@ -89,24 +107,40 @@ inline bool persist_decode(const uint8_t in[PERSIST_SIZE], PersistState& s) { memcpy(&s.rx_delay_base, in + 160, 4); memcpy(&s.tx_delay_factor, in + 164, 4); memcpy(&s.direct_tx_delay_factor, in + 168, 4); - s.multi_acks = in[172]; s.manual_add_contacts = in[173]; s.telemetry_modes = in[174]; - s.advert_loc_policy = in[175]; s.autoadd_config = in[176]; s.autoadd_max_hops = in[177]; - s.path_hash_mode = in[178]; s.tz_offset = (int8_t)in[179]; s.cad_enabled = in[180]; - s.interference_threshold = in[181]; s.agc_reset_interval = in[182]; s.rx_boosted_gain = in[183]; + s.multi_acks = in[172]; + s.manual_add_contacts = in[173]; + s.telemetry_modes = in[174]; + s.advert_loc_policy = in[175]; + s.autoadd_config = in[176]; + s.autoadd_max_hops = in[177]; + s.path_hash_mode = in[178]; + s.tz_offset = (int8_t)in[179]; + s.cad_enabled = in[180]; + s.interference_threshold = in[181]; + s.agc_reset_interval = in[182]; + s.rx_boosted_gain = in[183]; } s.gps_off = in[147] != 0; - for (int i = 0; i < 4; i++) s.seq |= (uint32_t)in[148 + i] << (8 * i); - for (int i = 0; i < 4; i++) s.gps_interval |= (uint32_t)in[152 + i] << (8 * i); + for (int i = 0; i < 4; i++) + s.seq |= (uint32_t)in[148 + i] << (8 * i); + for (int i = 0; i < 4; i++) + s.gps_interval |= (uint32_t)in[152 + i] << (8 * i); s.has_identity = (in[5] & 1) != 0; s.has_prefs = (in[5] & 2) != 0; - if (s.has_identity) { memcpy(s.prv, in + 8, 64); memcpy(s.pub, in + 72, 32); } + if (s.has_identity) { + memcpy(s.prv, in + 8, 64); + memcpy(s.pub, in + 72, 32); + } if (s.has_prefs) { - memcpy(s.node_name, in + 104, 32); s.node_name[31] = 0; + memcpy(s.node_name, in + 104, 32); + s.node_name[31] = 0; memcpy(&s.freq, in + 136, 4); memcpy(&s.bw, in + 140, 4); - s.sf = in[144]; s.cr = in[145]; s.tx_power_dbm = (int8_t)in[146]; + s.sf = in[144]; + s.cr = in[145]; + s.tx_power_dbm = (int8_t)in[146]; } return true; } -} // namespace asr650x +} // namespace asr650x diff --git a/src/helpers/asr650x/FlashRecordStore.h b/src/helpers/asr650x/FlashRecordStore.h index 301799f22c..0991e91b15 100644 --- a/src/helpers/asr650x/FlashRecordStore.h +++ b/src/helpers/asr650x/FlashRecordStore.h @@ -4,7 +4,7 @@ // Two-row A/B store for the 160-byte FlashRecord. Each commit writes the OTHER row with seq+1, so a // power loss while writing leaves the previous record intact (the SFLASH row erase+program is not atomic). // commit() changes the in-RAM state only after the backend confirmed the write: a rejected change can -// never leak into a later commit (review I1). Rows are 256 bytes apart (one SFLASH user row each). +// never leak into a later commit. Rows are 256 bytes apart (one SFLASH user row each). // // Backend concept: // void read(size_t offset, uint8_t* buf, size_t n); @@ -13,23 +13,24 @@ namespace asr650x { const size_t ROW_STRIDE = 256; -inline bool seq_newer(uint32_t a, uint32_t b) { return (int32_t)(a - b) > 0; } +inline bool seqNewer(uint32_t a, uint32_t b) { + return (int32_t)(a - b) > 0; +} -template -class PersistStore { - Backend& _be; +template class PersistStore { + Backend &_be; PersistState _state; - uint8_t _last[PERSIST_SIZE]; // bytes of the newest valid record (what is in flash now) + uint8_t _last[PERSIST_SIZE]; // bytes of the newest valid record (what is in flash now) bool _have_last; - int _next_row; // row the next commit writes + int _next_row; // row the next commit writes public: - explicit PersistStore(Backend& be) : _be(be), _have_last(false), _next_row(0) { + explicit PersistStore(Backend &be) : _be(be), _have_last(false), _next_row(0) { memset(&_state, 0, sizeof(_state)); memset(_last, 0, sizeof(_last)); } - const PersistState& state() const { return _state; } + const PersistState &state() const { return _state; } void begin() { uint8_t buf[2][PERSIST_SIZE]; @@ -37,12 +38,15 @@ class PersistStore { bool ok[2]; for (int r = 0; r < 2; r++) { _be.read(r * ROW_STRIDE, buf[r], PERSIST_SIZE); - ok[r] = persist_decode(buf[r], st[r]); + ok[r] = persistDecode(buf[r], st[r]); } int pick = -1; - if (ok[0] && ok[1]) pick = seq_newer(st[1].seq, st[0].seq) ? 1 : 0; - else if (ok[0]) pick = 0; - else if (ok[1]) pick = 1; + if (ok[0] && ok[1]) + pick = seqNewer(st[1].seq, st[0].seq) ? 1 : 0; + else if (ok[0]) + pick = 0; + else if (ok[1]) + pick = 1; if (pick < 0) { memset(&_state, 0, sizeof(_state)); _have_last = false; @@ -55,17 +59,17 @@ class PersistStore { _next_row = pick ^ 1; } - bool commit(const PersistState& candidate) { + bool commit(const PersistState &candidate) { PersistState c = candidate; uint8_t buf[PERSIST_SIZE]; - if (_have_last) { // unchanged content -> no flash write + if (_have_last) { // unchanged content -> no flash write c.seq = _state.seq; - persist_encode(c, buf); + persistEncode(c, buf); if (memcmp(buf, _last, PERSIST_SIZE) == 0) return true; } c.seq = _state.seq + 1; - persist_encode(c, buf); - if (!_be.write(_next_row * ROW_STRIDE, buf, PERSIST_SIZE)) return false; // state untouched on failure + persistEncode(c, buf); + if (!_be.write(_next_row * ROW_STRIDE, buf, PERSIST_SIZE)) return false; // state untouched on failure _state = c; memcpy(_last, buf, PERSIST_SIZE); _have_last = true; @@ -74,4 +78,4 @@ class PersistStore { } }; -} // namespace asr650x +} // namespace asr650x diff --git a/src/helpers/asr650x/Font5x7.h b/src/helpers/asr650x/Font5x7.h index 288319cf66..31fb7fd92c 100644 --- a/src/helpers/asr650x/Font5x7.h +++ b/src/helpers/asr650x/Font5x7.h @@ -5,36 +5,44 @@ namespace asr650x { // 5x7 ASCII font (0x20..0x7E), 5 column bytes per character, bit 0 = top pixel row; descenders use bit 7. static const uint8_t FONT5X7[95][5] = { - {0x00,0x00,0x00,0x00,0x00},{0x00,0x00,0x5F,0x00,0x00},{0x00,0x07,0x00,0x07,0x00},{0x14,0x7F,0x14,0x7F,0x14}, - {0x24,0x2A,0x7F,0x2A,0x12},{0x23,0x13,0x08,0x64,0x62},{0x36,0x49,0x56,0x20,0x50},{0x00,0x08,0x07,0x03,0x00}, - {0x00,0x1C,0x22,0x41,0x00},{0x00,0x41,0x22,0x1C,0x00},{0x2A,0x1C,0x7F,0x1C,0x2A},{0x08,0x08,0x3E,0x08,0x08}, - {0x00,0x80,0x70,0x30,0x00},{0x08,0x08,0x08,0x08,0x08},{0x00,0x00,0x60,0x60,0x00},{0x20,0x10,0x08,0x04,0x02}, - {0x3E,0x51,0x49,0x45,0x3E},{0x00,0x42,0x7F,0x40,0x00},{0x72,0x49,0x49,0x49,0x46},{0x21,0x41,0x49,0x4D,0x33}, - {0x18,0x14,0x12,0x7F,0x10},{0x27,0x45,0x45,0x45,0x39},{0x3C,0x4A,0x49,0x49,0x31},{0x41,0x21,0x11,0x09,0x07}, - {0x36,0x49,0x49,0x49,0x36},{0x46,0x49,0x49,0x29,0x1E},{0x00,0x00,0x14,0x00,0x00},{0x00,0x40,0x34,0x00,0x00}, - {0x00,0x08,0x14,0x22,0x41},{0x14,0x14,0x14,0x14,0x14},{0x00,0x41,0x22,0x14,0x08},{0x02,0x01,0x59,0x09,0x06}, - {0x3E,0x41,0x5D,0x59,0x4E},{0x7C,0x12,0x11,0x12,0x7C},{0x7F,0x49,0x49,0x49,0x36},{0x3E,0x41,0x41,0x41,0x22}, - {0x7F,0x41,0x41,0x41,0x3E},{0x7F,0x49,0x49,0x49,0x41},{0x7F,0x09,0x09,0x09,0x01},{0x3E,0x41,0x41,0x51,0x73}, - {0x7F,0x08,0x08,0x08,0x7F},{0x00,0x41,0x7F,0x41,0x00},{0x20,0x40,0x41,0x3F,0x01},{0x7F,0x08,0x14,0x22,0x41}, - {0x7F,0x40,0x40,0x40,0x40},{0x7F,0x02,0x1C,0x02,0x7F},{0x7F,0x04,0x08,0x10,0x7F},{0x3E,0x41,0x41,0x41,0x3E}, - {0x7F,0x09,0x09,0x09,0x06},{0x3E,0x41,0x51,0x21,0x5E},{0x7F,0x09,0x19,0x29,0x46},{0x26,0x49,0x49,0x49,0x32}, - {0x03,0x01,0x7F,0x01,0x03},{0x3F,0x40,0x40,0x40,0x3F},{0x1F,0x20,0x40,0x20,0x1F},{0x3F,0x40,0x38,0x40,0x3F}, - {0x63,0x14,0x08,0x14,0x63},{0x03,0x04,0x78,0x04,0x03},{0x61,0x59,0x49,0x4D,0x43},{0x00,0x7F,0x41,0x41,0x41}, - {0x02,0x04,0x08,0x10,0x20},{0x00,0x41,0x41,0x41,0x7F},{0x04,0x02,0x01,0x02,0x04},{0x40,0x40,0x40,0x40,0x40}, - {0x00,0x03,0x07,0x08,0x00},{0x20,0x54,0x54,0x78,0x40},{0x7F,0x28,0x44,0x44,0x38},{0x38,0x44,0x44,0x44,0x28}, - {0x38,0x44,0x44,0x28,0x7F},{0x38,0x54,0x54,0x54,0x18},{0x00,0x08,0x7E,0x09,0x02},{0x18,0xA4,0xA4,0x9C,0x78}, - {0x7F,0x08,0x04,0x04,0x78},{0x00,0x44,0x7D,0x40,0x00},{0x20,0x40,0x40,0x3D,0x00},{0x7F,0x10,0x28,0x44,0x00}, - {0x00,0x41,0x7F,0x40,0x00},{0x7C,0x04,0x78,0x04,0x78},{0x7C,0x08,0x04,0x04,0x78},{0x38,0x44,0x44,0x44,0x38}, - {0xFC,0x18,0x24,0x24,0x18},{0x18,0x24,0x24,0x18,0xFC},{0x7C,0x08,0x04,0x04,0x08},{0x48,0x54,0x54,0x54,0x24}, - {0x04,0x04,0x3F,0x44,0x24},{0x3C,0x40,0x40,0x20,0x7C},{0x1C,0x20,0x40,0x20,0x1C},{0x3C,0x40,0x30,0x40,0x3C}, - {0x44,0x28,0x10,0x28,0x44},{0x4C,0x90,0x90,0x90,0x7C},{0x44,0x64,0x54,0x4C,0x44},{0x00,0x08,0x36,0x41,0x00}, - {0x00,0x00,0x77,0x00,0x00},{0x00,0x41,0x36,0x08,0x00},{0x02,0x01,0x02,0x04,0x02} + { 0x00, 0x00, 0x00, 0x00, 0x00 }, { 0x00, 0x00, 0x5F, 0x00, 0x00 }, { 0x00, 0x07, 0x00, 0x07, 0x00 }, + { 0x14, 0x7F, 0x14, 0x7F, 0x14 }, { 0x24, 0x2A, 0x7F, 0x2A, 0x12 }, { 0x23, 0x13, 0x08, 0x64, 0x62 }, + { 0x36, 0x49, 0x56, 0x20, 0x50 }, { 0x00, 0x08, 0x07, 0x03, 0x00 }, { 0x00, 0x1C, 0x22, 0x41, 0x00 }, + { 0x00, 0x41, 0x22, 0x1C, 0x00 }, { 0x2A, 0x1C, 0x7F, 0x1C, 0x2A }, { 0x08, 0x08, 0x3E, 0x08, 0x08 }, + { 0x00, 0x80, 0x70, 0x30, 0x00 }, { 0x08, 0x08, 0x08, 0x08, 0x08 }, { 0x00, 0x00, 0x60, 0x60, 0x00 }, + { 0x20, 0x10, 0x08, 0x04, 0x02 }, { 0x3E, 0x51, 0x49, 0x45, 0x3E }, { 0x00, 0x42, 0x7F, 0x40, 0x00 }, + { 0x72, 0x49, 0x49, 0x49, 0x46 }, { 0x21, 0x41, 0x49, 0x4D, 0x33 }, { 0x18, 0x14, 0x12, 0x7F, 0x10 }, + { 0x27, 0x45, 0x45, 0x45, 0x39 }, { 0x3C, 0x4A, 0x49, 0x49, 0x31 }, { 0x41, 0x21, 0x11, 0x09, 0x07 }, + { 0x36, 0x49, 0x49, 0x49, 0x36 }, { 0x46, 0x49, 0x49, 0x29, 0x1E }, { 0x00, 0x00, 0x14, 0x00, 0x00 }, + { 0x00, 0x40, 0x34, 0x00, 0x00 }, { 0x00, 0x08, 0x14, 0x22, 0x41 }, { 0x14, 0x14, 0x14, 0x14, 0x14 }, + { 0x00, 0x41, 0x22, 0x14, 0x08 }, { 0x02, 0x01, 0x59, 0x09, 0x06 }, { 0x3E, 0x41, 0x5D, 0x59, 0x4E }, + { 0x7C, 0x12, 0x11, 0x12, 0x7C }, { 0x7F, 0x49, 0x49, 0x49, 0x36 }, { 0x3E, 0x41, 0x41, 0x41, 0x22 }, + { 0x7F, 0x41, 0x41, 0x41, 0x3E }, { 0x7F, 0x49, 0x49, 0x49, 0x41 }, { 0x7F, 0x09, 0x09, 0x09, 0x01 }, + { 0x3E, 0x41, 0x41, 0x51, 0x73 }, { 0x7F, 0x08, 0x08, 0x08, 0x7F }, { 0x00, 0x41, 0x7F, 0x41, 0x00 }, + { 0x20, 0x40, 0x41, 0x3F, 0x01 }, { 0x7F, 0x08, 0x14, 0x22, 0x41 }, { 0x7F, 0x40, 0x40, 0x40, 0x40 }, + { 0x7F, 0x02, 0x1C, 0x02, 0x7F }, { 0x7F, 0x04, 0x08, 0x10, 0x7F }, { 0x3E, 0x41, 0x41, 0x41, 0x3E }, + { 0x7F, 0x09, 0x09, 0x09, 0x06 }, { 0x3E, 0x41, 0x51, 0x21, 0x5E }, { 0x7F, 0x09, 0x19, 0x29, 0x46 }, + { 0x26, 0x49, 0x49, 0x49, 0x32 }, { 0x03, 0x01, 0x7F, 0x01, 0x03 }, { 0x3F, 0x40, 0x40, 0x40, 0x3F }, + { 0x1F, 0x20, 0x40, 0x20, 0x1F }, { 0x3F, 0x40, 0x38, 0x40, 0x3F }, { 0x63, 0x14, 0x08, 0x14, 0x63 }, + { 0x03, 0x04, 0x78, 0x04, 0x03 }, { 0x61, 0x59, 0x49, 0x4D, 0x43 }, { 0x00, 0x7F, 0x41, 0x41, 0x41 }, + { 0x02, 0x04, 0x08, 0x10, 0x20 }, { 0x00, 0x41, 0x41, 0x41, 0x7F }, { 0x04, 0x02, 0x01, 0x02, 0x04 }, + { 0x40, 0x40, 0x40, 0x40, 0x40 }, { 0x00, 0x03, 0x07, 0x08, 0x00 }, { 0x20, 0x54, 0x54, 0x78, 0x40 }, + { 0x7F, 0x28, 0x44, 0x44, 0x38 }, { 0x38, 0x44, 0x44, 0x44, 0x28 }, { 0x38, 0x44, 0x44, 0x28, 0x7F }, + { 0x38, 0x54, 0x54, 0x54, 0x18 }, { 0x00, 0x08, 0x7E, 0x09, 0x02 }, { 0x18, 0xA4, 0xA4, 0x9C, 0x78 }, + { 0x7F, 0x08, 0x04, 0x04, 0x78 }, { 0x00, 0x44, 0x7D, 0x40, 0x00 }, { 0x20, 0x40, 0x40, 0x3D, 0x00 }, + { 0x7F, 0x10, 0x28, 0x44, 0x00 }, { 0x00, 0x41, 0x7F, 0x40, 0x00 }, { 0x7C, 0x04, 0x78, 0x04, 0x78 }, + { 0x7C, 0x08, 0x04, 0x04, 0x78 }, { 0x38, 0x44, 0x44, 0x44, 0x38 }, { 0xFC, 0x18, 0x24, 0x24, 0x18 }, + { 0x18, 0x24, 0x24, 0x18, 0xFC }, { 0x7C, 0x08, 0x04, 0x04, 0x08 }, { 0x48, 0x54, 0x54, 0x54, 0x24 }, + { 0x04, 0x04, 0x3F, 0x44, 0x24 }, { 0x3C, 0x40, 0x40, 0x20, 0x7C }, { 0x1C, 0x20, 0x40, 0x20, 0x1C }, + { 0x3C, 0x40, 0x30, 0x40, 0x3C }, { 0x44, 0x28, 0x10, 0x28, 0x44 }, { 0x4C, 0x90, 0x90, 0x90, 0x7C }, + { 0x44, 0x64, 0x54, 0x4C, 0x44 }, { 0x00, 0x08, 0x36, 0x41, 0x00 }, { 0x00, 0x00, 0x77, 0x00, 0x00 }, + { 0x00, 0x41, 0x36, 0x08, 0x00 }, { 0x02, 0x01, 0x02, 0x04, 0x02 } }; -inline const uint8_t* font5x7_glyph(char c) { +inline const uint8_t *font5x7Glyph(char c) { unsigned char u = (unsigned char)c; if (u < 0x20 || u > 0x7E) u = '?'; return FONT5X7[u - 0x20]; } -} // namespace asr650x +} // namespace asr650x diff --git a/src/helpers/asr650x/GpsPolicy.h b/src/helpers/asr650x/GpsPolicy.h index 802d3fd004..54cf960f8f 100644 --- a/src/helpers/asr650x/GpsPolicy.h +++ b/src/helpers/asr650x/GpsPolicy.h @@ -22,21 +22,30 @@ class GpsPolicy { } public: - GpsPolicy() : _enabled(false), _interval(0), _timeout(300), _state(GPS_OFF), _t_start(0), _t_done(0), - _timed_out(false), _fixes(0) {} + GpsPolicy() + : _enabled(false), _interval(0), _timeout(300), _state(GPS_OFF), _t_start(0), _t_done(0), + _timed_out(false), _fixes(0) {} void configure(bool enabled, uint32_t interval_s, uint32_t fix_timeout_s) { _interval = interval_s; _timeout = fix_timeout_s; - if (!enabled) { _enabled = false; _state = GPS_OFF; return; } + if (!enabled) { + _enabled = false; + _state = GPS_OFF; + return; + } _enabled = true; } void begin(uint32_t now_s) { - if (_enabled) { _state = GPS_WARMING; _t_start = now_s; _timed_out = false; } + if (_enabled) { + _state = GPS_WARMING; + _t_start = now_s; + _timed_out = false; + } } - void request(uint32_t now_s) { // the app asked for a fresh position + void request(uint32_t now_s) { // the app asked for a fresh position _enabled = true; _state = GPS_WARMING; _t_start = now_s; @@ -46,18 +55,29 @@ class GpsPolicy { void tick(uint32_t now_s, bool fix_ok) { if (!_enabled) return; if (_state == GPS_WARMING) { - if (fix_ok) { _state = GPS_DONE; _t_done = now_s; _fixes++; _timed_out = false; } - else if (since(now_s, _t_start) >= _timeout) { _state = GPS_DONE; _t_done = now_s; _timed_out = true; } + if (fix_ok) { + _state = GPS_DONE; + _t_done = now_s; + _fixes++; + _timed_out = false; + } else if (since(now_s, _t_start) >= _timeout) { + _state = GPS_DONE; + _t_done = now_s; + _timed_out = true; + } } else if (_state == GPS_DONE) { - if (_interval > 0 && since(now_s, _t_done) >= _interval) { _state = GPS_WARMING; _t_start = now_s; } + if (_interval > 0 && since(now_s, _t_done) >= _interval) { + _state = GPS_WARMING; + _t_start = now_s; + } } } - bool power_wanted() const { return _state == GPS_WARMING; } + bool powerWanted() const { return _state == GPS_WARMING; } GpsState state() const { return _state; } - bool timed_out() const { return _timed_out; } - uint32_t fix_count() const { return _fixes; } - uint32_t warming_for(uint32_t now_s) const { return _state == GPS_WARMING ? since(now_s, _t_start) : 0; } + bool timedOut() const { return _timed_out; } + uint32_t fixCount() const { return _fixes; } + uint32_t warmingFor(uint32_t now_s) const { return _state == GPS_WARMING ? since(now_s, _t_start) : 0; } }; -} // namespace asr650x +} // namespace asr650x diff --git a/src/helpers/asr650x/IdentityCrypto.cpp b/src/helpers/asr650x/IdentityCrypto.cpp index daf16c809a..6ca520740f 100644 --- a/src/helpers/asr650x/IdentityCrypto.cpp +++ b/src/helpers/asr650x/IdentityCrypto.cpp @@ -1,45 +1,62 @@ #ifdef IDENTITY_CRYPTO_ON_BIG_STACK #define ASR650X_IDENTITY_CRYPTO_IMPL #include "IdentityCrypto.h" + #include "BigStack.h" #define ED25519_NO_SEED 1 -#include #include +#include namespace { struct Args { - unsigned char* o1; + unsigned char *o1; const unsigned char *a, *b, *c; size_t len; bool ok; }; -void runKeypair(void* p) { Args* x = (Args*)p; ed25519_create_keypair(x->o1, (unsigned char*)x->a, x->b); } -void runDerive(void* p) { Args* x = (Args*)p; ed25519_derive_pub(x->o1, x->a); } -void runSign(void* p) { Args* x = (Args*)p; ed25519_sign(x->o1, x->a, x->len, x->b, x->c); } -void runKeyex(void* p) { Args* x = (Args*)p; ed25519_key_exchange(x->o1, x->a, x->b); } -void runVerify(void* p) { Args* x = (Args*)p; x->ok = Ed25519::verify(x->a, x->b, x->c, x->len); } +void runKeypair(void *p) { + Args *x = (Args *)p; + ed25519_create_keypair(x->o1, (unsigned char *)x->a, x->b); +} +void runDerive(void *p) { + Args *x = (Args *)p; + ed25519_derive_pub(x->o1, x->a); +} +void runSign(void *p) { + Args *x = (Args *)p; + ed25519_sign(x->o1, x->a, x->len, x->b, x->c); +} +void runKeyex(void *p) { + Args *x = (Args *)p; + ed25519_key_exchange(x->o1, x->a, x->b); +} +void runVerify(void *p) { + Args *x = (Args *)p; + x->ok = Ed25519::verify(x->a, x->b, x->c, x->len); } +} // namespace -void asr650x_ed25519_create_keypair(unsigned char* pub, unsigned char* prv, const unsigned char* seed) { +void asr650xEd25519CreateKeypair(unsigned char *pub, unsigned char *prv, const unsigned char *seed) { Args a = { pub, prv, seed, 0, 0, false }; - asr650x_call_on_big_stack(runKeypair, &a); + asr650xCallOnBigStack(runKeypair, &a); } -void asr650x_ed25519_derive_pub(unsigned char* pub, const unsigned char* prv) { +void asr650xEd25519DerivePub(unsigned char *pub, const unsigned char *prv) { Args a = { pub, prv, 0, 0, 0, false }; - asr650x_call_on_big_stack(runDerive, &a); + asr650xCallOnBigStack(runDerive, &a); } -void asr650x_ed25519_sign(unsigned char* sig, const unsigned char* msg, size_t len, - const unsigned char* pub, const unsigned char* prv) { +void asr650xEd25519Sign(unsigned char *sig, const unsigned char *msg, size_t len, const unsigned char *pub, + const unsigned char *prv) { Args a = { sig, msg, pub, prv, len, false }; - asr650x_call_on_big_stack(runSign, &a); + asr650xCallOnBigStack(runSign, &a); } -void asr650x_ed25519_key_exchange(unsigned char* secret, const unsigned char* pub, const unsigned char* prv) { +void asr650xEd25519KeyExchange(unsigned char *secret, const unsigned char *pub, const unsigned char *prv) { Args a = { secret, pub, prv, 0, 0, false }; - asr650x_call_on_big_stack(runKeyex, &a); + asr650xCallOnBigStack(runKeyex, &a); } -bool asr650x_ed25519_verify(const unsigned char* sig, const unsigned char* pub, const unsigned char* msg, size_t len) { +bool asr650xEd25519Verify(const unsigned char *sig, const unsigned char *pub, const unsigned char *msg, + size_t len) { Args a = { 0, sig, pub, msg, len, false }; - asr650x_call_on_big_stack(runVerify, &a); + asr650xCallOnBigStack(runVerify, &a); return a.ok; } #endif diff --git a/src/helpers/asr650x/IdentityCrypto.h b/src/helpers/asr650x/IdentityCrypto.h index a57f2a11f8..6d6cf4228c 100644 --- a/src/helpers/asr650x/IdentityCrypto.h +++ b/src/helpers/asr650x/IdentityCrypto.h @@ -1,19 +1,20 @@ #pragma once #include -// Ed25519 on the big stack (BigStack.h): the software Ed25519 needs more than the ASR650x's fixed 2 KB main stack. -// Identity.cpp includes this header when IDENTITY_CRYPTO_ON_BIG_STACK is defined; the macros below then route its -// ed25519_* calls through these wrappers. -void asr650x_ed25519_create_keypair(unsigned char* pub, unsigned char* prv, const unsigned char* seed); -void asr650x_ed25519_derive_pub(unsigned char* pub, const unsigned char* prv); -void asr650x_ed25519_sign(unsigned char* sig, const unsigned char* msg, size_t len, - const unsigned char* pub, const unsigned char* prv); -void asr650x_ed25519_key_exchange(unsigned char* secret, const unsigned char* pub, const unsigned char* prv); -bool asr650x_ed25519_verify(const unsigned char* sig, const unsigned char* pub, const unsigned char* msg, size_t len); +// Ed25519 on the big stack (BigStack.h): the software Ed25519 needs more than the ASR650x's fixed 2 KB main +// stack. Identity.cpp includes this header when IDENTITY_CRYPTO_ON_BIG_STACK is defined; the macros below +// then route its ed25519_* calls through these wrappers. +void asr650xEd25519CreateKeypair(unsigned char *pub, unsigned char *prv, const unsigned char *seed); +void asr650xEd25519DerivePub(unsigned char *pub, const unsigned char *prv); +void asr650xEd25519Sign(unsigned char *sig, const unsigned char *msg, size_t len, const unsigned char *pub, + const unsigned char *prv); +void asr650xEd25519KeyExchange(unsigned char *secret, const unsigned char *pub, const unsigned char *prv); +bool asr650xEd25519Verify(const unsigned char *sig, const unsigned char *pub, const unsigned char *msg, + size_t len); #ifndef ASR650X_IDENTITY_CRYPTO_IMPL - #define ed25519_create_keypair asr650x_ed25519_create_keypair - #define ed25519_derive_pub asr650x_ed25519_derive_pub - #define ed25519_sign asr650x_ed25519_sign - #define ed25519_key_exchange asr650x_ed25519_key_exchange +#define ed25519_create_keypair asr650xEd25519CreateKeypair +#define ed25519_derive_pub asr650xEd25519DerivePub +#define ed25519_sign asr650xEd25519Sign +#define ed25519_key_exchange asr650xEd25519KeyExchange #endif diff --git a/src/helpers/asr650x/NmeaParser.h b/src/helpers/asr650x/NmeaParser.h index aa2956c88b..0a33747c97 100644 --- a/src/helpers/asr650x/NmeaParser.h +++ b/src/helpers/asr650x/NmeaParser.h @@ -9,28 +9,29 @@ namespace asr650x { enum NmeaEvent { NMEA_NONE = 0, NMEA_GGA = 1, NMEA_RMC = 2, NMEA_BAD = 3 }; struct GpsData { - bool pos_valid; // latest GGA/RMC carried a usable position - uint8_t fix_quality; // GGA field 6 (0 = none) - uint8_t sats; // GGA field 7 - int32_t lat_e6, lon_e6; // degrees x 1e6 - int32_t alt_dm; // decimetres - bool rmc_valid; // RMC status 'A' - uint32_t time_hhmmss; // UTC, from RMC - uint32_t date_ddmmyy; // from RMC + bool pos_valid; // latest GGA/RMC carried a usable position + uint8_t fix_quality; // GGA field 6 (0 = none) + uint8_t sats; // GGA field 7 + int32_t lat_e6, lon_e6; // degrees x 1e6 + int32_t alt_dm; // decimetres + bool rmc_valid; // RMC status 'A' + uint32_t time_hhmmss; // UTC, from RMC + uint32_t date_ddmmyy; // from RMC }; -inline uint8_t nmea_checksum(const char* s, size_t n) { +inline uint8_t nmeaChecksum(const char *s, size_t n) { uint8_t c = 0; - for (size_t i = 0; i < n; i++) c ^= (uint8_t)s[i]; + for (size_t i = 0; i < n; i++) + c ^= (uint8_t)s[i]; return c; } // "$body*HH\r\n"; returns the length, or 0 if out is too small. -inline int nmea_make(char* out, size_t out_size, const char* body) { +inline int nmeaMake(char *out, size_t out_size, const char *body) { size_t n = strlen(body); - if (out_size < n + 7) return 0; // $ + body + * + 2 hex + \r\n + NUL + if (out_size < n + 7) return 0; // $ + body + * + 2 hex + \r\n + NUL static const char H[] = "0123456789ABCDEF"; - uint8_t c = nmea_checksum(body, n); + uint8_t c = nmeaChecksum(body, n); out[0] = '$'; memcpy(out + 1, body, n); out[1 + n] = '*'; @@ -42,22 +43,26 @@ inline int nmea_make(char* out, size_t out_size, const char* body) { return (int)(n + 6); } -inline bool nmea_is_leap(int y) { return (y % 4 == 0 && y % 100 != 0) || y % 400 == 0; } +inline bool nmeaIsLeap(int y) { + return (y % 4 == 0 && y % 100 != 0) || y % 400 == 0; +} // UTC epoch from RMC date/time. 0 means invalid (year outside 2024..2060, bad month/day/hour/minute/second). -inline uint32_t gps_to_epoch(uint32_t date_ddmmyy, uint32_t time_hhmmss) { +inline uint32_t gpsToEpoch(uint32_t date_ddmmyy, uint32_t time_hhmmss) { int dd = (int)(date_ddmmyy / 10000), mm = (int)((date_ddmmyy / 100) % 100), yy = (int)(date_ddmmyy % 100); int hh = (int)(time_hhmmss / 10000), mi = (int)((time_hhmmss / 100) % 100), ss = (int)(time_hhmmss % 100); int year = 2000 + yy; if (year < 2024 || year > 2060) return 0; if (mm < 1 || mm > 12 || dd < 1) return 0; - static const int dim[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; - int days_in_month = dim[mm - 1] + ((mm == 2 && nmea_is_leap(year)) ? 1 : 0); + static const int dim[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; + int days_in_month = dim[mm - 1] + ((mm == 2 && nmeaIsLeap(year)) ? 1 : 0); if (dd > days_in_month) return 0; if (hh > 23 || mi > 59 || ss > 59) return 0; uint32_t days = 0; - for (int y = 1970; y < year; y++) days += nmea_is_leap(y) ? 366 : 365; - for (int m = 1; m < mm; m++) days += dim[m - 1] + ((m == 2 && nmea_is_leap(year)) ? 1 : 0); + for (int y = 1970; y < year; y++) + days += nmeaIsLeap(y) ? 366 : 365; + for (int m = 1; m < mm; m++) + days += dim[m - 1] + ((m == 2 && nmeaIsLeap(year)) ? 1 : 0); days += (uint32_t)(dd - 1); return days * 86400u + (uint32_t)hh * 3600u + (uint32_t)mi * 60u + (uint32_t)ss; } @@ -69,7 +74,7 @@ class NmeaParser { GpsData _d; uint32_t _ok, _bad; - static bool parse_uint(const char* s, uint32_t* out) { // digits only, non-empty + static bool parseUint(const char *s, uint32_t *out) { // digits only, non-empty if (!*s) return false; uint32_t v = 0; for (; *s; s++) { @@ -82,46 +87,66 @@ class NmeaParser { } // "ddmm.mmmm" (or dddmm.mmmm) + hemisphere -> degrees x 1e6; false if malformed or empty - static bool parse_latlon(const char* s, char hemi, int deg_digits, int32_t* out) { + static bool parseLatlon(const char *s, char hemi, int deg_digits, int32_t *out) { if (!*s) return false; int n = 0; - while (s[n] >= '0' && s[n] <= '9') n++; + while (s[n] >= '0' && s[n] <= '9') + n++; if (n != deg_digits + 2) return false; uint32_t deg = 0; - for (int i = 0; i < deg_digits; i++) deg = deg * 10 + (uint32_t)(s[i] - '0'); + for (int i = 0; i < deg_digits; i++) + deg = deg * 10 + (uint32_t)(s[i] - '0'); uint32_t whole = (uint32_t)(s[deg_digits] - '0') * 10 + (uint32_t)(s[deg_digits + 1] - '0'); - const char* p = s + n; - uint64_t frac = 0, scale = 1; // fractional minutes, up to 6 digits + const char *p = s + n; + uint64_t frac = 0, scale = 1; // fractional minutes, up to 6 digits if (*p == '.') { p++; int k = 0; while (*p >= '0' && *p <= '9') { - if (k < 6) { frac = frac * 10 + (uint64_t)(*p - '0'); scale *= 10; k++; } + if (k < 6) { + frac = frac * 10 + (uint64_t)(*p - '0'); + scale *= 10; + k++; + } p++; } } if (*p != 0) return false; if (whole >= 60) return false; - uint64_t minutes_scaled = (uint64_t)whole * scale + frac; // minutes x scale - uint64_t frac_e6 = (minutes_scaled * 1000000u + 30u * scale) / (60u * scale); // rounded + uint64_t minutes_scaled = (uint64_t)whole * scale + frac; // minutes x scale + uint64_t frac_e6 = (minutes_scaled * 1000000u + 30u * scale) / (60u * scale); // rounded int64_t v = (int64_t)deg * 1000000 + (int64_t)frac_e6; - if (v > (deg_digits == 2 ? 90000000LL : 180000000LL)) return false; // beyond the pole / the antimeridian - if (hemi == 'S' || hemi == 'W') v = -v; - else if (hemi != 'N' && hemi != 'E') return false; + if (v > (deg_digits == 2 ? 90000000LL : 180000000LL)) return false; // beyond the pole / the antimeridian + if (hemi == 'S' || hemi == 'W') + v = -v; + else if (hemi != 'N' && hemi != 'E') + return false; *out = (int32_t)v; return true; } // "123.4" -> decimetres (one decimal, truncated); false if malformed - static bool parse_dm(const char* s, int32_t* out) { + static bool parseDm(const char *s, int32_t *out) { if (!*s) return false; bool neg = false; - if (*s == '-') { neg = true; s++; } + if (*s == '-') { + neg = true; + s++; + } if (*s < '0' || *s > '9') return false; int32_t v = 0; - while (*s >= '0' && *s <= '9') { v = v * 10 + (*s - '0'); s++; if (v > 1000000) return false; } + while (*s >= '0' && *s <= '9') { + v = v * 10 + (*s - '0'); + s++; + if (v > 1000000) return false; + } int32_t d = 0; - if (*s == '.') { s++; if (*s >= '0' && *s <= '9') d = *s - '0'; while (*s >= '0' && *s <= '9') s++; } + if (*s == '.') { + s++; + if (*s >= '0' && *s <= '9') d = *s - '0'; + while (*s >= '0' && *s <= '9') + s++; + } if (*s != 0) return false; v = v * 10 + d; *out = neg ? -v : v; @@ -137,32 +162,41 @@ class NmeaParser { NmeaEvent parse() { _buf[_len] = 0; - char* star = (char*)memchr(_buf, '*', _len); + char *star = (char *)memchr(_buf, '*', _len); if (!star || (_buf + _len) - star != 3) return NMEA_BAD; int h1 = hex(star[1]), h2 = hex(star[2]); if (h1 < 0 || h2 < 0) return NMEA_BAD; - if (nmea_checksum(_buf, (size_t)(star - _buf)) != (uint8_t)(h1 * 16 + h2)) return NMEA_BAD; + if (nmeaChecksum(_buf, (size_t)(star - _buf)) != (uint8_t)(h1 * 16 + h2)) return NMEA_BAD; *star = 0; - char* f[20]; + char *f[20]; int nf = 0; f[nf++] = _buf; - for (char* p = _buf; *p && nf < 20; p++) if (*p == ',') { *p = 0; f[nf++] = p + 1; } + for (char *p = _buf; *p && nf < 20; p++) + if (*p == ',') { + *p = 0; + f[nf++] = p + 1; + } size_t idlen = strlen(f[0]); - if (idlen < 5) { _ok++; return NMEA_NONE; } - const char* type = f[0] + idlen - 3; + if (idlen < 5) { + _ok++; + return NMEA_NONE; + } + const char *type = f[0] + idlen - 3; if (strcmp(type, "GGA") == 0 && nf >= 10) { uint32_t q = 0, sats = 0; - parse_uint(f[6], &q); - parse_uint(f[7], &sats); + parseUint(f[6], &q); + parseUint(f[7], &sats); _d.fix_quality = (uint8_t)(q > 255 ? 255 : q); _d.sats = (uint8_t)(sats > 255 ? 255 : sats); int32_t lat = 0, lon = 0, alt = 0; - bool ok = _d.fix_quality > 0 && parse_latlon(f[2], f[3][0], 2, &lat) && parse_latlon(f[4], f[5][0], 3, &lon); + bool ok = + _d.fix_quality > 0 && parseLatlon(f[2], f[3][0], 2, &lat) && parseLatlon(f[4], f[5][0], 3, &lon); if (ok) { - _d.lat_e6 = lat; _d.lon_e6 = lon; - if (parse_dm(f[9], &alt)) _d.alt_dm = alt; + _d.lat_e6 = lat; + _d.lon_e6 = lon; + if (parseDm(f[9], &alt)) _d.alt_dm = alt; } _d.pos_valid = ok; _ok++; @@ -173,16 +207,26 @@ class NmeaParser { _d.rmc_valid = false; uint32_t t = 0, dt = 0; int32_t lat = 0, lon = 0; - bool ok = a && parse_latlon(f[3], f[4][0], 2, &lat) && parse_latlon(f[5], f[6][0], 3, &lon); + bool ok = a && parseLatlon(f[3], f[4][0], 2, &lat) && parseLatlon(f[5], f[6][0], 3, &lon); // time "hhmmss[.sss]": keep the integer part char tb[8]; size_t tl = 0; - while (f[1][tl] && f[1][tl] != '.' && tl < 7) { tb[tl] = f[1][tl]; tl++; } + while (f[1][tl] && f[1][tl] != '.' && tl < 7) { + tb[tl] = f[1][tl]; + tl++; + } tb[tl] = 0; - bool tok = tl == 6 && parse_uint(tb, &t); - bool dok = strlen(f[9]) == 6 && parse_uint(f[9], &dt); - if (a && tok && dok) { _d.rmc_valid = true; _d.time_hhmmss = t; _d.date_ddmmyy = dt; } - if (ok) { _d.lat_e6 = lat; _d.lon_e6 = lon; } + bool tok = tl == 6 && parseUint(tb, &t); + bool dok = strlen(f[9]) == 6 && parseUint(f[9], &dt); + if (a && tok && dok) { + _d.rmc_valid = true; + _d.time_hhmmss = t; + _d.date_ddmmyy = dt; + } + if (ok) { + _d.lat_e6 = lat; + _d.lon_e6 = lon; + } _d.pos_valid = ok; _ok++; return NMEA_RMC; @@ -194,13 +238,18 @@ class NmeaParser { public: NmeaParser() : _len(0), _active(false), _ok(0), _bad(0) { memset(&_d, 0, sizeof(_d)); } - const GpsData& data() const { return _d; } - void reset_fix() { _d.pos_valid = false; _d.rmc_valid = false; _d.fix_quality = 0; _d.sats = 0; } // after a power cycle: old data is not a fix - uint32_t sentences_ok() const { return _ok; } - uint32_t sentences_bad() const { return _bad; } + const GpsData &data() const { return _d; } + void resetFix() { + _d.pos_valid = false; + _d.rmc_valid = false; + _d.fix_quality = 0; + _d.sats = 0; + } // after a power cycle: old data is not a fix + uint32_t sentencesOk() const { return _ok; } + uint32_t sentencesBad() const { return _bad; } NmeaEvent feed(uint8_t c) { - if (c == '$') { // (re)start; a '$' inside a broken sentence is a resync + if (c == '$') { // (re)start; a '$' inside a broken sentence is a resync if (_active && _len > 0) _bad++; _len = 0; _active = true; @@ -215,7 +264,7 @@ class NmeaParser { _len = 0; return e; } - if (_len >= sizeof(_buf) - 1) { // overlong: drop it + if (_len >= sizeof(_buf) - 1) { // overlong: drop it _active = false; _len = 0; _bad++; @@ -226,4 +275,4 @@ class NmeaParser { } }; -} // namespace asr650x +} // namespace asr650x diff --git a/src/helpers/asr650x/OledText.h b/src/helpers/asr650x/OledText.h index 0e1a01d651..8a70525805 100644 --- a/src/helpers/asr650x/OledText.h +++ b/src/helpers/asr650x/OledText.h @@ -1,21 +1,23 @@ #pragma once +#include "Font5x7.h" + #include #include -#include "Font5x7.h" namespace asr650x { const int OLED_COLS = 128; -const int OLED_CHARS = 21; // 21 characters x 6 columns = 126 +const int OLED_CHARS = 21; // 21 characters x 6 columns = 126 -// One SSD1306 page (8 pixel rows, 128 columns) holding a line of text; bit 0 of each byte is the top pixel row. -inline void render_line(const char* text, uint8_t page[OLED_COLS]) { +// One SSD1306 page (8 pixel rows, 128 columns) holding a line of text; bit 0 of each byte is the top pixel +// row. +inline void renderLine(const char *text, uint8_t page[OLED_COLS]) { memset(page, 0, OLED_COLS); if (!text) return; for (int i = 0; i < OLED_CHARS && text[i]; i++) { - const uint8_t* g = font5x7_glyph(text[i]); + const uint8_t *g = font5x7Glyph(text[i]); memcpy(page + i * 6, g, 5); } } -} // namespace asr650x +} // namespace asr650x diff --git a/src/helpers/asr650x/RadioParams.h b/src/helpers/asr650x/RadioParams.h index c1c9848ebb..5d4c6bb1b9 100644 --- a/src/helpers/asr650x/RadioParams.h +++ b/src/helpers/asr650x/RadioParams.h @@ -1,61 +1,73 @@ #pragma once -#include #include +#include // Pure helpers for the SX1262 wrapper (host-testable). Bandwidth table order is the Heltec driver's -// `Bandwidths[]` (libraries/LoraWan102/src/radio/radio.c): 125, 250, 500, 62.5, 41.67, 31.25, 20.83, 15.63, 10.42, 7.81 kHz. +// `Bandwidths[]` (libraries/LoraWan102/src/radio/radio.c): 125, 250, +// 500, 62.5, 41.67, 31.25, 20.83, 15.63, 10.42, 7.81 kHz. namespace asr650x { -inline float bw_value(int index) { - static const float table[10] = {125.0f, 250.0f, 500.0f, 62.5f, 41.67f, 31.25f, 20.83f, 15.63f, 10.42f, 7.81f}; +inline float bwValue(int index) { + static const float table[10] = { 125.0f, 250.0f, 500.0f, 62.5f, 41.67f, + 31.25f, 20.83f, 15.63f, 10.42f, 7.81f }; if (index < 0) index = 0; if (index > 9) index = 9; return table[index]; } // Nearest supported bandwidth by ratio (so 100 kHz maps to 125, not to 62.5). Never returns an invalid index. -inline int bw_index(float bw_khz) { - if (!(bw_khz > 0.0f)) return 9; // zero, negative or NaN -> smallest +inline int bwIndex(float bw_khz) { + if (!(bw_khz > 0.0f)) return 9; // zero, negative or NaN -> smallest int best = 0; float best_ratio = 1e9f; for (int i = 0; i < 10; i++) { - float v = bw_value(i); - float r = bw_khz > v ? bw_khz / v : v / bw_khz; // >= 1, closer to 1 is nearer - if (r < best_ratio) { best_ratio = r; best = i; } + float v = bwValue(i); + float r = bw_khz > v ? bw_khz / v : v / bw_khz; // >= 1, closer to 1 is nearer + if (r < best_ratio) { + best_ratio = r; + best = i; + } } return best; } -inline uint8_t cr_index(uint8_t cr) { // MeshCore 5..8 (4/5..4/8) -> Heltec 1..4 +inline uint8_t crIndex(uint8_t cr) { // MeshCore 5..8 (4/5..4/8) -> Heltec 1..4 if (cr < 5) cr = 5; if (cr > 8) cr = 8; return (uint8_t)(cr - 4); } -inline uint8_t sf_clamp(uint8_t sf) { return sf < 5 ? 5 : (sf > 12 ? 12 : sf); } +inline uint8_t sfClamp(uint8_t sf) { + return sf < 5 ? 5 : (sf > 12 ? 12 : sf); +} -inline uint16_t preamble_for_sf(uint8_t sf) { return sf <= 8 ? 32 : 16; } // same rule as MeshCore's RadioLibWrapper +inline uint16_t preambleForSf(uint8_t sf) { + return sf <= 8 ? 32 : 16; +} // same rule as MeshCore's RadioLibWrapper -// Semtech time-on-air, explicit header, CRC on, integer microsecond arithmetic. Returns milliseconds (rounded up). -inline uint32_t airtime_ms(int len, float bw_khz, uint8_t sf, uint8_t cr, uint16_t preamble) { +// Semtech time-on-air, explicit header, CRC on, integer microsecond arithmetic. Returns milliseconds (rounded +// up). +inline uint32_t airtimeMs(int len, float bw_khz, uint8_t sf, uint8_t cr, uint16_t preamble) { if (len < 0) len = 0; - sf = sf_clamp(sf); - uint8_t crv = cr_index(cr); // 1..4 + sf = sfClamp(sf); + uint8_t crv = crIndex(cr); // 1..4 uint32_t bw_hz = (uint32_t)(bw_khz * 1000.0f + 0.5f); if (bw_hz == 0) bw_hz = 1; uint32_t tsym_us = (uint32_t)(((uint64_t)1000000u << sf) / bw_hz); - int de = tsym_us >= 16380u ? 1 : 0; // low data-rate optimisation: same threshold as the Heltec driver (16.38 ms) - int num = 8 * len - 4 * (int)sf + 28 + 16; // +16 CRC, explicit header (no -20) + int de = + tsym_us >= 16380u ? 1 : 0; // low data-rate optimisation: same threshold as the Heltec driver (16.38 ms) + int num = 8 * len - 4 * (int)sf + 28 + 16; // +16 CRC, explicit header (no -20) int den = 4 * ((int)sf - 2 * de); int blocks = num > 0 ? (num + den - 1) / den : 0; uint32_t npay = 8u + (uint32_t)(blocks * (crv + 4)); - uint64_t us = ((uint64_t)(preamble * 4 + 17) * tsym_us) / 4 + (uint64_t)npay * tsym_us; // (n_pre + 4.25) symbols + uint64_t us = + ((uint64_t)(preamble * 4 + 17) * tsym_us) / 4 + (uint64_t)npay * tsym_us; // (n_pre + 4.25) symbols return (uint32_t)((us + 999u) / 1000u); } // Same estimate MeshCore uses to weigh received packets (RadioLibWrapper::packetScoreInt). -inline float packet_score(float snr, int sf, int packet_len) { - static const float snr_threshold[] = {-7.5f, -10.0f, -12.5f, -15.0f, -17.5f, -20.0f}; // SF7..SF12 +inline float packetScore(float snr, int sf, int packet_len) { + static const float snr_threshold[] = { -7.5f, -10.0f, -12.5f, -15.0f, -17.5f, -20.0f }; // SF7..SF12 if (sf < 7 || sf > 12) return 0.0f; if (snr < snr_threshold[sf - 7]) return 0.0f; float success = (snr - snr_threshold[sf - 7]) / 10.0f; @@ -64,4 +76,4 @@ inline float packet_score(float snr, int sf, int packet_len) { return v < 0.0f ? 0.0f : (v > 1.0f ? 1.0f : v); } -} // namespace asr650x +} // namespace asr650x diff --git a/src/helpers/asr650x/Sbrk.cpp b/src/helpers/asr650x/Sbrk.cpp index a982e5c6b8..512ef7213f 100644 --- a/src/helpers/asr650x/Sbrk.cpp +++ b/src/helpers/asr650x/Sbrk.cpp @@ -1,24 +1,24 @@ // Override of the core's weak _sbrk (Cm0plusStart.c). The core caps the heap at CYDEV_HEAP_SIZE (4096 B) -// although the linker leaves everything between .bss and the 2 KB stack unused. With the crypto stack -// (2.3 KB), the packet pool, the UART buffer and the flash page cache (256 B, allocated during every -// EEPROM commit) the 4 KB cap is exceeded, malloc() fails and crypto silently runs on the 2 KB main -// stack (M1 hardware run). Here the heap may grow up to the bottom of the stack region. +// although the linker leaves everything between .bss and the 2 KB stack unused. The crypto stack (2 KB), the +// packet pool, the UART buffer and the flash page cache (256 B, allocated during every SFLASH write) need +// more than 4 KB, so with the core's version malloc() fails. Here the heap may grow up to the bottom of the +// stack. #include #include extern "C" { -extern int end; // linker: first free byte after .bss -extern uint32_t __cy_stack_limit[]; // linker: bottom of the 2 KB stack region +extern int end; // linker: first free byte after .bss +extern uint32_t __cy_stack_limit[]; // linker: bottom of the 2 KB stack region -void* _sbrk(int nbytes) { - static uint8_t* heap_ptr = (uint8_t*)&end; - uint8_t* limit = (uint8_t*)__cy_stack_limit; - uint8_t* next = heap_ptr + nbytes; - if (next > limit || next < (uint8_t*)&end) { +void *_sbrk(int nbytes) { + static uint8_t *heap_ptr = (uint8_t *)&end; + uint8_t *limit = (uint8_t *)__cy_stack_limit; + uint8_t *next = heap_ptr + nbytes; + if (next > limit || next < (uint8_t *)&end) { errno = ENOMEM; - return (void*)-1; + return (void *)-1; } - void* prev = heap_ptr; + void *prev = heap_ptr; heap_ptr = next; return prev; } diff --git a/src/helpers/asr650x/SmallStrtod.cpp b/src/helpers/asr650x/SmallStrtod.cpp index 43e90412ba..65b2e393cc 100644 --- a/src/helpers/asr650x/SmallStrtod.cpp +++ b/src/helpers/asr650x/SmallStrtod.cpp @@ -1,10 +1,17 @@ #ifdef ASR650X_PLATFORM -// Linked before libc: these definitions replace newlib's strtod/strtof/atof (and the ~7 KB of gdtoa they pull in). +// Linked before libc: these definitions replace newlib's strtod/strtof/atof (and the ~7 KB of gdtoa they pull +// in). #include "SmallStrtod.h" extern "C" { -double strtod(const char* s, char** end) { return asr650x_strtod(s, end); } -float strtof(const char* s, char** end) { return (float)asr650x_strtod(s, end); } -double atof(const char* s) { return asr650x_strtod(s, 0); } +double strtod(const char *s, char **end) { + return asr650xStrtod(s, end); +} +float strtof(const char *s, char **end) { + return (float)asr650xStrtod(s, end); +} +double atof(const char *s) { + return asr650xStrtod(s, 0); +} } #endif diff --git a/src/helpers/asr650x/SmallStrtod.h b/src/helpers/asr650x/SmallStrtod.h index 047332fc94..ad26c6ddb6 100644 --- a/src/helpers/asr650x/SmallStrtod.h +++ b/src/helpers/asr650x/SmallStrtod.h @@ -1,53 +1,79 @@ #pragma once -#include #include +#include // Decimal-only strtod() (optional sign, digits, optional fraction, optional exponent; no hex/inf/nan). // newlib's strtod() pulls in ~7 KB of flash (gdtoa); the CLI only parses plain decimal numbers. // SmallStrtod.cpp installs it as strtod/strtof/atof on ASR650x builds. -inline double asr650x_strtod(const char* s, char** end) { - const char* p = s; - while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == '\f' || *p == '\v') p++; +inline double asr650xStrtod(const char *s, char **end) { + const char *p = s; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == '\f' || *p == '\v') + p++; bool neg = false; - if (*p == '+' || *p == '-') { neg = (*p == '-'); p++; } + if (*p == '+' || *p == '-') { + neg = (*p == '-'); + p++; + } uint64_t mant = 0; int digits = 0, scale = 0; bool any = false; while (*p >= '0' && *p <= '9') { any = true; - if (digits < 19) { mant = mant * 10 + (uint64_t)(*p - '0'); digits += (mant != 0); } - else scale++; // more digits than fit: keep the magnitude only + if (digits < 19) { + mant = mant * 10 + (uint64_t)(*p - '0'); + digits += (mant != 0); + } else + scale++; // more digits than fit: keep the magnitude only p++; } if (*p == '.') { - const char* q = p + 1; + const char *q = p + 1; bool frac = false; while (*q >= '0' && *q <= '9') { frac = true; - if (digits < 19) { mant = mant * 10 + (uint64_t)(*q - '0'); digits += (mant != 0); scale--; } + if (digits < 19) { + mant = mant * 10 + (uint64_t)(*q - '0'); + digits += (mant != 0); + scale--; + } q++; } - if (any || frac) { any = true; p = q; } + if (any || frac) { + any = true; + p = q; + } } if (!any) { - if (end) *end = (char*)s; + if (end) *end = (char *)s; return 0.0; } if (*p == 'e' || *p == 'E') { - const char* q = p + 1; + const char *q = p + 1; bool eneg = false; - if (*q == '+' || *q == '-') { eneg = (*q == '-'); q++; } + if (*q == '+' || *q == '-') { + eneg = (*q == '-'); + q++; + } if (*q >= '0' && *q <= '9') { int e = 0; - while (*q >= '0' && *q <= '9') { if (e < 400) e = e * 10 + (*q - '0'); q++; } + while (*q >= '0' && *q <= '9') { + if (e < 400) e = e * 10 + (*q - '0'); + q++; + } scale += eneg ? -e : e; p = q; } } double v = (double)mant; - while (scale > 0) { v *= 10.0; scale--; } - while (scale < 0) { v /= 10.0; scale++; } - if (end) *end = (char*)p; + while (scale > 0) { + v *= 10.0; + scale--; + } + while (scale < 0) { + v /= 10.0; + scale++; + } + if (end) *end = (char *)p; return neg ? -v : v; } diff --git a/src/helpers/asr650x/StatusLines.h b/src/helpers/asr650x/StatusLines.h index 191ca883e3..610e412bf8 100644 --- a/src/helpers/asr650x/StatusLines.h +++ b/src/helpers/asr650x/StatusLines.h @@ -7,78 +7,94 @@ namespace asr650x { struct StatusInput { - const char* name; - uint8_t id4[4]; // first bytes of the public key + const char *name; + uint8_t id4[4]; // first bytes of the public key bool time_valid; - uint32_t epoch; // UTC seconds - uint8_t gps_state; // 0 off, 1 searching, 2 fixed, 3 timeout + uint32_t epoch; // UTC seconds + uint8_t gps_state; // 0 off, 1 searching, 2 fixed, 3 timeout uint8_t sats; bool pos_valid; int32_t lat_e6, lon_e6; - uint16_t batt_mv; // 0 = not measured - uint8_t contacts; // contacts in the table - uint8_t max_contacts; // table size (MAX_CONTACTS) + uint16_t batt_mv; // 0 = not measured + uint8_t contacts; // contacts in the table + uint8_t max_contacts; // table size (MAX_CONTACTS) uint32_t freq_khz; uint8_t sf; - uint32_t warming_s; + uint32_t warmingS; }; // The text of screen line `index` (0..7), at most 21 characters (plus the NUL in out[22]). -inline void status_line(int index, const StatusInput& in, char out[22]) { +inline void statusLine(int index, const StatusInput &in, char out[22]) { char t[48]; t[0] = 0; switch (index) { - case 0: { - size_t n = 0; - if (in.name) { - for (; in.name[n] && n < 21; n++) { - unsigned char c = (unsigned char)in.name[n]; - t[n] = (c >= 0x20 && c < 0x7f) ? (char)c : '?'; - } + case 0: { + size_t n = 0; + if (in.name) { + for (; in.name[n] && n < 21; n++) { + unsigned char c = (unsigned char)in.name[n]; + t[n] = (c >= 0x20 && c < 0x7f) ? (char)c : '?'; } - t[n] = 0; - break; } - case 1: snprintf(t, sizeof(t), "ID %02x%02x%02x%02x", (unsigned)in.id4[0], (unsigned)in.id4[1], (unsigned)in.id4[2], (unsigned)in.id4[3]); break; - case 2: - if (!in.time_valid) snprintf(t, sizeof(t), "--:--:-- no time"); - else { - uint32_t s = in.epoch % 86400u; - snprintf(t, sizeof(t), "%02u:%02u:%02u UTC", (unsigned)(s / 3600), (unsigned)((s / 60) % 60), (unsigned)(s % 60)); - } - break; - case 3: - if (in.gps_state == 1) snprintf(t, sizeof(t), "GPS search %us", (unsigned)(in.warming_s > 999 ? 999 : in.warming_s)); - else if (in.gps_state == 2) snprintf(t, sizeof(t), "GPS fix %u sats", (unsigned)in.sats); - else if (in.gps_state == 3) snprintf(t, sizeof(t), "GPS timeout"); - else snprintf(t, sizeof(t), "GPS off"); - break; - case 4: - case 5: { - if (!in.pos_valid) { snprintf(t, sizeof(t), index == 4 ? "LAT --" : "LON --"); break; } - int32_t v = index == 4 ? in.lat_e6 : in.lon_e6; - bool neg = v < 0; - uint32_t a = neg ? (uint32_t)(-(int64_t)v) : (uint32_t)v; - // sign and whole degrees are right-aligned in a fixed field so the decimals line up - char whole[12]; - snprintf(whole, sizeof(whole), "%s%u", neg ? "-" : "", (unsigned)(a / 1000000u)); - snprintf(t, sizeof(t), "%s %3s.%06u", index == 4 ? "LAT" : "LON", whole, (unsigned)(a % 1000000u)); - break; + t[n] = 0; + break; + } + case 1: + snprintf(t, sizeof(t), "ID %02x%02x%02x%02x", (unsigned)in.id4[0], (unsigned)in.id4[1], + (unsigned)in.id4[2], (unsigned)in.id4[3]); + break; + case 2: + if (!in.time_valid) + snprintf(t, sizeof(t), "--:--:-- no time"); + else { + uint32_t s = in.epoch % 86400u; + snprintf(t, sizeof(t), "%02u:%02u:%02u UTC", (unsigned)(s / 3600), (unsigned)((s / 60) % 60), + (unsigned)(s % 60)); } - case 6: { - char b[10]; - if (in.batt_mv == 0) snprintf(b, sizeof(b), "--"); - else snprintf(b, sizeof(b), "%u.%02uV", (unsigned)(in.batt_mv / 1000), (unsigned)((in.batt_mv % 1000) / 10)); - snprintf(t, sizeof(t), "BAT %s CT %u/%u", b, (unsigned)in.contacts, (unsigned)in.max_contacts); + break; + case 3: + if (in.gps_state == 1) + snprintf(t, sizeof(t), "GPS search %us", (unsigned)(in.warmingS > 999 ? 999 : in.warmingS)); + else if (in.gps_state == 2) + snprintf(t, sizeof(t), "GPS fix %u sats", (unsigned)in.sats); + else if (in.gps_state == 3) + snprintf(t, sizeof(t), "GPS timeout"); + else + snprintf(t, sizeof(t), "GPS off"); + break; + case 4: + case 5: { + if (!in.pos_valid) { + snprintf(t, sizeof(t), index == 4 ? "LAT --" : "LON --"); break; } - case 7: - snprintf(t, sizeof(t), "%u.%03uMHz SF%u", (unsigned)(in.freq_khz / 1000), (unsigned)(in.freq_khz % 1000), (unsigned)in.sf); - break; - default: break; + int32_t v = index == 4 ? in.lat_e6 : in.lon_e6; + bool neg = v < 0; + uint32_t a = neg ? (uint32_t)(-(int64_t)v) : (uint32_t)v; + // sign and whole degrees are right-aligned in a fixed field so the decimals line up + char whole[12]; + snprintf(whole, sizeof(whole), "%s%u", neg ? "-" : "", (unsigned)(a / 1000000u)); + snprintf(t, sizeof(t), "%s %3s.%06u", index == 4 ? "LAT" : "LON", whole, (unsigned)(a % 1000000u)); + break; + } + case 6: { + char b[10]; + if (in.batt_mv == 0) + snprintf(b, sizeof(b), "--"); + else + snprintf(b, sizeof(b), "%u.%02uV", (unsigned)(in.batt_mv / 1000), (unsigned)((in.batt_mv % 1000) / 10)); + snprintf(t, sizeof(t), "BAT %s CT %u/%u", b, (unsigned)in.contacts, (unsigned)in.max_contacts); + break; + } + case 7: + snprintf(t, sizeof(t), "%u.%03uMHz SF%u", (unsigned)(in.freq_khz / 1000), (unsigned)(in.freq_khz % 1000), + (unsigned)in.sf); + break; + default: + break; } strncpy(out, t, 21); out[21] = 0; } -} // namespace asr650x +} // namespace asr650x diff --git a/src/helpers/asr650x/TxGate.cpp b/src/helpers/asr650x/TxGate.cpp index 8a7ac8ee7f..f6a34bc59b 100644 --- a/src/helpers/asr650x/TxGate.cpp +++ b/src/helpers/asr650x/TxGate.cpp @@ -2,5 +2,9 @@ static bool g_tx_allowed = false; -bool asr650x_tx_allowed() { return g_tx_allowed; } -void asr650x_set_tx_allowed(bool allowed) { g_tx_allowed = allowed; } +bool asr650xTxAllowed() { + return g_tx_allowed; +} +void asr650xSetTxAllowed(bool allowed) { + g_tx_allowed = allowed; +} diff --git a/src/helpers/asr650x/TxGate.h b/src/helpers/asr650x/TxGate.h index 5f419925a4..d75e64b281 100644 --- a/src/helpers/asr650x/TxGate.h +++ b/src/helpers/asr650x/TxGate.h @@ -2,5 +2,5 @@ // The companion firmware for this board does not generate its own identity: the app imports a private key. // Until then the node runs with a placeholder identity derived from the chip ID, which must never transmit. -bool asr650x_tx_allowed(); -void asr650x_set_tx_allowed(bool allowed); +bool asr650xTxAllowed(); +void asr650xSetTxAllowed(bool allowed); diff --git a/test/test_asr650x_button/test_asr650x_button.cpp b/test/test_asr650x_button/test_asr650x_button.cpp index fc6d990b3b..eea3dfd07e 100644 --- a/test/test_asr650x_button/test_asr650x_button.cpp +++ b/test/test_asr650x_button/test_asr650x_button.cpp @@ -1,19 +1,20 @@ -#include -#include #include "helpers/asr650x/Button.h" +#include +#include + TEST(Asr650xButton, Behaves) { /* a clean press is reported once, after the debounce time, and not again while held */ { asr650x::Button b; EXPECT_TRUE(!b.update(false, 0)); EXPECT_TRUE(!b.update(true, 100)); - EXPECT_TRUE(!b.update(true, 120)); // 20 ms: still bouncing - EXPECT_TRUE(b.update(true, 135)); // >= 30 ms held: reported - EXPECT_TRUE(!b.update(true, 500)); // held: no repeat + EXPECT_TRUE(!b.update(true, 120)); // 20 ms: still bouncing + EXPECT_TRUE(b.update(true, 135)); // >= 30 ms held: reported + EXPECT_TRUE(!b.update(true, 500)); // held: no repeat EXPECT_TRUE(!b.update(false, 600)); EXPECT_TRUE(!b.update(true, 700)); - EXPECT_TRUE(b.update(true, 731)); // second press needs the release in between + EXPECT_TRUE(b.update(true, 731)); // second press needs the release in between } /* contact bounce shorter than the debounce time never fires */ { @@ -31,7 +32,8 @@ TEST(Asr650xButton, Behaves) { EXPECT_TRUE(!b.update(true, 1001)); EXPECT_TRUE(!b.update(true, 1030)); EXPECT_TRUE(!b.update(true, 1049)); - EXPECT_TRUE(!b.update(true, 1060)); // pin still LOW after the hold-off: not a new press, it must be released first + EXPECT_TRUE(!b.update( + true, 1060)); // pin still LOW after the hold-off: not a new press, it must be released first EXPECT_TRUE(!b.update(false, 1100)); EXPECT_TRUE(!b.update(true, 1200)); EXPECT_TRUE(b.update(true, 1235)); @@ -48,11 +50,11 @@ TEST(Asr650xButton, Behaves) { { asr650x::Button b; EXPECT_TRUE(!b.update(true, 0xFFFFFFF0u)); - EXPECT_TRUE(b.update(true, 0x00000020u)); // 48 ms later across the wrap + EXPECT_TRUE(b.update(true, 0x00000020u)); // 48 ms later across the wrap } } -int main(int argc, char** argv) { +int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } diff --git a/test/test_asr650x_epoch_clock/test_asr650x_epoch_clock.cpp b/test/test_asr650x_epoch_clock/test_asr650x_epoch_clock.cpp index 8866d37ada..013712c962 100644 --- a/test/test_asr650x_epoch_clock/test_asr650x_epoch_clock.cpp +++ b/test/test_asr650x_epoch_clock/test_asr650x_epoch_clock.cpp @@ -1,28 +1,34 @@ -#include -#include #include "helpers/asr650x/EpochClock.h" +#include +#include + TEST(Asr650xEpochClock, Behaves) { asr650x::EpochClock c(1000); - EXPECT_TRUE(c.now(0) == 1000); // starts at the boot epoch - EXPECT_TRUE(c.now(999) == 1000); // not yet a full second + EXPECT_TRUE(c.now(0) == 1000); // starts at the boot epoch + EXPECT_TRUE(c.now(999) == 1000); // not yet a full second EXPECT_TRUE(c.now(1000) == 1001); - EXPECT_TRUE(c.now(3500) == 1003); // remainder carried, not lost + EXPECT_TRUE(c.now(3500) == 1003); // remainder carried, not lost c.set(5000, 3500); - EXPECT_TRUE(c.now(3500) == 5000); // set() is exact at that instant + EXPECT_TRUE(c.now(3500) == 5000); // set() is exact at that instant EXPECT_TRUE(c.now(4499) == 5000); EXPECT_TRUE(c.now(4500) == 5001); - c.set(2000, 4500); // going backwards is allowed + c.set(2000, 4500); // going backwards is allowed EXPECT_TRUE(c.now(5500) == 2001); - asr650x::EpochClock w(100); // millis() wrap at 2^32 + asr650x::EpochClock w(100); // millis() wrap at 2^32 EXPECT_TRUE(w.now(0xFFFFFF00u) == 100 + 4294967u); uint32_t before = w.now(0xFFFFFF00u); - uint32_t after = w.now(0x00000100u); // 0x200 ms later + uint32_t after = w.now(0x00000100u); // 0x200 ms later EXPECT_TRUE(after >= before && after - before <= 1); - { asr650x::EpochClock s(1000); EXPECT_TRUE(!s.synced()); s.set(2000, 0); EXPECT_TRUE(s.synced()); } + { + asr650x::EpochClock s(1000); + EXPECT_TRUE(!s.synced()); + s.set(2000, 0); + EXPECT_TRUE(s.synced()); + } } -int main(int argc, char** argv) { +int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } diff --git a/test/test_asr650x_gps_policy/test_asr650x_gps_policy.cpp b/test/test_asr650x_gps_policy/test_asr650x_gps_policy.cpp index 2a9a382c5f..52eaccf8b8 100644 --- a/test/test_asr650x_gps_policy/test_asr650x_gps_policy.cpp +++ b/test/test_asr650x_gps_policy/test_asr650x_gps_policy.cpp @@ -1,21 +1,22 @@ -#include -#include #include "helpers/asr650x/GpsPolicy.h" +#include +#include + TEST(Asr650xGpsPolicy, Behaves) { /* default: enabled, interval 0, timeout 300 s */ { asr650x::GpsPolicy p; p.configure(true, 0, 300); - EXPECT_TRUE(p.state() == asr650x::GPS_OFF && !p.power_wanted()); // nothing happens before begin() + EXPECT_TRUE(p.state() == asr650x::GPS_OFF && !p.powerWanted()); // nothing happens before begin() p.begin(100); - EXPECT_TRUE(p.state() == asr650x::GPS_WARMING && p.power_wanted()); + EXPECT_TRUE(p.state() == asr650x::GPS_WARMING && p.powerWanted()); p.tick(110, false); - EXPECT_TRUE(p.state() == asr650x::GPS_WARMING && p.warming_for(110) == 10); - p.tick(130, true); // fix -> done, power off - EXPECT_TRUE(p.state() == asr650x::GPS_DONE && !p.power_wanted() && p.fix_count() == 1 && !p.timed_out()); - p.tick(100000, false); // interval 0: stays off forever - EXPECT_TRUE(p.state() == asr650x::GPS_DONE && !p.power_wanted()); + EXPECT_TRUE(p.state() == asr650x::GPS_WARMING && p.warmingFor(110) == 10); + p.tick(130, true); // fix -> done, power off + EXPECT_TRUE(p.state() == asr650x::GPS_DONE && !p.powerWanted() && p.fixCount() == 1 && !p.timedOut()); + p.tick(100000, false); // interval 0: stays off forever + EXPECT_TRUE(p.state() == asr650x::GPS_DONE && !p.powerWanted()); } /* timeout without a fix */ { @@ -25,7 +26,7 @@ TEST(Asr650xGpsPolicy, Behaves) { p.tick(299, false); EXPECT_TRUE(p.state() == asr650x::GPS_WARMING); p.tick(300, false); - EXPECT_TRUE(p.state() == asr650x::GPS_DONE && p.timed_out() && p.fix_count() == 0 && !p.power_wanted()); + EXPECT_TRUE(p.state() == asr650x::GPS_DONE && p.timedOut() && p.fixCount() == 0 && !p.powerWanted()); } /* periodic refresh */ { @@ -36,10 +37,10 @@ TEST(Asr650xGpsPolicy, Behaves) { EXPECT_TRUE(p.state() == asr650x::GPS_DONE); p.tick(619, false); EXPECT_TRUE(p.state() == asr650x::GPS_DONE); - p.tick(620, false); // 600 s after entering DONE at t=20 - EXPECT_TRUE(p.state() == asr650x::GPS_WARMING && p.power_wanted()); + p.tick(620, false); // 600 s after entering DONE at t=20 + EXPECT_TRUE(p.state() == asr650x::GPS_WARMING && p.powerWanted()); p.tick(640, true); - EXPECT_TRUE(p.state() == asr650x::GPS_DONE && p.fix_count() == 2); + EXPECT_TRUE(p.state() == asr650x::GPS_DONE && p.fixCount() == 2); } /* disabled: never powers, even on tick/begin */ { @@ -47,7 +48,7 @@ TEST(Asr650xGpsPolicy, Behaves) { p.configure(false, 60, 300); p.begin(0); p.tick(1000, true); - EXPECT_TRUE(p.state() == asr650x::GPS_OFF && !p.power_wanted() && p.fix_count() == 0); + EXPECT_TRUE(p.state() == asr650x::GPS_OFF && !p.powerWanted() && p.fixCount() == 0); } /* the app turns GPS on later: starts warming immediately; turning it off stops at once */ { @@ -56,9 +57,9 @@ TEST(Asr650xGpsPolicy, Behaves) { p.begin(0); p.configure(true, 0, 300); p.request(50); - EXPECT_TRUE(p.state() == asr650x::GPS_WARMING && p.power_wanted() && p.warming_for(60) == 10); + EXPECT_TRUE(p.state() == asr650x::GPS_WARMING && p.powerWanted() && p.warmingFor(60) == 10); p.configure(false, 0, 300); - EXPECT_TRUE(p.state() == asr650x::GPS_OFF && !p.power_wanted()); + EXPECT_TRUE(p.state() == asr650x::GPS_OFF && !p.powerWanted()); } /* request() while already DONE re-acquires and clears the timeout flag */ { @@ -66,17 +67,17 @@ TEST(Asr650xGpsPolicy, Behaves) { p.configure(true, 0, 10); p.begin(0); p.tick(10, false); - EXPECT_TRUE(p.timed_out()); + EXPECT_TRUE(p.timedOut()); p.request(20); - EXPECT_TRUE(p.state() == asr650x::GPS_WARMING && !p.timed_out()); + EXPECT_TRUE(p.state() == asr650x::GPS_WARMING && !p.timedOut()); } /* clock going backwards (RTC set by the app) must not wedge the machine */ { asr650x::GpsPolicy p; p.configure(true, 100, 300); p.begin(1000); - p.tick(500, false); // now < start: treated as 0 s elapsed, no underflow - EXPECT_TRUE(p.state() == asr650x::GPS_WARMING && p.warming_for(500) == 0); + p.tick(500, false); // now < start: treated as 0 s elapsed, no underflow + EXPECT_TRUE(p.state() == asr650x::GPS_WARMING && p.warmingFor(500) == 0); } /* changing the interval while DONE applies to the next cycle */ { @@ -99,18 +100,18 @@ TEST(Asr650xGpsPolicy, TimeoutAndIntervalAcrossCounterWrap) { p.begin(0xFFFFFF00u); p.tick(0xFFFFFF00u + 299u, false); EXPECT_EQ(p.state(), asr650x::GPS_WARMING); - p.tick(0xFFFFFF00u + 300u, false); // wrapped: 0x2C + p.tick(0xFFFFFF00u + 300u, false); // wrapped: 0x2C EXPECT_EQ(p.state(), asr650x::GPS_DONE); - EXPECT_TRUE(p.timed_out()); + EXPECT_TRUE(p.timedOut()); uint32_t done = 0xFFFFFF00u + 300u; p.tick(done + 599u, false); EXPECT_EQ(p.state(), asr650x::GPS_DONE); p.tick(done + 600u, false); EXPECT_EQ(p.state(), asr650x::GPS_WARMING); - EXPECT_EQ(p.warming_for(done + 610u), 10u); + EXPECT_EQ(p.warmingFor(done + 610u), 10u); } -int main(int argc, char** argv) { +int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } diff --git a/test/test_asr650x_nmea/test_asr650x_nmea.cpp b/test/test_asr650x_nmea/test_asr650x_nmea.cpp index 263ccd4f66..1f613655f8 100644 --- a/test/test_asr650x_nmea/test_asr650x_nmea.cpp +++ b/test/test_asr650x_nmea/test_asr650x_nmea.cpp @@ -1,10 +1,11 @@ -#include +#include "helpers/asr650x/NmeaParser.h" + #include #include +#include #include -#include "helpers/asr650x/NmeaParser.h" -static asr650x::NmeaEvent feed_str(asr650x::NmeaParser& p, const std::string& s) { +static asr650x::NmeaEvent feed_str(asr650x::NmeaParser &p, const std::string &s) { asr650x::NmeaEvent last = asr650x::NMEA_NONE; for (unsigned char c : s) { asr650x::NmeaEvent e = p.feed(c); @@ -17,31 +18,37 @@ TEST(Asr650xNmeaParser, Behaves) { /* classic GGA example: 48 07.038 N, 11 31.000 E, 8 sats, 545.4 m */ { asr650x::NmeaParser p; - EXPECT_TRUE(feed_str(p, "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n") == asr650x::NMEA_GGA); - const asr650x::GpsData& d = p.data(); + EXPECT_TRUE(feed_str(p, "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n") == + asr650x::NMEA_GGA); + const asr650x::GpsData &d = p.data(); EXPECT_TRUE(d.pos_valid && d.fix_quality == 1 && d.sats == 8); EXPECT_TRUE(d.lat_e6 == 48117300 && d.lon_e6 == 11516667 && d.alt_dm == 5454); - EXPECT_TRUE(p.sentences_ok() == 1 && p.sentences_bad() == 0); + EXPECT_TRUE(p.sentencesOk() == 1 && p.sentencesBad() == 0); } /* 5-decimal minutes, western hemisphere (Air530 style, GN talker) */ { asr650x::NmeaParser p; - EXPECT_TRUE(feed_str(p, "$GNGGA,092750.000,5321.68020,N,00630.33720,W,1,08,1.03,61.7,M,55.2,M,,*58\r\n") == asr650x::NMEA_GGA); + EXPECT_TRUE( + feed_str(p, "$GNGGA,092750.000,5321.68020,N,00630.33720,W,1,08,1.03,61.7,M,55.2,M,,*58\r\n") == + asr650x::NMEA_GGA); EXPECT_TRUE(p.data().lat_e6 == 53361337 && p.data().lon_e6 == -6505620 && p.data().alt_dm == 617); } /* southern / eastern */ { asr650x::NmeaParser p; - EXPECT_TRUE(feed_str(p, "$GNGGA,000000.000,3351.60000,S,15112.00000,E,1,05,1.0,10.0,M,0.0,M,,*5E\r\n") == asr650x::NMEA_GGA); + EXPECT_TRUE(feed_str(p, "$GNGGA,000000.000,3351.60000,S,15112.00000,E,1,05,1.0,10.0,M,0.0,M,,*5E\r\n") == + asr650x::NMEA_GGA); EXPECT_TRUE(p.data().lat_e6 == -33860000 && p.data().lon_e6 == 151200000 && p.data().sats == 5); } /* RMC with a 2026 date */ { asr650x::NmeaParser p; - EXPECT_TRUE(feed_str(p, "$GNRMC,092750.000,A,5321.68020,N,00630.33720,W,0.02,31.66,041026,,,A*53\r\n") == asr650x::NMEA_RMC); + EXPECT_TRUE(feed_str(p, "$GNRMC,092750.000,A,5321.68020,N,00630.33720,W,0.02,31.66,041026,,,A*53\r\n") == + asr650x::NMEA_RMC); EXPECT_TRUE(p.data().rmc_valid && p.data().time_hhmmss == 92750 && p.data().date_ddmmyy == 41026); EXPECT_TRUE(p.data().pos_valid && p.data().lat_e6 == 53361337); - EXPECT_TRUE(asr650x::gps_to_epoch(p.data().date_ddmmyy, p.data().time_hhmmss) == 1791106070u); /* 2026-10-04 09:27:50 UTC */ + EXPECT_TRUE(asr650x::gpsToEpoch(p.data().date_ddmmyy, p.data().time_hhmmss) == + 1791106070u); /* 2026-10-04 09:27:50 UTC */ } /* no fix: empty fields, quality 0 / status V: never a valid position */ { @@ -62,16 +69,21 @@ TEST(Asr650xNmeaParser, Behaves) { /* bad checksum, missing '*', truncated, no terminator, garbage, resync on '$' */ { asr650x::NmeaParser p; - EXPECT_TRUE(feed_str(p, "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*48\r\n") == asr650x::NMEA_BAD); + EXPECT_TRUE(feed_str(p, "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*48\r\n") == + asr650x::NMEA_BAD); EXPECT_TRUE(!p.data().pos_valid); - EXPECT_TRUE(feed_str(p, "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,\r\n") == asr650x::NMEA_BAD); - EXPECT_TRUE(feed_str(p, "$GPGGA,1235") == asr650x::NMEA_NONE); /* truncated: nothing until a terminator */ - EXPECT_TRUE(feed_str(p, "\r\n") == asr650x::NMEA_BAD); /* the truncated sentence ends here: rejected */ - EXPECT_TRUE(feed_str(p, "\x01\x02\xff garbage \x80\r\n") == asr650x::NMEA_NONE); /* noise outside a sentence is ignored */ + EXPECT_TRUE(feed_str(p, "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,\r\n") == + asr650x::NMEA_BAD); + EXPECT_TRUE(feed_str(p, "$GPGGA,1235") == asr650x::NMEA_NONE); /* truncated: nothing until a terminator */ + EXPECT_TRUE(feed_str(p, "\r\n") == asr650x::NMEA_BAD); /* the truncated sentence ends here: rejected */ + EXPECT_TRUE(feed_str(p, "\x01\x02\xff garbage \x80\r\n") == + asr650x::NMEA_NONE); /* noise outside a sentence is ignored */ /* a '$' in the middle of a broken sentence restarts it */ - EXPECT_TRUE(feed_str(p, "$GPGGA,12$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n") == asr650x::NMEA_GGA); + EXPECT_TRUE( + feed_str(p, "$GPGGA,12$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n") == + asr650x::NMEA_GGA); EXPECT_TRUE(p.data().pos_valid && p.data().lat_e6 == 48117300); - EXPECT_TRUE(p.sentences_bad() >= 2); + EXPECT_TRUE(p.sentencesBad() >= 2); } /* overlong sentence: dropped, parser recovers */ { @@ -80,20 +92,21 @@ TEST(Asr650xNmeaParser, Behaves) { longs.append(300, 'A'); longs += "*00\r\n"; EXPECT_TRUE(feed_str(p, longs) == asr650x::NMEA_BAD); - EXPECT_TRUE(feed_str(p, "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n") == asr650x::NMEA_GGA); + EXPECT_TRUE(feed_str(p, "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n") == + asr650x::NMEA_GGA); } /* other sentence types are valid but ignored */ { asr650x::NmeaParser p; EXPECT_TRUE(feed_str(p, "$GPGSV,1,1,00*79\r\n") == asr650x::NMEA_NONE); - EXPECT_TRUE(p.sentences_ok() == 1); + EXPECT_TRUE(p.sentencesOk() == 1); } /* sentences with non-numeric junk in numeric fields must not become a position */ { asr650x::NmeaParser p; - const char* body = "GPGGA,123519,48x7.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,"; + const char *body = "GPGGA,123519,48x7.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,"; char line[128]; - EXPECT_TRUE(asr650x::nmea_make(line, sizeof(line), body) > 0); + EXPECT_TRUE(asr650x::nmeaMake(line, sizeof(line), body) > 0); feed_str(p, line); EXPECT_TRUE(!p.data().pos_valid); } @@ -101,61 +114,70 @@ TEST(Asr650xNmeaParser, Behaves) { /* checksum helper */ { char out[128]; - int n = asr650x::nmea_make(out, sizeof(out), "PCAS03,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0"); + int n = asr650x::nmeaMake(out, sizeof(out), "PCAS03,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0"); EXPECT_TRUE(n > 0 && std::strcmp(out, "$PCAS03,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0*02\r\n") == 0); char small[8]; - EXPECT_TRUE(asr650x::nmea_make(small, sizeof(small), "PCAS03,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0") == 0); + EXPECT_TRUE(asr650x::nmeaMake(small, sizeof(small), "PCAS03,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0") == 0); } /* epoch conversion */ - EXPECT_TRUE(asr650x::gps_to_epoch(41026, 123456) == 1791117296u); /* 2026-10-04 12:34:56 */ - EXPECT_TRUE(asr650x::gps_to_epoch(10125, 0) == 1735689600u); /* 2025-01-01 00:00:00 */ - EXPECT_TRUE(asr650x::gps_to_epoch(280226, 235959) == 1772323199u); /* 2026-02-28 23:59:59 */ - EXPECT_TRUE(asr650x::gps_to_epoch(290228, 0) == 1835395200u); /* 2028-02-29 (leap day) */ - EXPECT_TRUE(asr650x::gps_to_epoch(290227, 0) == 0); /* 2027-02-29 does not exist */ - EXPECT_TRUE(asr650x::gps_to_epoch(310226, 0) == 0); /* Feb 31 */ - EXPECT_TRUE(asr650x::gps_to_epoch(230394, 123519) == 0); /* 2094: outside the accepted window */ - EXPECT_TRUE(asr650x::gps_to_epoch(41023, 0) == 0); /* 2023: before the window */ - EXPECT_TRUE(asr650x::gps_to_epoch(41026, 250000) == 0 && asr650x::gps_to_epoch(41026, 126000) == 0 && asr650x::gps_to_epoch(41026, 120060) == 0); - EXPECT_TRUE(asr650x::gps_to_epoch(0, 0) == 0 && asr650x::gps_to_epoch(41026, 0) != 0); + EXPECT_TRUE(asr650x::gpsToEpoch(41026, 123456) == 1791117296u); /* 2026-10-04 12:34:56 */ + EXPECT_TRUE(asr650x::gpsToEpoch(10125, 0) == 1735689600u); /* 2025-01-01 00:00:00 */ + EXPECT_TRUE(asr650x::gpsToEpoch(280226, 235959) == 1772323199u); /* 2026-02-28 23:59:59 */ + EXPECT_TRUE(asr650x::gpsToEpoch(290228, 0) == 1835395200u); /* 2028-02-29 (leap day) */ + EXPECT_TRUE(asr650x::gpsToEpoch(290227, 0) == 0); /* 2027-02-29 does not exist */ + EXPECT_TRUE(asr650x::gpsToEpoch(310226, 0) == 0); /* Feb 31 */ + EXPECT_TRUE(asr650x::gpsToEpoch(230394, 123519) == 0); /* 2094: outside the accepted window */ + EXPECT_TRUE(asr650x::gpsToEpoch(41023, 0) == 0); /* 2023: before the window */ + EXPECT_TRUE(asr650x::gpsToEpoch(41026, 250000) == 0 && asr650x::gpsToEpoch(41026, 126000) == 0 && + asr650x::gpsToEpoch(41026, 120060) == 0); + EXPECT_TRUE(asr650x::gpsToEpoch(0, 0) == 0 && asr650x::gpsToEpoch(41026, 0) != 0); /* after a GPS power cycle the previous fix must not be reported (reviewer finding: stale RMC/GGA data) */ { asr650x::NmeaParser p; - EXPECT_TRUE(feed_str(p, "$GNGGA,092750.000,5321.68020,N,00630.33720,W,1,08,1.03,61.7,M,55.2,M,,*58\r\n") == asr650x::NMEA_GGA); + EXPECT_TRUE( + feed_str(p, "$GNGGA,092750.000,5321.68020,N,00630.33720,W,1,08,1.03,61.7,M,55.2,M,,*58\r\n") == + asr650x::NMEA_GGA); EXPECT_TRUE(p.data().pos_valid && p.data().fix_quality == 1); - p.reset_fix(); - EXPECT_TRUE(!p.data().pos_valid && !p.data().rmc_valid && p.data().fix_quality == 0 && p.data().sats == 0); + p.resetFix(); + EXPECT_TRUE(!p.data().pos_valid && !p.data().rmc_valid && p.data().fix_quality == 0 && + p.data().sats == 0); } } // a sentence with a valid checksum but an impossible position must not be taken as a fix TEST(Asr650xNmea, OutOfRangeLatLonRejected) { - const char* bodies[] = { - "GNGGA,092750.000,9530.00000,N,00630.33720,W,1,08,1.03,61.7,M,55.2,M,,", // latitude 95 deg - "GNGGA,092750.000,9000.00100,S,00630.33720,W,1,08,1.03,61.7,M,55.2,M,,", // just over 90 deg - "GNGGA,092750.000,5321.68020,N,18530.33720,E,1,08,1.03,61.7,M,55.2,M,,", // longitude 185 deg + const char *bodies[] = { + "GNGGA,092750.000,9530.00000,N,00630.33720,W,1,08,1.03,61.7,M,55.2,M,,", // latitude 95 deg + "GNGGA,092750.000,9000.00100,S,00630.33720,W,1,08,1.03,61.7,M,55.2,M,,", // just over 90 deg + "GNGGA,092750.000,5321.68020,N,18530.33720,E,1,08,1.03,61.7,M,55.2,M,,", // longitude 185 deg }; - for (const char* b : bodies) { + for (const char *b : bodies) { asr650x::NmeaParser p; char line[100]; - int n = asr650x::nmea_make(line, sizeof(line), b); + int n = asr650x::nmeaMake(line, sizeof(line), b); ASSERT_GT(n, 0); asr650x::NmeaEvent last = asr650x::NMEA_NONE; - for (int i = 0; i < n; i++) { asr650x::NmeaEvent e = p.feed((uint8_t)line[i]); if (e != asr650x::NMEA_NONE) last = e; } + for (int i = 0; i < n; i++) { + asr650x::NmeaEvent e = p.feed((uint8_t)line[i]); + if (e != asr650x::NMEA_NONE) last = e; + } EXPECT_EQ(last, asr650x::NMEA_GGA) << b; EXPECT_FALSE(p.data().pos_valid) << b; } // the limits themselves are valid asr650x::NmeaParser p; char line[100]; - int n = asr650x::nmea_make(line, sizeof(line), "GNGGA,092750.000,9000.00000,S,18000.00000,W,1,08,1.03,61.7,M,55.2,M,,"); - for (int i = 0; i < n; i++) p.feed((uint8_t)line[i]); + int n = asr650x::nmeaMake(line, sizeof(line), + "GNGGA,092750.000,9000.00000,S,18000.00000,W,1,08,1.03,61.7,M,55.2,M,,"); + for (int i = 0; i < n; i++) + p.feed((uint8_t)line[i]); EXPECT_TRUE(p.data().pos_valid); EXPECT_EQ(p.data().lat_e6, -90000000); EXPECT_EQ(p.data().lon_e6, -180000000); } -int main(int argc, char** argv) { +int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } diff --git a/test/test_asr650x_oled_text/test_asr650x_oled_text.cpp b/test/test_asr650x_oled_text/test_asr650x_oled_text.cpp index 7f77a39705..cde724868f 100644 --- a/test/test_asr650x_oled_text/test_asr650x_oled_text.cpp +++ b/test/test_asr650x_oled_text/test_asr650x_oled_text.cpp @@ -1,48 +1,60 @@ -#include +#include "helpers/asr650x/OledText.h" + #include #include -#include "helpers/asr650x/OledText.h" +#include TEST(Asr650xOledText, Behaves) { /* glyph table: space is blank, every printable glyph is non-blank, unknown characters map to '?' */ - const uint8_t* sp = asr650x::font5x7_glyph(' '); - for (int i = 0; i < 5; i++) EXPECT_TRUE(sp[i] == 0); + const uint8_t *sp = asr650x::font5x7Glyph(' '); + for (int i = 0; i < 5; i++) + EXPECT_TRUE(sp[i] == 0); for (int c = 0x21; c <= 0x7E; c++) { - const uint8_t* g = asr650x::font5x7_glyph((char)c); + const uint8_t *g = asr650x::font5x7Glyph((char)c); bool any = false; - for (int i = 0; i < 5; i++) any = any || g[i] != 0; + for (int i = 0; i < 5; i++) + any = any || g[i] != 0; EXPECT_TRUE(any); } - EXPECT_TRUE(std::memcmp(asr650x::font5x7_glyph('\x01'), asr650x::font5x7_glyph('?'), 5) == 0); - EXPECT_TRUE(std::memcmp(asr650x::font5x7_glyph((char)0xC3), asr650x::font5x7_glyph('?'), 5) == 0); /* UTF-8 bytes show as '?' */ - EXPECT_TRUE(std::memcmp(asr650x::font5x7_glyph('\x7f'), asr650x::font5x7_glyph('?'), 5) == 0); + EXPECT_TRUE(std::memcmp(asr650x::font5x7Glyph('\x01'), asr650x::font5x7Glyph('?'), 5) == 0); + EXPECT_TRUE(std::memcmp(asr650x::font5x7Glyph((char)0xC3), asr650x::font5x7Glyph('?'), 5) == + 0); /* UTF-8 bytes show as '?' */ + EXPECT_TRUE(std::memcmp(asr650x::font5x7Glyph('\x7f'), asr650x::font5x7Glyph('?'), 5) == 0); /* distinct shapes for look-alikes */ - EXPECT_TRUE(std::memcmp(asr650x::font5x7_glyph('0'), asr650x::font5x7_glyph('O'), 5) != 0); - EXPECT_TRUE(std::memcmp(asr650x::font5x7_glyph('l'), asr650x::font5x7_glyph('I'), 5) != 0); + EXPECT_TRUE(std::memcmp(asr650x::font5x7Glyph('0'), asr650x::font5x7Glyph('O'), 5) != 0); + EXPECT_TRUE(std::memcmp(asr650x::font5x7Glyph('l'), asr650x::font5x7Glyph('I'), 5) != 0); /* one page = 128 columns: 5 glyph columns + 1 gap per character */ uint8_t page[128]; - asr650x::render_line("A", page); - for (int i = 0; i < 5; i++) EXPECT_TRUE(page[i] == asr650x::font5x7_glyph('A')[i]); + asr650x::renderLine("A", page); + for (int i = 0; i < 5; i++) + EXPECT_TRUE(page[i] == asr650x::font5x7Glyph('A')[i]); EXPECT_TRUE(page[5] == 0); - for (int i = 6; i < 128; i++) EXPECT_TRUE(page[i] == 0); - asr650x::render_line("AB", page); - for (int i = 0; i < 5; i++) EXPECT_TRUE(page[6 + i] == asr650x::font5x7_glyph('B')[i]); + for (int i = 6; i < 128; i++) + EXPECT_TRUE(page[i] == 0); + asr650x::renderLine("AB", page); + for (int i = 0; i < 5; i++) + EXPECT_TRUE(page[6 + i] == asr650x::font5x7Glyph('B')[i]); /* empty and null text give a blank page */ - asr650x::render_line("", page); - for (int i = 0; i < 128; i++) EXPECT_TRUE(page[i] == 0); - asr650x::render_line(nullptr, page); - for (int i = 0; i < 128; i++) EXPECT_TRUE(page[i] == 0); + asr650x::renderLine("", page); + for (int i = 0; i < 128; i++) + EXPECT_TRUE(page[i] == 0); + asr650x::renderLine(nullptr, page); + for (int i = 0; i < 128; i++) + EXPECT_TRUE(page[i] == 0); /* more than 21 characters: cut, last two columns stay blank */ - asr650x::render_line("ABCDEFGHIJKLMNOPQRSTUVWXYZ", page); + asr650x::renderLine("ABCDEFGHIJKLMNOPQRSTUVWXYZ", page); EXPECT_TRUE(page[126] == 0 && page[127] == 0); - for (int i = 0; i < 5; i++) EXPECT_TRUE(page[20 * 6 + i] == asr650x::font5x7_glyph('U')[i]); + for (int i = 0; i < 5; i++) + EXPECT_TRUE(page[20 * 6 + i] == asr650x::font5x7Glyph('U')[i]); /* a long run of UTF-8 does not overrun the buffer */ - asr650x::render_line("\xe1\xbb\x87\xe1\xbb\x87\xe1\xbb\x87\xe1\xbb\x87\xe1\xbb\x87\xe1\xbb\x87\xe1\xbb\x87\xe1\xbb\x87\xe1\xbb\x87", page); + asr650x::renderLine("\xe1\xbb\x87\xe1\xbb\x87\xe1\xbb\x87\xe1\xbb\x87\xe1\xbb\x87\xe1\xbb\x87\xe1\xbb\x87" + "\xe1\xbb\x87\xe1\xbb\x87", + page); EXPECT_TRUE(page[127] == 0); } -int main(int argc, char** argv) { +int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } diff --git a/test/test_asr650x_persist/test_asr650x_persist.cpp b/test/test_asr650x_persist/test_asr650x_persist.cpp index 6145b7b281..dfdc4e3e98 100644 --- a/test/test_asr650x_persist/test_asr650x_persist.cpp +++ b/test/test_asr650x_persist/test_asr650x_persist.cpp @@ -1,70 +1,91 @@ -#include +#include "helpers/asr650x/FlashRecord.h" + #include #include -#include "helpers/asr650x/FlashRecord.h" +#include static asr650x::PersistState sample() { - asr650x::PersistState s; memset(&s, 0, sizeof(s)); + asr650x::PersistState s; + memset(&s, 0, sizeof(s)); s.has_identity = true; - for (int i = 0; i < 64; i++) s.prv[i] = (uint8_t)(i + 1); - for (int i = 0; i < 32; i++) s.pub[i] = (uint8_t)(0x80 + i); + for (int i = 0; i < 64; i++) + s.prv[i] = (uint8_t)(i + 1); + for (int i = 0; i < 32; i++) + s.pub[i] = (uint8_t)(0x80 + i); s.has_prefs = true; - strcpy(s.node_name, "KTM-test"); - s.freq = 921.5f; s.bw = 62.5f; s.sf = 7; s.cr = 5; s.tx_power_dbm = 14; + strcpy(s.node_name, "node-test"); + s.freq = 921.5f; + s.bw = 62.5f; + s.sf = 7; + s.cr = 5; + s.tx_power_dbm = 14; return s; } TEST(Asr650xFlashRecord, Behaves) { uint8_t buf[asr650x::PERSIST_SIZE]; - EXPECT_TRUE(asr650x::crc32((const uint8_t*)"123456789", 9) == 0xCBF43926u); // standard CRC-32 check value + EXPECT_TRUE(asr650x::crc32((const uint8_t *)"123456789", 9) == 0xCBF43926u); // standard CRC-32 check value asr650x::PersistState a = sample(), b; - asr650x::persist_encode(a, buf); - EXPECT_TRUE(asr650x::persist_decode(buf, b)); + asr650x::persistEncode(a, buf); + EXPECT_TRUE(asr650x::persistDecode(buf, b)); EXPECT_TRUE(b.has_identity && memcmp(b.prv, a.prv, 64) == 0 && memcmp(b.pub, a.pub, 32) == 0); - EXPECT_TRUE(b.has_prefs && strcmp(b.node_name, "KTM-test") == 0); + EXPECT_TRUE(b.has_prefs && strcmp(b.node_name, "node-test") == 0); EXPECT_TRUE(b.freq == 921.5f && b.bw == 62.5f && b.sf == 7 && b.cr == 5 && b.tx_power_dbm == 14); /* identity only / prefs only */ - a.has_prefs = false; asr650x::persist_encode(a, buf); - EXPECT_TRUE(asr650x::persist_decode(buf, b) && b.has_identity && !b.has_prefs); - a = sample(); a.has_identity = false; asr650x::persist_encode(a, buf); - EXPECT_TRUE(asr650x::persist_decode(buf, b) && !b.has_identity && b.has_prefs); + a.has_prefs = false; + asr650x::persistEncode(a, buf); + EXPECT_TRUE(asr650x::persistDecode(buf, b) && b.has_identity && !b.has_prefs); + a = sample(); + a.has_identity = false; + asr650x::persistEncode(a, buf); + EXPECT_TRUE(asr650x::persistDecode(buf, b) && !b.has_identity && b.has_prefs); /* every single-byte corruption must be rejected and reset the state to empty */ - a = sample(); asr650x::persist_encode(a, buf); + a = sample(); + asr650x::persistEncode(a, buf); for (size_t i = 0; i < asr650x::PERSIST_SIZE; i++) { - uint8_t bad[asr650x::PERSIST_SIZE]; memcpy(bad, buf, sizeof(bad)); + uint8_t bad[asr650x::PERSIST_SIZE]; + memcpy(bad, buf, sizeof(bad)); bad[i] ^= 0x01; asr650x::PersistState c = sample(); - EXPECT_TRUE(!asr650x::persist_decode(bad, c)); + EXPECT_TRUE(!asr650x::persistDecode(bad, c)); EXPECT_TRUE(!c.has_identity && !c.has_prefs); } /* erased flash (all 0x00 / all 0xFF) is not a valid record */ - uint8_t z[asr650x::PERSIST_SIZE]; memset(z, 0x00, sizeof(z)); EXPECT_TRUE(!asr650x::persist_decode(z, b)); - memset(z, 0xFF, sizeof(z)); EXPECT_TRUE(!asr650x::persist_decode(z, b)); + uint8_t z[asr650x::PERSIST_SIZE]; + memset(z, 0x00, sizeof(z)); + EXPECT_TRUE(!asr650x::persistDecode(z, b)); + memset(z, 0xFF, sizeof(z)); + EXPECT_TRUE(!asr650x::persistDecode(z, b)); /* a name without NUL terminator in the record is terminated on decode */ - a = sample(); memset(a.node_name, 'A', 32); asr650x::persist_encode(a, buf); - EXPECT_TRUE(asr650x::persist_decode(buf, b) && b.node_name[31] == 0); + a = sample(); + memset(a.node_name, 'A', 32); + asr650x::persistEncode(a, buf); + EXPECT_TRUE(asr650x::persistDecode(buf, b) && b.node_name[31] == 0); /* GPS settings round-trip and are covered by the CRC */ { asr650x::PersistState g = sample(); - g.gps_off = true; g.gps_interval = 3600; - asr650x::persist_encode(g, buf); + g.gps_off = true; + g.gps_interval = 3600; + asr650x::persistEncode(g, buf); EXPECT_TRUE(buf[147] == 1); - EXPECT_TRUE(buf[152] == (3600 & 0xFF) && buf[153] == ((3600 >> 8) & 0xFF) && buf[154] == 0 && buf[155] == 0); + EXPECT_TRUE(buf[152] == (3600 & 0xFF) && buf[153] == ((3600 >> 8) & 0xFF) && buf[154] == 0 && + buf[155] == 0); asr650x::PersistState h; - EXPECT_TRUE(asr650x::persist_decode(buf, h) && h.gps_off && h.gps_interval == 3600); + EXPECT_TRUE(asr650x::persistDecode(buf, h) && h.gps_off && h.gps_interval == 3600); buf[153] ^= 1; - EXPECT_TRUE(!asr650x::persist_decode(buf, h)); // corrupting the interval byte is detected + EXPECT_TRUE(!asr650x::persistDecode(buf, h)); // corrupting the interval byte is detected } /* a legacy record (bytes 147 and 152..155 are zero) reads as 'GPS on, interval 0' */ { asr650x::PersistState legacy = sample(); - legacy.gps_off = false; legacy.gps_interval = 0; - asr650x::persist_encode(legacy, buf); + legacy.gps_off = false; + legacy.gps_interval = 0; + asr650x::persistEncode(legacy, buf); asr650x::PersistState r; - EXPECT_TRUE(asr650x::persist_decode(buf, r) && !r.gps_off && r.gps_interval == 0); + EXPECT_TRUE(asr650x::persistDecode(buf, r) && !r.gps_off && r.gps_interval == 0); } } @@ -72,45 +93,88 @@ TEST(Asr650xFlashRecord, Behaves) { TEST(Asr650xFlashRecord, Version2KeepsCompanionSettings) { asr650x::PersistState a = sample(), b; a.has_ext = true; - a.airtime_factor = 1.5f; a.rx_delay_base = 2.25f; a.tx_delay_factor = 0.5f; a.direct_tx_delay_factor = 0.25f; - a.multi_acks = 1; a.manual_add_contacts = 1; a.telemetry_modes = 0x26; a.advert_loc_policy = 1; - a.autoadd_config = 0x0E; a.autoadd_max_hops = 3; a.path_hash_mode = 2; a.tz_offset = -7; - a.cad_enabled = 1; a.interference_threshold = 14; a.agc_reset_interval = 5; a.rx_boosted_gain = 1; + a.airtime_factor = 1.5f; + a.rx_delay_base = 2.25f; + a.tx_delay_factor = 0.5f; + a.direct_tx_delay_factor = 0.25f; + a.multi_acks = 1; + a.manual_add_contacts = 1; + a.telemetry_modes = 0x26; + a.advert_loc_policy = 1; + a.autoadd_config = 0x0E; + a.autoadd_max_hops = 3; + a.path_hash_mode = 2; + a.tz_offset = -7; + a.cad_enabled = 1; + a.interference_threshold = 14; + a.agc_reset_interval = 5; + a.rx_boosted_gain = 1; uint8_t buf[asr650x::PERSIST_SIZE]; - asr650x::persist_encode(a, buf); - ASSERT_TRUE(asr650x::persist_decode(buf, b)); + asr650x::persistEncode(a, buf); + ASSERT_TRUE(asr650x::persistDecode(buf, b)); EXPECT_TRUE(b.has_ext); - EXPECT_EQ(b.airtime_factor, 1.5f); EXPECT_EQ(b.rx_delay_base, 2.25f); - EXPECT_EQ(b.tx_delay_factor, 0.5f); EXPECT_EQ(b.direct_tx_delay_factor, 0.25f); - EXPECT_EQ(b.multi_acks, 1); EXPECT_EQ(b.manual_add_contacts, 1); EXPECT_EQ(b.telemetry_modes, 0x26); - EXPECT_EQ(b.advert_loc_policy, 1); EXPECT_EQ(b.autoadd_config, 0x0E); EXPECT_EQ(b.autoadd_max_hops, 3); - EXPECT_EQ(b.path_hash_mode, 2); EXPECT_EQ(b.tz_offset, -7); EXPECT_EQ(b.cad_enabled, 1); - EXPECT_EQ(b.interference_threshold, 14); EXPECT_EQ(b.agc_reset_interval, 5); EXPECT_EQ(b.rx_boosted_gain, 1); - EXPECT_EQ(b.freq, 921.5f); // version 1 fields unchanged - buf[176] ^= 0x01; // a corrupted settings byte is detected - EXPECT_FALSE(asr650x::persist_decode(buf, b)); + EXPECT_EQ(b.airtime_factor, 1.5f); + EXPECT_EQ(b.rx_delay_base, 2.25f); + EXPECT_EQ(b.tx_delay_factor, 0.5f); + EXPECT_EQ(b.direct_tx_delay_factor, 0.25f); + EXPECT_EQ(b.multi_acks, 1); + EXPECT_EQ(b.manual_add_contacts, 1); + EXPECT_EQ(b.telemetry_modes, 0x26); + EXPECT_EQ(b.advert_loc_policy, 1); + EXPECT_EQ(b.autoadd_config, 0x0E); + EXPECT_EQ(b.autoadd_max_hops, 3); + EXPECT_EQ(b.path_hash_mode, 2); + EXPECT_EQ(b.tz_offset, -7); + EXPECT_EQ(b.cad_enabled, 1); + EXPECT_EQ(b.interference_threshold, 14); + EXPECT_EQ(b.agc_reset_interval, 5); + EXPECT_EQ(b.rx_boosted_gain, 1); + EXPECT_EQ(b.freq, 921.5f); // version 1 fields unchanged + buf[176] ^= 0x01; // a corrupted settings byte is detected + EXPECT_FALSE(asr650x::persistDecode(buf, b)); } -// a record written by the first firmware (version 1, 160 bytes, CRC at 156) still loads; the new settings are absent +// a record written by the first firmware (version 1, 160 bytes, CRC at 156) still loads; the new settings are +// absent TEST(Asr650xFlashRecord, Version1RecordStillLoads) { asr650x::PersistState a = sample(); uint8_t v2[asr650x::PERSIST_SIZE]; - asr650x::persist_encode(a, v2); + asr650x::persistEncode(a, v2); uint8_t v1[asr650x::PERSIST_SIZE]; - memset(v1, 0xFF, sizeof(v1)); // bytes after the old record: erased flash + memset(v1, 0xFF, sizeof(v1)); // bytes after the old record: erased flash memcpy(v1, v2, 156); v1[4] = 1; uint32_t crc = asr650x::crc32(v1, 156); - for (int i = 0; i < 4; i++) v1[156 + i] = (uint8_t)(crc >> (8 * i)); + for (int i = 0; i < 4; i++) + v1[156 + i] = (uint8_t)(crc >> (8 * i)); asr650x::PersistState b; - ASSERT_TRUE(asr650x::persist_decode(v1, b)); + ASSERT_TRUE(asr650x::persistDecode(v1, b)); EXPECT_FALSE(b.has_ext); EXPECT_TRUE(b.has_identity && b.has_prefs); EXPECT_EQ(memcmp(b.prv, a.prv, 64), 0); - EXPECT_STREQ(b.node_name, "KTM-test"); + EXPECT_STREQ(b.node_name, "node-test"); +} + +// records are written with the "ASR1" magic; records from early builds ("KTM1") still load +TEST(Asr650xFlashRecord, MagicAndLegacyMagic) { + asr650x::PersistState a = sample(), b; + uint8_t buf[asr650x::PERSIST_SIZE]; + asr650x::persistEncode(a, buf); + EXPECT_EQ(memcmp(buf, "ASR1", 4), 0); + memcpy(buf, "KTM1", 4); + uint32_t crc = asr650x::crc32(buf, 188); + for (int i = 0; i < 4; i++) + buf[188 + i] = (uint8_t)(crc >> (8 * i)); + ASSERT_TRUE(asr650x::persistDecode(buf, b)); + EXPECT_EQ(memcmp(b.prv, a.prv, 64), 0); + memcpy(buf, "XXX1", 4); + crc = asr650x::crc32(buf, 188); + for (int i = 0; i < 4; i++) + buf[188 + i] = (uint8_t)(crc >> (8 * i)); + EXPECT_FALSE(asr650x::persistDecode(buf, b)); } -int main(int argc, char** argv) { +int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } diff --git a/test/test_asr650x_persist_store/test_asr650x_persist_store.cpp b/test/test_asr650x_persist_store/test_asr650x_persist_store.cpp index a304db6b85..7929794ccc 100644 --- a/test/test_asr650x_persist_store/test_asr650x_persist_store.cpp +++ b/test/test_asr650x_persist_store/test_asr650x_persist_store.cpp @@ -1,30 +1,43 @@ -#include +#include "helpers/asr650x/FlashRecordStore.h" + #include #include -#include "helpers/asr650x/FlashRecordStore.h" +#include // RAM-backed fake of the SFLASH user rows: two 256-byte rows, a record per row. struct FakeBackend { uint8_t mem[512]; bool fail_next_write; - size_t torn_bytes; // if > 0: only this many bytes of the next write land (simulated power loss) + size_t torn_bytes; // if > 0: only this many bytes of the next write land (simulated power loss) int writes; size_t last_off; - FakeBackend() : fail_next_write(false), torn_bytes(0), writes(0), last_off(0) { memset(mem, 0xFF, sizeof(mem)); } - void read(size_t off, uint8_t* buf, size_t n) { memcpy(buf, mem + off, n); } - bool write(size_t off, const uint8_t* buf, size_t n) { - writes++; last_off = off; - if (fail_next_write) { fail_next_write = false; return false; } - if (torn_bytes) { memcpy(mem + off, buf, torn_bytes); torn_bytes = 0; return false; } + FakeBackend() : fail_next_write(false), torn_bytes(0), writes(0), last_off(0) { + memset(mem, 0xFF, sizeof(mem)); + } + void read(size_t off, uint8_t *buf, size_t n) { memcpy(buf, mem + off, n); } + bool write(size_t off, const uint8_t *buf, size_t n) { + writes++; + last_off = off; + if (fail_next_write) { + fail_next_write = false; + return false; + } + if (torn_bytes) { + memcpy(mem + off, buf, torn_bytes); + torn_bytes = 0; + return false; + } memcpy(mem + off, buf, n); return true; } }; static asr650x::PersistState with_identity(uint8_t fill) { - asr650x::PersistState s; memset(&s, 0, sizeof(s)); + asr650x::PersistState s; + memset(&s, 0, sizeof(s)); s.has_identity = true; - memset(s.prv, fill, 64); memset(s.pub, fill ^ 0x5A, 32); + memset(s.prv, fill, 64); + memset(s.pub, fill ^ 0x5A, 32); return s; } @@ -37,11 +50,14 @@ TEST(Asr650xFlashRecordStore, Behaves) { /* identity persists across a new store instance, alternating rows */ EXPECT_TRUE(st.commit(with_identity(1))); EXPECT_TRUE(be.last_off == 0); - asr650x::PersistState s2 = st.state(); s2.has_prefs = true; strcpy(s2.node_name, "one"); + asr650x::PersistState s2 = st.state(); + s2.has_prefs = true; + strcpy(s2.node_name, "one"); EXPECT_TRUE(st.commit(s2)); EXPECT_TRUE(be.last_off == 256); { - asr650x::PersistStore again(be); again.begin(); + asr650x::PersistStore again(be); + again.begin(); EXPECT_TRUE(again.state().has_identity && again.state().prv[0] == 1); EXPECT_TRUE(again.state().has_prefs && strcmp(again.state().node_name, "one") == 0); } @@ -51,50 +67,58 @@ TEST(Asr650xFlashRecordStore, Behaves) { EXPECT_TRUE(st.commit(st.state())); EXPECT_TRUE(be.writes == w); - /* review I1: a failed commit must not change state, and must not leak into the next commit */ - asr650x::PersistState want_key2 = st.state(); memset(want_key2.prv, 2, 64); + /* a failed commit must not change state, and must not leak into the next commit */ + asr650x::PersistState want_key2 = st.state(); + memset(want_key2.prv, 2, 64); be.fail_next_write = true; EXPECT_TRUE(!st.commit(want_key2)); EXPECT_TRUE(st.state().prv[0] == 1); - asr650x::PersistState rename = st.state(); strcpy(rename.node_name, "two"); + asr650x::PersistState rename = st.state(); + strcpy(rename.node_name, "two"); EXPECT_TRUE(st.commit(rename)); { - asr650x::PersistStore again(be); again.begin(); - EXPECT_TRUE(again.state().prv[0] == 1); // the rejected key never reached flash + asr650x::PersistStore again(be); + again.begin(); + EXPECT_TRUE(again.state().prv[0] == 1); // the rejected key never reached flash EXPECT_TRUE(strcmp(again.state().node_name, "two") == 0); } - /* review I2: power loss while writing the newer row keeps the previous record intact */ + /* power loss while writing the newer row keeps the previous record intact */ asr650x::PersistState before = st.state(); - asr650x::PersistState next = st.state(); strcpy(next.node_name, "three"); + asr650x::PersistState next = st.state(); + strcpy(next.node_name, "three"); be.torn_bytes = 70; EXPECT_TRUE(!st.commit(next)); { - asr650x::PersistStore again(be); again.begin(); + asr650x::PersistStore again(be); + again.begin(); EXPECT_TRUE(again.state().has_identity && memcmp(again.state().prv, before.prv, 64) == 0); EXPECT_TRUE(strcmp(again.state().node_name, "two") == 0); /* and the store keeps working afterwards (writes the row that was torn) */ EXPECT_TRUE(again.commit(next)); - asr650x::PersistStore third(be); third.begin(); + asr650x::PersistStore third(be); + third.begin(); EXPECT_TRUE(strcmp(third.state().node_name, "three") == 0); } /* both rows corrupt -> empty */ FakeBackend bad; memset(bad.mem, 0xA5, sizeof(bad.mem)); - asr650x::PersistStore sb(bad); sb.begin(); + asr650x::PersistStore sb(bad); + sb.begin(); EXPECT_TRUE(!sb.state().has_identity && !sb.state().has_prefs); EXPECT_TRUE(sb.commit(with_identity(9))); - asr650x::PersistStore sb2(bad); sb2.begin(); + asr650x::PersistStore sb2(bad); + sb2.begin(); EXPECT_TRUE(sb2.state().has_identity && sb2.state().prv[0] == 9); /* sequence number wrap: 0 is newer than 0xFFFFFFFF */ - EXPECT_TRUE(asr650x::seq_newer(0u, 0xFFFFFFFFu)); - EXPECT_TRUE(!asr650x::seq_newer(0xFFFFFFFFu, 0u)); - EXPECT_TRUE(asr650x::seq_newer(5u, 4u) && !asr650x::seq_newer(4u, 5u) && !asr650x::seq_newer(7u, 7u)); + EXPECT_TRUE(asr650x::seqNewer(0u, 0xFFFFFFFFu)); + EXPECT_TRUE(!asr650x::seqNewer(0xFFFFFFFFu, 0u)); + EXPECT_TRUE(asr650x::seqNewer(5u, 4u) && !asr650x::seqNewer(4u, 5u) && !asr650x::seqNewer(7u, 7u)); } -int main(int argc, char** argv) { +int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } diff --git a/test/test_asr650x_radio_params/test_asr650x_radio_params.cpp b/test/test_asr650x_radio_params/test_asr650x_radio_params.cpp index eb579db831..95dfed761c 100644 --- a/test/test_asr650x_radio_params/test_asr650x_radio_params.cpp +++ b/test/test_asr650x_radio_params/test_asr650x_radio_params.cpp @@ -1,13 +1,16 @@ +#include "helpers/asr650x/RadioParams.h" + #include -#include #include #include -#include "helpers/asr650x/RadioParams.h" +#include // Independent reference: Semtech SX1262 datasheet time-on-air (explicit header, CRC on), in microseconds. static double ref_airtime_us(int len, double bw_khz, int sf, int cr, int preamble) { - double tsym = (double)(1 << sf) / (bw_khz * 1000.0); // seconds - int de = (tsym * 1000.0 >= 16.38) ? 1 : 0; // low data-rate optimisation, same rule as the Heltec driver (radio.c) + double tsym = (double)(1 << sf) / (bw_khz * 1000.0); // seconds + int de = (tsym * 1000.0 >= 16.38) + ? 1 + : 0; // low data-rate optimisation, same rule as the Heltec driver (radio.c) double tpre = (preamble + 4.25) * tsym; double num = 8.0 * len - 4.0 * sf + 28 + 16 /*CRC*/ - 0 /*explicit header*/; double den = 4.0 * (sf - 2 * de); @@ -17,42 +20,55 @@ static double ref_airtime_us(int len, double bw_khz, int sf, int cr, int preambl TEST(Asr650xRadioParams, Behaves) { /* bandwidth mapping: exact, nearest, extremes */ - EXPECT_TRUE(asr650x::bw_index(62.5f) == 3 && asr650x::bw_index(125.0f) == 0 && asr650x::bw_index(250.0f) == 1 && asr650x::bw_index(500.0f) == 2); - EXPECT_TRUE(asr650x::bw_index(41.67f) == 4 && asr650x::bw_index(31.25f) == 5 && asr650x::bw_index(20.83f) == 6); - EXPECT_TRUE(asr650x::bw_index(15.63f) == 7 && asr650x::bw_index(10.42f) == 8 && asr650x::bw_index(7.81f) == 9); - EXPECT_TRUE(asr650x::bw_index(100.0f) == 0); // nearest of {62.5, 125} by ratio is 125 (100/62.5 = 1.6 > 125/100 = 1.25) - EXPECT_TRUE(asr650x::bw_index(7.0f) == 9); // below range -> smallest - EXPECT_TRUE(asr650x::bw_index(900.0f) == 2); // above range -> largest - EXPECT_TRUE(asr650x::bw_index(0.0f) == 9 && asr650x::bw_index(-5.0f) == 9); // nonsense never indexes out of the table - EXPECT_TRUE(std::fabs(asr650x::bw_value(3) - 62.5f) < 1e-3 && std::fabs(asr650x::bw_value(0) - 125.0f) < 1e-3); + EXPECT_TRUE(asr650x::bwIndex(62.5f) == 3 && asr650x::bwIndex(125.0f) == 0 && + asr650x::bwIndex(250.0f) == 1 && asr650x::bwIndex(500.0f) == 2); + EXPECT_TRUE(asr650x::bwIndex(41.67f) == 4 && asr650x::bwIndex(31.25f) == 5 && + asr650x::bwIndex(20.83f) == 6); + EXPECT_TRUE(asr650x::bwIndex(15.63f) == 7 && asr650x::bwIndex(10.42f) == 8 && asr650x::bwIndex(7.81f) == 9); + EXPECT_TRUE(asr650x::bwIndex(100.0f) == + 0); // nearest of {62.5, 125} by ratio is 125 (100/62.5 = 1.6 > 125/100 = 1.25) + EXPECT_TRUE(asr650x::bwIndex(7.0f) == 9); // below range -> smallest + EXPECT_TRUE(asr650x::bwIndex(900.0f) == 2); // above range -> largest + EXPECT_TRUE(asr650x::bwIndex(0.0f) == 9 && + asr650x::bwIndex(-5.0f) == 9); // nonsense never indexes out of the table + EXPECT_TRUE(std::fabs(asr650x::bwValue(3) - 62.5f) < 1e-3 && + std::fabs(asr650x::bwValue(0) - 125.0f) < 1e-3); - /* coding rate / sf / preamble. cr arguments use the MeshCore convention 5..8, the result is the Heltec 1..4 */ - EXPECT_TRUE(asr650x::cr_index(5) == 1 && asr650x::cr_index(8) == 4 && asr650x::cr_index(4) == 1 && asr650x::cr_index(9) == 4 && asr650x::cr_index(0) == 1); - EXPECT_TRUE(asr650x::sf_clamp(4) == 5 && asr650x::sf_clamp(13) == 12 && asr650x::sf_clamp(8) == 8); - EXPECT_TRUE(asr650x::preamble_for_sf(7) == 32 && asr650x::preamble_for_sf(8) == 32 && asr650x::preamble_for_sf(9) == 16 && asr650x::preamble_for_sf(12) == 16); + /* coding rate / sf / preamble. cr arguments use the MeshCore convention 5..8, the result is the Heltec 1..4 + */ + EXPECT_TRUE(asr650x::crIndex(5) == 1 && asr650x::crIndex(8) == 4 && asr650x::crIndex(4) == 1 && + asr650x::crIndex(9) == 4 && asr650x::crIndex(0) == 1); + EXPECT_TRUE(asr650x::sfClamp(4) == 5 && asr650x::sfClamp(13) == 12 && asr650x::sfClamp(8) == 8); + EXPECT_TRUE(asr650x::preambleForSf(7) == 32 && asr650x::preambleForSf(8) == 32 && + asr650x::preambleForSf(9) == 16 && asr650x::preambleForSf(12) == 16); /* airtime within 1.5 ms of the independent reference, for the production setting and a spread of others */ - struct { int len; double bw; int sf, cr, pre; } cases[] = { - {110, 62.5, 8, 5, 32}, {1, 62.5, 8, 5, 32}, {255, 62.5, 8, 5, 32}, {110, 125, 7, 5, 32}, - {110, 250, 10, 5, 16}, {50, 125, 12, 8, 16}, {255, 500, 7, 6, 32}, {20, 62.5, 11, 5, 16}, - {20, 15.63, 8, 5, 32} /* 16.378 ms symbols: the driver keeps LDRO off */ + struct { + int len; + double bw; + int sf, cr, pre; + } cases[] = { + { 110, 62.5, 8, 5, 32 }, { 1, 62.5, 8, 5, 32 }, { 255, 62.5, 8, 5, 32 }, { 110, 125, 7, 5, 32 }, + { 110, 250, 10, 5, 16 }, { 50, 125, 12, 8, 16 }, { 255, 500, 7, 6, 32 }, { 20, 62.5, 11, 5, 16 }, + { 20, 15.63, 8, 5, 32 } /* 16.378 ms symbols: the driver keeps LDRO off */ }; - for (auto& c : cases) { - double ref = ref_airtime_us(c.len, c.bw, c.sf, c.cr, c.pre) / 1000.0; // multiplier is CR+4 = 5..8 - double got = asr650x::airtime_ms(c.len, (float)c.bw, (uint8_t)c.sf, (uint8_t)c.cr, (uint16_t)c.pre); + for (auto &c : cases) { + double ref = ref_airtime_us(c.len, c.bw, c.sf, c.cr, c.pre) / 1000.0; // multiplier is CR+4 = 5..8 + double got = asr650x::airtimeMs(c.len, (float)c.bw, (uint8_t)c.sf, (uint8_t)c.cr, (uint16_t)c.pre); EXPECT_NEAR(ref, got, 1.5) << "len=" << c.len << " bw=" << c.bw << " sf=" << c.sf << " cr=" << c.cr; } - EXPECT_TRUE(asr650x::airtime_ms(0, 62.5f, 8, 5, 32) > 0); // an empty packet still has preamble + header + EXPECT_TRUE(asr650x::airtimeMs(0, 62.5f, 8, 5, 32) > 0); // an empty packet still has preamble + header /* packet score */ - EXPECT_TRUE(asr650x::packet_score(-20.0f, 8, 100) == 0.0f); // below the SF8 threshold (-10 dB) - EXPECT_TRUE(asr650x::packet_score(0.0f, 6, 100) == 0.0f); // unsupported SF - float good = asr650x::packet_score(9.0f, 8, 110); + EXPECT_TRUE(asr650x::packetScore(-20.0f, 8, 100) == 0.0f); // below the SF8 threshold (-10 dB) + EXPECT_TRUE(asr650x::packetScore(0.0f, 6, 100) == 0.0f); // unsupported SF + float good = asr650x::packetScore(9.0f, 8, 110); EXPECT_TRUE(good > 0.0f && good <= 1.0f); - EXPECT_TRUE(asr650x::packet_score(9.0f, 8, 10) > asr650x::packet_score(9.0f, 8, 200)); // longer packets score lower + EXPECT_TRUE(asr650x::packetScore(9.0f, 8, 10) > + asr650x::packetScore(9.0f, 8, 200)); // longer packets score lower } -int main(int argc, char** argv) { +int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } diff --git a/test/test_asr650x_status/test_asr650x_status.cpp b/test/test_asr650x_status/test_asr650x_status.cpp index 4ea4aa1de0..cdb2027bfa 100644 --- a/test/test_asr650x_status/test_asr650x_status.cpp +++ b/test/test_asr650x_status/test_asr650x_status.cpp @@ -1,51 +1,67 @@ -#include +#include "helpers/asr650x/StatusLines.h" + #include #include +#include #include -#include "helpers/asr650x/StatusLines.h" static asr650x::StatusInput base() { asr650x::StatusInput in; std::memset(&in, 0, sizeof(in)); - in.name = "HELTEC-M3"; - in.id4[0] = 0xab; in.id4[1] = 0x26; in.id4[2] = 0x86; in.id4[3] = 0x31; - in.freq_khz = 920250; in.sf = 8; + in.name = "AB02S-NODE"; + in.id4[0] = 0xab; + in.id4[1] = 0x26; + in.id4[2] = 0x86; + in.id4[3] = 0x31; + in.freq_khz = 920250; + in.sf = 8; in.max_contacts = 12; return in; } -static std::string line(int i, const asr650x::StatusInput& in) { char o[22]; asr650x::status_line(i, in, o); return o; } +static std::string line(int i, const asr650x::StatusInput &in) { + char o[22]; + asr650x::statusLine(i, in, o); + return o; +} TEST(Asr650xStatusLines, Behaves) { asr650x::StatusInput in = base(); - EXPECT_TRUE(line(0, in) == "HELTEC-M3"); + EXPECT_TRUE(line(0, in) == "AB02S-NODE"); EXPECT_TRUE(line(1, in) == "ID ab268631"); - EXPECT_TRUE(line(2, in) == "--:--:-- no time"); /* clock never synced */ + EXPECT_TRUE(line(2, in) == "--:--:-- no time"); /* clock never synced */ EXPECT_TRUE(line(3, in) == "GPS off"); EXPECT_TRUE(line(4, in) == "LAT --"); EXPECT_TRUE(line(5, in) == "LON --"); - EXPECT_TRUE(line(6, in) == "BAT -- CT 0/12"); /* battery not measured yet */ + EXPECT_TRUE(line(6, in) == "BAT -- CT 0/12"); /* battery not measured yet */ EXPECT_TRUE(line(7, in) == "920.250MHz SF8"); - in.time_valid = true; in.epoch = 1791117296u; /* 2026-10-04 12:34:56 UTC */ + in.time_valid = true; + in.epoch = 1791117296u; /* 2026-10-04 12:34:56 UTC */ EXPECT_TRUE(line(2, in) == "12:34:56 UTC"); - in.epoch = 1735689600u; /* midnight */ + in.epoch = 1735689600u; /* midnight */ EXPECT_TRUE(line(2, in) == "00:00:00 UTC"); in.epoch = 1735689600u + 86399u; EXPECT_TRUE(line(2, in) == "23:59:59 UTC"); - in.gps_state = 1; in.warming_s = 45; + in.gps_state = 1; + in.warmingS = 45; EXPECT_TRUE(line(3, in) == "GPS search 45s"); - in.warming_s = 100000; /* absurd: still within 21 characters */ + in.warmingS = 100000; /* absurd: still within 21 characters */ EXPECT_TRUE(line(3, in).size() <= 21); - in.gps_state = 2; in.sats = 8; in.pos_valid = true; + in.gps_state = 2; + in.sats = 8; + in.pos_valid = true; EXPECT_TRUE(line(3, in) == "GPS fix 8 sats"); - in.lat_e6 = 21028511; in.lon_e6 = 105804817; + in.lat_e6 = 21028511; + in.lon_e6 = 105804817; EXPECT_TRUE(line(4, in) == "LAT 21.028511"); EXPECT_TRUE(line(5, in) == "LON 105.804817"); - in.lat_e6 = -33860000; in.lon_e6 = -6505620; + in.lat_e6 = -33860000; + in.lon_e6 = -6505620; EXPECT_TRUE(line(4, in) == "LAT -33.860000"); EXPECT_TRUE(line(5, in) == "LON -6.505620"); - in.lat_e6 = -50000; in.lon_e6 = 5; /* -0.05 degrees keeps its sign, tiny values pad */ + in.lat_e6 = -50000; + in.lon_e6 = 5; /* -0.05 degrees keeps its sign, tiny values pad */ EXPECT_TRUE(line(4, in) == "LAT -0.050000"); EXPECT_TRUE(line(5, in) == "LON 0.000005"); in.pos_valid = false; @@ -53,29 +69,35 @@ TEST(Asr650xStatusLines, Behaves) { in.gps_state = 3; EXPECT_TRUE(line(3, in) == "GPS timeout"); - in.batt_mv = 3981; in.contacts = 3; + in.batt_mv = 3981; + in.contacts = 3; EXPECT_TRUE(line(6, in) == "BAT 3.98V CT 3/12"); - in.batt_mv = 5000; in.contacts = 255; in.max_contacts = 255; + in.batt_mv = 5000; + in.contacts = 255; + in.max_contacts = 255; EXPECT_TRUE(line(6, in) == "BAT 5.00V CT 255/255"); in.batt_mv = 65535; EXPECT_TRUE(line(6, in).size() <= 21); - in.freq_khz = 915000; in.sf = 12; + in.freq_khz = 915000; + in.sf = 12; EXPECT_TRUE(line(7, in) == "915.000MHz SF12"); /* long or odd names are cut to 21 characters; control characters and a null name are safe */ in.name = "A-VERY-LONG-NODE-NAME-THAT-DOES-NOT-FIT-ON-THE-SCREEN"; - EXPECT_TRUE(line(0, in) == "A-VERY-LONG-NODE-NAME"); /* exactly 21 characters */ + EXPECT_TRUE(line(0, in) == "A-VERY-LONG-NODE-NAME"); /* exactly 21 characters */ in.name = nullptr; EXPECT_TRUE(line(0, in) == ""); in.name = "tab\there\n"; - for (char c : line(0, in)) EXPECT_TRUE((unsigned char)c >= 0x20 && (unsigned char)c < 0x7f); + for (char c : line(0, in)) + EXPECT_TRUE((unsigned char)c >= 0x20 && (unsigned char)c < 0x7f); /* out-of-range line index gives an empty line */ EXPECT_TRUE(line(8, in) == "" && line(-1, in) == ""); - for (int i = 0; i < 8; i++) EXPECT_TRUE(line(i, in).size() <= 21); + for (int i = 0; i < 8; i++) + EXPECT_TRUE(line(i, in).size() <= 21); } -int main(int argc, char** argv) { +int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } diff --git a/test/test_asr650x_strtod/test_asr650x_strtod.cpp b/test/test_asr650x_strtod/test_asr650x_strtod.cpp index a18b85cb4a..a09473135e 100644 --- a/test/test_asr650x_strtod/test_asr650x_strtod.cpp +++ b/test/test_asr650x_strtod/test_asr650x_strtod.cpp @@ -1,41 +1,43 @@ +#include "helpers/asr650x/SmallStrtod.h" + #include #include -#include "helpers/asr650x/SmallStrtod.h" -// The CLI parses numbers such as radio frequencies ("869.618"), bandwidths ("62.5"), dB values ("-3.5") and factors. +// The CLI parses numbers such as radio frequencies ("869.618"), bandwidths ("62.5"), dB values ("-3.5") and +// factors. TEST(Asr650xStrtod, MatchesLibcOnCliStyleNumbers) { - const char* in[] = {"0", "1", "-1", "869.618", "62.5", "-3.5", "+7", "0.000001", "123456789", "1e3", "2.5E-2", - " 42", "920.25,62.5", ".5", "5.", "-0.0"}; - for (const char* s : in) { + const char *in[] = { "0", "1", "-1", "869.618", "62.5", "-3.5", "+7", "0.000001", + "123456789", "1e3", "2.5E-2", " 42", "920.25,62.5", ".5", "5.", "-0.0" }; + for (const char *s : in) { char *e1, *e2; double want = strtod(s, &e1); - double got = asr650x_strtod(s, &e2); + double got = asr650xStrtod(s, &e2); EXPECT_NEAR(got, want, 1e-9 * (want < 0 ? -want : want) + 1e-12) << s; EXPECT_EQ(e2 - s, e1 - s) << s; } } TEST(Asr650xStrtod, NoDigitsConsumesNothing) { - const char* in[] = {"", "abc", "-", ".", "e5", "+."}; - for (const char* s : in) { - char* e; - EXPECT_EQ(asr650x_strtod(s, &e), 0.0) << s; + const char *in[] = { "", "abc", "-", ".", "e5", "+." }; + for (const char *s : in) { + char *e; + EXPECT_EQ(asr650xStrtod(s, &e), 0.0) << s; EXPECT_EQ(e, s) << s; } } TEST(Asr650xStrtod, ExponentWithoutDigitsIsNotConsumed) { - char* e; - const char* s = "12e"; - EXPECT_EQ(asr650x_strtod(s, &e), 12.0); + char *e; + const char *s = "12e"; + EXPECT_EQ(asr650xStrtod(s, &e), 12.0); EXPECT_EQ(e - s, 2); } TEST(Asr650xStrtod, NullEndPointerIsAllowed) { - EXPECT_EQ(asr650x_strtod("3.25", nullptr), 3.25); + EXPECT_EQ(asr650xStrtod("3.25", nullptr), 3.25); } -int main(int argc, char** argv) { +int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } diff --git a/test/test_config_serializer_noop/test_config_serializer_noop.cpp b/test/test_config_serializer_noop/test_config_serializer_noop.cpp index f901f8ef65..b56cfe88a3 100644 --- a/test/test_config_serializer_noop/test_config_serializer_noop.cpp +++ b/test/test_config_serializer_noop/test_config_serializer_noop.cpp @@ -1,39 +1,57 @@ // Runs only in [env:native_noop_config] (CONFIG_SERIALIZER_NOOP): boards that keep prefs in a binary record. -#include -#include #include "helpers/ConfigSerializer.h" #include "helpers/DynamicConfigSerializer.h" +#include +#include + namespace { class Prefs : public ConfigSerializer { public: char name[16]; - int32_t i32 = 1; int16_t i16 = 2; int8_t i8 = 3; uint32_t u32 = 4; uint16_t u16 = 5; uint8_t u8 = 6; - float f = 7.0f; double d = 8.0; bool b = true; uint8_t blob[4] = {1, 2, 3, 4}; + int32_t i32 = 1; + int16_t i16 = 2; + int8_t i8 = 3; + uint32_t u32 = 4; + uint16_t u16 = 5; + uint8_t u8 = 6; + float f = 7.0f; + double d = 8.0; + bool b = true; + uint8_t blob[4] = { 1, 2, 3, 4 }; void touch() { markDirty(); } + protected: void structure() override { - def("name", name, sizeof(name)); def("blob", blob, sizeof(blob)); - def("i32", i32); def("i16", i16); def("i8", i8); def("u32", u32); def("u16", u16); def("u8", u8); - def("f", f); def("d", d); def("b", b); + def("name", name, sizeof(name)); + def("blob", blob, sizeof(blob)); + def("i32", i32); + def("i16", i16); + def("i8", i8); + def("u32", u32); + def("u16", u16); + def("u8", u8); + def("f", f); + def("d", d); + def("b", b); } }; class OneKey : public KeyValueStore { public: char v[8] = "old"; - bool setByKey(const char* key, const char* value) override { + bool setByKey(const char *key, const char *value) override { if (strcmp(key, "known") != 0) return false; strncpy(v, value, sizeof(v) - 1); return true; } - bool getByKey(const char* key, char* value, size_t max_len) override { + bool getByKey(const char *key, char *value, size_t max_len) override { if (strcmp(key, "known") != 0) return false; strncpy(value, v, max_len); return true; } }; -} +} // namespace TEST(ConfigSerializerNoop, DirtyFlagStillWorks) { Prefs p; @@ -52,15 +70,15 @@ TEST(DynamicConfigSerializerNoop, OnlyTheFallbackStoresValues) { EXPECT_STREQ(fb.v, "new"); EXPECT_TRUE(d.getByKey("known", out, sizeof(out) - 1)); EXPECT_STREQ(out, "new"); - EXPECT_FALSE(d.setByKey("other", "x")); // no local text store in this build + EXPECT_FALSE(d.setByKey("other", "x")); // no local text store in this build EXPECT_FALSE(d.getByKey("other", out, sizeof(out) - 1)); } TEST(DynamicConfigSerializerNoop, HasNoTextBuffer) { - EXPECT_LE(sizeof(DynamicConfigSerializer), 4 * sizeof(void*)); + EXPECT_LE(sizeof(DynamicConfigSerializer), 4 * sizeof(void *)); } -int main(int argc, char** argv) { +int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } diff --git a/test/test_contact_path/test_contact_path.cpp b/test/test_contact_path/test_contact_path.cpp index 421008cb55..f756493314 100644 --- a/test/test_contact_path/test_contact_path.cpp +++ b/test/test_contact_path/test_contact_path.cpp @@ -5,54 +5,59 @@ #include "helpers/ContactPath.h" // encoded path_len: upper 2 bits = hash size - 1, lower 6 bits = hop count -static uint8_t enc(uint8_t hash_size, uint8_t count) { return (uint8_t)(((hash_size - 1) << 6) | count); } +static uint8_t enc(uint8_t hash_size, uint8_t count) { + return (uint8_t)(((hash_size - 1) << 6) | count); +} TEST(ContactPath, ShortPathIsStored) { uint8_t src[64], dst[CONTACT_OUT_PATH_SIZE + 1]; - for (int i = 0; i < 64; i++) src[i] = (uint8_t)(i + 1); - dst[CONTACT_OUT_PATH_SIZE] = 0xAA; // guard byte + for (int i = 0; i < 64; i++) + src[i] = (uint8_t)(i + 1); + dst[CONTACT_OUT_PATH_SIZE] = 0xAA; // guard byte EXPECT_EQ(contactStorePath(dst, src, enc(1, 5)), enc(1, 5)); EXPECT_EQ(memcmp(dst, src, 5), 0); EXPECT_EQ(dst[CONTACT_OUT_PATH_SIZE], 0xAA); } TEST(ContactPath, ExactlyFullIsStored) { - uint8_t src[64] = {0}, dst[CONTACT_OUT_PATH_SIZE + 1]; + uint8_t src[64] = { 0 }, dst[CONTACT_OUT_PATH_SIZE + 1]; dst[CONTACT_OUT_PATH_SIZE] = 0xAA; - EXPECT_EQ(contactStorePath(dst, src, enc(2, 8)), enc(2, 8)); // 8 hops x 2 bytes = 16 + EXPECT_EQ(contactStorePath(dst, src, enc(2, 8)), enc(2, 8)); // 8 hops x 2 bytes = 16 EXPECT_EQ(dst[CONTACT_OUT_PATH_SIZE], 0xAA); } TEST(ContactPath, TooLongBecomesUnknown) { - uint8_t src[64] = {0}, dst[CONTACT_OUT_PATH_SIZE + 1]; + uint8_t src[64] = { 0 }, dst[CONTACT_OUT_PATH_SIZE + 1]; dst[CONTACT_OUT_PATH_SIZE] = 0xAA; - for (uint8_t hops = 17; hops <= 63; hops++) { // 1-byte hashes + for (uint8_t hops = 17; hops <= 63; hops++) { // 1-byte hashes EXPECT_EQ(contactStorePath(dst, src, enc(1, hops)), OUT_PATH_UNKNOWN) << (int)hops; } - EXPECT_EQ(contactStorePath(dst, src, enc(3, 6)), OUT_PATH_UNKNOWN); // 18 bytes + EXPECT_EQ(contactStorePath(dst, src, enc(3, 6)), OUT_PATH_UNKNOWN); // 18 bytes EXPECT_EQ(dst[CONTACT_OUT_PATH_SIZE], 0xAA); } TEST(ContactPath, ReservedHashSizeBecomesUnknown) { - uint8_t src[64] = {0}, dst[CONTACT_OUT_PATH_SIZE + 1]; + uint8_t src[64] = { 0 }, dst[CONTACT_OUT_PATH_SIZE + 1]; dst[CONTACT_OUT_PATH_SIZE] = 0xAA; - EXPECT_EQ(contactStorePath(dst, src, enc(4, 2)), OUT_PATH_UNKNOWN); // hash size 4 is reserved + EXPECT_EQ(contactStorePath(dst, src, enc(4, 2)), OUT_PATH_UNKNOWN); // hash size 4 is reserved EXPECT_EQ(dst[CONTACT_OUT_PATH_SIZE], 0xAA); } TEST(ContactPath, UnknownStaysUnknown) { - uint8_t src[64] = {0}, dst[CONTACT_OUT_PATH_SIZE]; + uint8_t src[64] = { 0 }, dst[CONTACT_OUT_PATH_SIZE]; EXPECT_EQ(contactStorePath(dst, src, OUT_PATH_UNKNOWN), OUT_PATH_UNKNOWN); } TEST(ContactPath, FrameAlwaysGets64Bytes) { uint8_t stored[CONTACT_OUT_PATH_SIZE], frame[65]; - for (int i = 0; i < CONTACT_OUT_PATH_SIZE; i++) stored[i] = (uint8_t)(0x10 + i); + for (int i = 0; i < CONTACT_OUT_PATH_SIZE; i++) + stored[i] = (uint8_t)(0x10 + i); memset(frame, 0xEE, sizeof(frame)); contactWritePath64(frame, stored, enc(1, 4)); EXPECT_EQ(memcmp(frame, stored, 4), 0); - for (int i = 4; i < 64; i++) EXPECT_EQ(frame[i], 0) << i; // nothing stale after the route - EXPECT_EQ(frame[64], 0xEE); // nothing written past 64 + for (int i = 4; i < 64; i++) + EXPECT_EQ(frame[i], 0) << i; // nothing stale after the route + EXPECT_EQ(frame[64], 0xEE); // nothing written past 64 } TEST(ContactPath, FrameForUnknownRouteIsAllZero) { @@ -60,26 +65,29 @@ TEST(ContactPath, FrameForUnknownRouteIsAllZero) { memset(stored, 0x55, sizeof(stored)); memset(frame, 0xEE, sizeof(frame)); contactWritePath64(frame, stored, OUT_PATH_UNKNOWN); - for (int i = 0; i < 64; i++) EXPECT_EQ(frame[i], 0) << i; + for (int i = 0; i < 64; i++) + EXPECT_EQ(frame[i], 0) << i; } // call-site wrappers (small mode): from a 64-byte frame/file field, from a packet route, and back to 64 bytes TEST(ContactPath, WrappersClampInSmallMode) { uint8_t src[64], dst[CONTACT_OUT_PATH_SIZE + 1], out[64]; - for (int i = 0; i < 64; i++) src[i] = (uint8_t)(0x30 + i); + for (int i = 0; i < 64; i++) + src[i] = (uint8_t)(0x30 + i); dst[CONTACT_OUT_PATH_SIZE] = 0xAA; EXPECT_EQ(contactPathFrom64(dst, src, enc(1, 3)), enc(1, 3)); EXPECT_EQ(memcmp(dst, src, 3), 0); EXPECT_EQ(contactPathFrom64(dst, src, enc(1, 40)), OUT_PATH_UNKNOWN); - EXPECT_EQ(contactPathFromPacket(dst, src, enc(2, 9)), OUT_PATH_UNKNOWN); // 18 bytes + EXPECT_EQ(contactPathFromPacket(dst, src, enc(2, 9)), OUT_PATH_UNKNOWN); // 18 bytes EXPECT_EQ(contactPathFromPacket(dst, src, enc(2, 8)), enc(2, 8)); EXPECT_EQ(dst[CONTACT_OUT_PATH_SIZE], 0xAA); contactPathTo64(out, dst, enc(2, 8)); EXPECT_EQ(memcmp(out, src, 16), 0); - for (int i = 16; i < 64; i++) EXPECT_EQ(out[i], 0) << i; + for (int i = 16; i < 64; i++) + EXPECT_EQ(out[i], 0) << i; } -int main(int argc, char** argv) { +int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } diff --git a/test/test_ed25519_vectors/test_ed25519_vectors.cpp b/test/test_ed25519_vectors/test_ed25519_vectors.cpp index c983829017..f62fc50046 100644 --- a/test/test_ed25519_vectors/test_ed25519_vectors.cpp +++ b/test/test_ed25519_vectors/test_ed25519_vectors.cpp @@ -1,14 +1,15 @@ // Ed25519 known-answer tests (RFC 8032 section 7.1, TEST 1 and TEST 2). Run with the full fixed-base table // ([env:native]) and with the small one ([env:native_ed25519_small], -D ED25519_SMALL_BASE_TABLE). +#include "ed_25519.h" + #include #include -#include "ed_25519.h" extern "C" { #include "ge.h" #include "precomp_data.h" } -static void unhex(uint8_t* out, const char* hex) { +static void unhex(uint8_t *out, const char *hex) { for (size_t i = 0; hex[2 * i]; i++) { unsigned v; sscanf(hex + 2 * i, "%2x", &v); @@ -16,21 +17,28 @@ static void unhex(uint8_t* out, const char* hex) { } } -struct Vec { const char *seed, *pub, *msg, *sig; }; +struct Vec { + const char *seed, *pub, *msg, *sig; +}; static const Vec VECTORS[] = { - {"9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", - "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", "", - "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b"}, - {"4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb", - "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c", "72", - "92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00"}, + { "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", + "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", "", + "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0" + "595bbe24655141438e7a100b" }, + { "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb", + "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c", "72", + "92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eae" + "b4302aeeb00d291612bb0c00" }, }; TEST(Ed25519Vectors, KeypairSignVerifyMatchRfc8032) { - for (const Vec& v : VECTORS) { + for (const Vec &v : VECTORS) { uint8_t seed[32], pub_want[32], sig_want[64], msg[8], pub[32], prv[64], sig[64]; size_t msg_len = strlen(v.msg) / 2; - unhex(seed, v.seed); unhex(pub_want, v.pub); unhex(sig_want, v.sig); unhex(msg, v.msg); + unhex(seed, v.seed); + unhex(pub_want, v.pub); + unhex(sig_want, v.sig); + unhex(msg, v.msg); ed25519_create_keypair(pub, prv, seed); EXPECT_EQ(memcmp(pub, pub_want, 32), 0); uint8_t derived[32]; @@ -46,7 +54,8 @@ TEST(Ed25519Vectors, KeypairSignVerifyMatchRfc8032) { TEST(Ed25519Vectors, KeyExchangeIsSymmetric) { uint8_t sa[32], sb[32], pa[32], pb[32], ka[64], kb[64], s1[32], s2[32]; - unhex(sa, VECTORS[0].seed); unhex(sb, VECTORS[1].seed); + unhex(sa, VECTORS[0].seed); + unhex(sb, VECTORS[1].seed); ed25519_create_keypair(pa, ka, sa); ed25519_create_keypair(pb, kb, sb); ed25519_key_exchange(s1, pb, ka); @@ -60,7 +69,7 @@ TEST(Ed25519Vectors, SmallBuildKeepsOnlyOneTableRow) { } #endif -int main(int argc, char** argv) { +int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } diff --git a/test/test_fast_pow/test_fast_pow.cpp b/test/test_fast_pow/test_fast_pow.cpp index d3a48ba11c..4f92b768f6 100644 --- a/test/test_fast_pow/test_fast_pow.cpp +++ b/test/test_fast_pow/test_fast_pow.cpp @@ -1,6 +1,7 @@ +#include "helpers/FastPow.h" + #include #include -#include "helpers/FastPow.h" // calcRxDelay() uses 10^(0.85 - score); score is in [0, 1] in practice, the range below is wider on purpose TEST(FastPow10, WithinOnePercentOfPow) { @@ -20,7 +21,7 @@ TEST(FastPow10, ExactAtIntegers) { // MyMesh::calcRxDelay() uses rx_delay_base^(0.85 - score) with a user-set base TEST(FastPow, AnyBaseWithinOnePercentOfPow) { - const float bases[] = {1.05f, 1.5f, 2.0f, 5.0f, 10.0f, 20.0f}; + const float bases[] = { 1.05f, 1.5f, 2.0f, 5.0f, 10.0f, 20.0f }; for (float b : bases) { for (int i = -200; i <= 200; i++) { float x = i / 100.0f; @@ -29,7 +30,7 @@ TEST(FastPow, AnyBaseWithinOnePercentOfPow) { } } -int main(int argc, char** argv) { +int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } diff --git a/test/test_serial_frame_strict/test_serial_frame_strict.cpp b/test/test_serial_frame_strict/test_serial_frame_strict.cpp index ec401a2f97..a01f0be1f4 100644 --- a/test/test_serial_frame_strict/test_serial_frame_strict.cpp +++ b/test/test_serial_frame_strict/test_serial_frame_strict.cpp @@ -1,9 +1,10 @@ -// ArduinoSerialInterface built with SERIAL_FRAME_STRICT: a stalled (truncated) frame must not swallow the next frame, -// and a frame longer than MAX_FRAME_SIZE must be dropped, not executed truncated. +// ArduinoSerialInterface built with SERIAL_FRAME_STRICT: a stalled (truncated) frame must not swallow the +// next frame, and a frame longer than MAX_FRAME_SIZE must be dropped, not executed truncated. #define SERIAL_FRAME_STRICT +#include "../../src/helpers/ArduinoSerialInterface.cpp" + #include #include -#include "../../src/helpers/ArduinoSerialInterface.cpp" namespace { class FakeSerial : public Stream { @@ -12,21 +13,21 @@ class FakeSerial : public Stream { size_t pos = 0; int available() override { return (int)(in.size() - pos); } int read() override { return pos < in.size() ? in[pos++] : -1; } - void push(const std::vector& b) { in.insert(in.end(), b.begin(), b.end()); } + void push(const std::vector &b) { in.insert(in.end(), b.begin(), b.end()); } }; -std::vector frame(const std::vector& payload) { - std::vector f = {'<', (uint8_t)(payload.size() & 0xFF), (uint8_t)(payload.size() >> 8)}; +std::vector frame(const std::vector &payload) { + std::vector f = { '<', (uint8_t)(payload.size() & 0xFF), (uint8_t)(payload.size() >> 8) }; f.insert(f.end(), payload.begin(), payload.end()); return f; } -} +} // namespace TEST(SerialFrameStrict, CompleteFrameIsDelivered) { FakeSerial s; ArduinoSerialInterface sif; sif.begin(s); - s.push(frame({22, 3})); + s.push(frame({ 22, 3 })); uint8_t out[MAX_FRAME_SIZE]; EXPECT_EQ(sif.checkRecvFrame(out), 2u); EXPECT_EQ(out[0], 22); @@ -38,12 +39,12 @@ TEST(SerialFrameStrict, StalledFrameDoesNotSwallowTheNextOne) { sif.begin(s); uint8_t out[MAX_FRAME_SIZE]; g_mock_millis = 1000; - s.push({'<', 10, 0, 1, 2, 3}); // header says 10 bytes, only 3 arrive + s.push({ '<', 10, 0, 1, 2, 3 }); // header says 10 bytes, only 3 arrive EXPECT_EQ(sif.checkRecvFrame(out), 0u); - g_mock_millis = 1150; // the main loop polls during the silence (nothing buffered): - EXPECT_EQ(sif.checkRecvFrame(out), 0u); // > 100 ms since the last byte -> the partial frame is abandoned + g_mock_millis = 1150; // the main loop polls during the silence (nothing buffered): + EXPECT_EQ(sif.checkRecvFrame(out), 0u); // > 100 ms since the last byte -> the partial frame is abandoned g_mock_millis = 1200; - s.push(frame({22, 3})); + s.push(frame({ 22, 3 })); EXPECT_EQ(sif.checkRecvFrame(out), 2u); EXPECT_EQ(out[0], 22); } @@ -56,9 +57,9 @@ TEST(SerialFrameStrict, OversizeFrameIsDroppedNotExecuted) { std::vector big(MAX_FRAME_SIZE + 20, 0x41); big[0] = 22; s.push(frame(big)); - EXPECT_EQ(sif.checkRecvFrame(out), 0u); // dropped - s.push(frame({5})); - EXPECT_EQ(sif.checkRecvFrame(out), 1u); // and the next frame still parses + EXPECT_EQ(sif.checkRecvFrame(out), 0u); // dropped + s.push(frame({ 5 })); + EXPECT_EQ(sif.checkRecvFrame(out), 1u); // and the next frame still parses EXPECT_EQ(out[0], 5); } @@ -70,16 +71,16 @@ TEST(SerialFrameStrict, BusyDeviceDoesNotDropAQueuedFrame) { sif.begin(s); uint8_t out[MAX_FRAME_SIZE]; g_mock_millis = 5000; - s.push({'<', 4, 0, 22, 1}); // first part read + s.push({ '<', 4, 0, 22, 1 }); // first part read EXPECT_EQ(sif.checkRecvFrame(out), 0u); - s.push({2, 3}); // rest arrived while the loop was busy + s.push({ 2, 3 }); // rest arrived while the loop was busy g_mock_millis = 5300; EXPECT_EQ(sif.checkRecvFrame(out), 4u); EXPECT_EQ(out[0], 22); EXPECT_EQ(out[3], 3); } -int main(int argc, char** argv) { +int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } diff --git a/variants/heltec_cubecell_ab02s/CubeCellBoard.cpp b/variants/heltec_cubecell_ab02s/CubeCellBoard.cpp index d5a88786a6..a015aec5b6 100644 --- a/variants/heltec_cubecell_ab02s/CubeCellBoard.cpp +++ b/variants/heltec_cubecell_ab02s/CubeCellBoard.cpp @@ -1,12 +1,13 @@ -#include "target.h" +#include "CubeCellGPS.h" #include "Oled.h" #include "StatusScreen.h" -#include "CubeCellGPS.h" -#include +#include "target.h" + #include +#include #ifndef BIGSTACK_SIZE - #define BIGSTACK_SIZE 2048 +#define BIGSTACK_SIZE 2048 #endif static OledPanel oled; @@ -16,30 +17,35 @@ static uint32_t last_input_ms = 0; void CubeCellBoard::begin() { // Ed25519 needs more than the fixed 2 KB main stack: it runs on a stack taken from the heap (BigStack.h). // Without it, signing would overflow the main stack, so stop here (the app sees no reply). - if (!asr650x_bigstack_init(BIGSTACK_SIZE)) { - while (true) delay(1000); + if (!asr650xBigstackInit(BIGSTACK_SIZE)) { + while (true) + delay(1000); } bool ok = oled.begin(); sensors.setOledStatus(ok ? "ok" : "none"); screen.begin(&oled); #ifdef ASR650X_STACK_WATCH - asr650x_stack_paint(); + asr650xStackPaint(); #endif } -static void update_status_input(uint32_t now) { +static void updateStatusInput(uint32_t now) { asr650x::StatusInput in; memset(&in, 0, sizeof(in)); - NodePrefs* prefs = the_mesh.getNodePrefs(); - const asr650x::GpsData& g = cubecell_gps.data(); + NodePrefs *prefs = the_mesh.getNodePrefs(); + const asr650x::GpsData &g = cubecell_gps.data(); in.name = prefs->node_name; memcpy(in.id4, the_mesh.self_id.pub_key, 4); in.time_valid = rtc_clock.synced(); in.epoch = rtc_clock.getCurrentTime(); - if (!cubecell_gps.enabled()) in.gps_state = 0; - else if (cubecell_gps.state() == asr650x::GPS_WARMING) in.gps_state = 1; - else if (g.pos_valid) in.gps_state = 2; - else if (cubecell_gps.timed_out()) in.gps_state = 3; + if (!cubecell_gps.enabled()) + in.gps_state = 0; + else if (cubecell_gps.state() == asr650x::GPS_WARMING) + in.gps_state = 1; + else if (g.pos_valid) + in.gps_state = 2; + else if (cubecell_gps.timedOut()) + in.gps_state = 3; in.sats = g.sats; in.pos_valid = g.pos_valid; in.lat_e6 = g.lat_e6; @@ -49,39 +55,45 @@ static void update_status_input(uint32_t now) { in.max_contacts = (uint8_t)(MAX_CONTACTS > 255 ? 255 : MAX_CONTACTS); in.freq_khz = (uint32_t)(prefs->freq * 1000.0f + 0.5f); in.sf = prefs->sf; - in.warming_s = cubecell_gps.warming_s(); - screen.set_input(in); + in.warmingS = cubecell_gps.warmingS(); + screen.setInput(in); } void CubeCellBoard::loop() { uint32_t now = millis(); if (now - last_input_ms >= 1000) { last_input_ms = now; - update_status_input(now); + updateStatusInput(now); } screen.loop(now); } #ifdef ASR650X_STACK_WATCH -extern "C" { extern uint32_t __cy_stack_limit[]; extern uint32_t __cy_stack[]; void* sbrk(int); } +extern "C" { +extern uint32_t __cy_stack_limit[]; +extern uint32_t __cy_stack[]; +void *sbrk(int); +} -void asr650x_stack_paint() { - uint32_t* sp; +void asr650xStackPaint() { + uint32_t *sp; __asm volatile("mov %0, sp" : "=r"(sp)); - for (uint32_t* p = __cy_stack_limit; p < sp - 16; p++) *p = 0xA5A5A5A5u; + for (uint32_t *p = __cy_stack_limit; p < sp - 16; p++) + *p = 0xA5A5A5A5u; } -static uint32_t stack_used() { - uint32_t* p = __cy_stack_limit; - while (p < __cy_stack && *p == 0xA5A5A5A5u) p++; - return (uint32_t)((uint8_t*)__cy_stack - (uint8_t*)p); +static uint32_t stackUsed() { + uint32_t *p = __cy_stack_limit; + while (p < __cy_stack && *p == 0xA5A5A5A5u) + p++; + return (uint32_t)((uint8_t *)__cy_stack - (uint8_t *)p); } -const char* asr650x_watch_string() { +const char *asr650xWatchString() { static char name[64]; extern char _end; - snprintf(name, sizeof(name), "CC stk=%u big=%u heap=%u p=%02x%02x%02x", (unsigned)stack_used(), - (unsigned)asr650x_bigstack_highwater(), (unsigned)((uint32_t)sbrk(0) - (uint32_t)&_end), + snprintf(name, sizeof(name), "CC stk=%u big=%u heap=%u p=%02x%02x%02x", (unsigned)stackUsed(), + (unsigned)asr650xBigstackHighwater(), (unsigned)((uint32_t)sbrk(0) - (uint32_t)&_end), (unsigned)asr650x_boot_pool[0], (unsigned)asr650x_boot_pool[1], (unsigned)asr650x_boot_pool[2]); return name; } diff --git a/variants/heltec_cubecell_ab02s/CubeCellBoard.h b/variants/heltec_cubecell_ab02s/CubeCellBoard.h index b357f39fa8..e1ba247b1c 100644 --- a/variants/heltec_cubecell_ab02s/CubeCellBoard.h +++ b/variants/heltec_cubecell_ab02s/CubeCellBoard.h @@ -7,20 +7,21 @@ extern "C" void CySoftwareReset(void); #ifdef ASR650X_STACK_WATCH -// Measurement builds only (never released): DEVICE_QUERY reports stack/heap high-water marks in the manufacturer field. -void asr650x_stack_paint(); -const char* asr650x_watch_string(); +// Measurement builds only (never released): DEVICE_QUERY reports stack/heap high-water marks in the +// manufacturer field. +void asr650xStackPaint(); +const char *asr650xWatchString(); #endif class CubeCellBoard : public mesh::MainBoard { public: - void begin(); // crypto stack, OLED status screen - void loop() override; // OLED status screen (one page per call) - void attachDynamicPrefs(KeyValueStore*) { } // no board-specific prefs + void begin(); // crypto stack, OLED status screen + void loop() override; // OLED status screen (one page per call) + void attachDynamicPrefs(KeyValueStore *) {} // no board-specific prefs uint16_t getBattMilliVolts() override { return getBatteryVoltage(); } - const char* getManufacturerName() const override { + const char *getManufacturerName() const override { #ifdef ASR650X_STACK_WATCH - return asr650x_watch_string(); + return asr650xWatchString(); #else return "Heltec CubeCell HTCC-AB02S"; #endif diff --git a/variants/heltec_cubecell_ab02s/CubeCellGPS.cpp b/variants/heltec_cubecell_ab02s/CubeCellGPS.cpp index dde3b129c7..033e7ed490 100644 --- a/variants/heltec_cubecell_ab02s/CubeCellGPS.cpp +++ b/variants/heltec_cubecell_ab02s/CubeCellGPS.cpp @@ -1,15 +1,19 @@ #include "CubeCellGPS.h" + #include "target.h" + #include CubeCellGPS cubecell_gps; -static const uint32_t FIX_TIMEOUT_S = 300; // spec: wait at most 5 minutes for a fix +static const uint32_t FIX_TIMEOUT_S = 300; // give up on a fix after 5 minutes (GPS off again) -CubeCellGPS::CubeCellGPS() : _enabled(true), _interval(0), _powered(false), _began(false), _power_on_ms(0), _cfg_sent(0), _uptime_s(0), _uptime_ms_mark(0), _last_tick_ms(0) {} +CubeCellGPS::CubeCellGPS() + : _enabled(true), _interval(0), _powered(false), _began(false), _power_on_ms(0), _cfg_sent(0), + _uptime_s(0), _uptime_ms_mark(0), _last_tick_ms(0) {} uint32_t CubeCellGPS::uptime(uint32_t now_ms) { - uint32_t d = now_ms - _uptime_ms_mark; // unsigned: correct across the millis() wrap + uint32_t d = now_ms - _uptime_ms_mark; // unsigned: correct across the millis() wrap _uptime_s += d / 1000; _uptime_ms_mark += (d / 1000) * 1000; return _uptime_s; @@ -17,15 +21,19 @@ uint32_t CubeCellGPS::uptime(uint32_t now_ms) { void CubeCellGPS::power(bool on, uint32_t now_ms) { if (on == _powered) return; - digitalWrite(GPIO14, on ? LOW : HIGH); // VGPS_Control: LOW = powered + digitalWrite(GPIO14, on ? LOW : HIGH); // VGPS_Control: LOW = powered _powered = on; - if (on) { _power_on_ms = now_ms; _cfg_sent = 0; _nmea.reset_fix(); } + if (on) { + _power_on_ms = now_ms; + _cfg_sent = 0; + _nmea.resetFix(); + } } void CubeCellGPS::begin() { pinMode(GPIO14, OUTPUT); - digitalWrite(GPIO14, HIGH); // off - Serial1.begin(9600); // once + digitalWrite(GPIO14, HIGH); // off + Serial1.begin(9600); // once _began = true; _policy.configure(_enabled, _interval, FIX_TIMEOUT_S); _uptime_ms_mark = millis(); @@ -44,15 +52,15 @@ void CubeCellGPS::request() { if (_enabled && _began) _policy.request(uptime(millis())); } -void CubeCellGPS::on_fix(bool from_rmc) { - const asr650x::GpsData& d = _nmea.data(); +void CubeCellGPS::onFix(bool from_rmc) { + const asr650x::GpsData &d = _nmea.data(); if (d.pos_valid) { sensors.node_lat = d.lat_e6 / 1000000.0; sensors.node_lon = d.lon_e6 / 1000000.0; sensors.node_altitude = d.alt_dm / 10.0; } - if (from_rmc && d.rmc_valid) { // time only from the RMC just parsed, never from a stale one - uint32_t epoch = asr650x::gps_to_epoch(d.date_ddmmyy, d.time_hhmmss); + if (from_rmc && d.rmc_valid) { // time only from the RMC just parsed, never from a stale one + uint32_t epoch = asr650x::gpsToEpoch(d.date_ddmmyy, d.time_hhmmss); if (epoch != 0) { uint32_t now = rtc_clock.getCurrentTime(); uint32_t diff = epoch > now ? epoch - now : now - epoch; @@ -63,29 +71,30 @@ void CubeCellGPS::on_fix(bool from_rmc) { void CubeCellGPS::loop(uint32_t now_ms) { if (!_began) return; - while (Serial1.available() > 0) { // always drain: the UART buffer must not fill up + while (Serial1.available() > 0) { // always drain: the UART buffer must not fill up int c = Serial1.read(); if (c < 0) break; if (!_powered) continue; asr650x::NmeaEvent e = _nmea.feed((uint8_t)c); if (e == asr650x::NMEA_GGA || e == asr650x::NMEA_RMC) { - const asr650x::GpsData& d = _nmea.data(); - if (d.pos_valid) on_fix(e == asr650x::NMEA_RMC); + const asr650x::GpsData &d = _nmea.data(); + if (d.pos_valid) onFix(e == asr650x::NMEA_RMC); } } - // Air530Z: $PCAS03 keeps only GGA and RMC (the Air530 PGKC242 command is ignored by this module). Sent at 1 s - // and again at 3 s after power-on, in case the module was not listening yet the first time. + // Air530Z: $PCAS03 keeps only GGA and RMC (the Air530 PGKC242 command is ignored by this module). Sent at 1 + // s and again at 3 s after power-on, in case the module was not listening yet the first time. if (_powered && _cfg_sent < 2 && now_ms - _power_on_ms >= (_cfg_sent == 0 ? 1000u : 3000u)) { char line[64]; - int n = asr650x::nmea_make(line, sizeof(line), "PCAS03,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0"); - if (n > 0) Serial1.write((const uint8_t*)line, (size_t)n); + int n = asr650x::nmeaMake(line, sizeof(line), "PCAS03,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0"); + if (n > 0) Serial1.write((const uint8_t *)line, (size_t)n); _cfg_sent++; } if (now_ms - _last_tick_ms >= 1000) { _last_tick_ms = now_ms; - const asr650x::GpsData& d = _nmea.data(); - bool fix_ok = _powered && d.pos_valid && d.rmc_valid && asr650x::gps_to_epoch(d.date_ddmmyy, d.time_hhmmss) != 0; + const asr650x::GpsData &d = _nmea.data(); + bool fix_ok = + _powered && d.pos_valid && d.rmc_valid && asr650x::gpsToEpoch(d.date_ddmmyy, d.time_hhmmss) != 0; _policy.tick(uptime(now_ms), fix_ok); - power(_policy.power_wanted(), now_ms); + power(_policy.powerWanted(), now_ms); } } diff --git a/variants/heltec_cubecell_ab02s/CubeCellGPS.h b/variants/heltec_cubecell_ab02s/CubeCellGPS.h index f053d24cf1..483a477a32 100644 --- a/variants/heltec_cubecell_ab02s/CubeCellGPS.h +++ b/variants/heltec_cubecell_ab02s/CubeCellGPS.h @@ -1,10 +1,10 @@ #pragma once -#include -#include #include +#include +#include -// Air530 on Serial1 (9600), power on GPIO14 (LOW = on). Serial1.begin() is called ONCE (each call leaks a 256 B buffer); -// the module is switched with the power pin only. Never prints to Serial (the protocol port). +// Air530 on Serial1 (9600), power on GPIO14 (LOW = on). Serial1.begin() is called ONCE (each call leaks a 256 +// B buffer); the module is switched with the power pin only. Never prints to Serial (the protocol port). class CubeCellGPS { asr650x::NmeaParser _nmea; asr650x::GpsPolicy _policy; @@ -14,11 +14,12 @@ class CubeCellGPS { bool _began; uint32_t _power_on_ms; uint8_t _cfg_sent; - uint32_t _uptime_s, _uptime_ms_mark; // monotonic seconds (wraps at 2^32 s, not at the millis() wrap) + uint32_t _uptime_s, _uptime_ms_mark; // monotonic seconds (wraps at 2^32 s, not at the millis() wrap) uint32_t uptime(uint32_t now_ms); uint32_t _last_tick_ms; void power(bool on, uint32_t now_ms); - void on_fix(bool from_rmc); + void onFix(bool from_rmc); + public: CubeCellGPS(); void begin(); @@ -29,10 +30,10 @@ class CubeCellGPS { uint32_t interval() const { return _interval; } bool powered() const { return _powered; } asr650x::GpsState state() const { return _policy.state(); } - bool timed_out() const { return _policy.timed_out(); } - const asr650x::GpsData& data() const { return _nmea.data(); } - uint32_t sentences_ok() const { return _nmea.sentences_ok(); } - uint32_t warming_s() const { return _policy.warming_for(_uptime_s); } + bool timedOut() const { return _policy.timedOut(); } + const asr650x::GpsData &data() const { return _nmea.data(); } + uint32_t sentencesOk() const { return _nmea.sentencesOk(); } + uint32_t warmingS() const { return _policy.warmingFor(_uptime_s); } }; extern CubeCellGPS cubecell_gps; diff --git a/variants/heltec_cubecell_ab02s/CubeCellSensors.cpp b/variants/heltec_cubecell_ab02s/CubeCellSensors.cpp index a9235baa2a..70cc497a69 100644 --- a/variants/heltec_cubecell_ab02s/CubeCellSensors.cpp +++ b/variants/heltec_cubecell_ab02s/CubeCellSensors.cpp @@ -1,32 +1,56 @@ #include "CubeCellSensors.h" + #include "CubeCellGPS.h" + #include #include #include -bool CubeCellSensors::begin() { cubecell_gps.begin(); return true; } -void CubeCellSensors::loop() { cubecell_gps.loop(millis()); } +bool CubeCellSensors::begin() { + cubecell_gps.begin(); + return true; +} +void CubeCellSensors::loop() { + cubecell_gps.loop(millis()); +} -static const char* const NAMES[7] = {"gps", "gps_interval", "gps_fix", "gps_sats", "gps_power", "gps_sent", "oled"}; +static const char *const NAMES[7] = { "gps", "gps_interval", "gps_fix", "gps_sats", + "gps_power", "gps_sent", "oled" }; -const char* CubeCellSensors::getSettingName(int i) const { return (i >= 0 && i < 7) ? NAMES[i] : NULL; } +const char *CubeCellSensors::getSettingName(int i) const { + return (i >= 0 && i < 7) ? NAMES[i] : NULL; +} -const char* CubeCellSensors::getSettingValue(int i) const { - char* v = const_cast(_val); +const char *CubeCellSensors::getSettingValue(int i) const { + char *v = const_cast(_val); switch (i) { - case 0: snprintf(v, sizeof(_val), "%d", cubecell_gps.enabled() ? 1 : 0); break; - case 1: snprintf(v, sizeof(_val), "%u", (unsigned)cubecell_gps.interval()); break; - case 2: snprintf(v, sizeof(_val), "%d", cubecell_gps.data().pos_valid ? 1 : 0); break; - case 3: snprintf(v, sizeof(_val), "%u", (unsigned)cubecell_gps.data().sats); break; - case 4: snprintf(v, sizeof(_val), "%d", cubecell_gps.powered() ? 1 : 0); break; - case 5: snprintf(v, sizeof(_val), "%u", (unsigned)cubecell_gps.sentences_ok()); break; - case 6: return _oled; - default: return NULL; + case 0: + snprintf(v, sizeof(_val), "%d", cubecell_gps.enabled() ? 1 : 0); + break; + case 1: + snprintf(v, sizeof(_val), "%u", (unsigned)cubecell_gps.interval()); + break; + case 2: + snprintf(v, sizeof(_val), "%d", cubecell_gps.data().pos_valid ? 1 : 0); + break; + case 3: + snprintf(v, sizeof(_val), "%u", (unsigned)cubecell_gps.data().sats); + break; + case 4: + snprintf(v, sizeof(_val), "%d", cubecell_gps.powered() ? 1 : 0); + break; + case 5: + snprintf(v, sizeof(_val), "%u", (unsigned)cubecell_gps.sentencesOk()); + break; + case 6: + return _oled; + default: + return NULL; } return _val; } -bool CubeCellSensors::setSettingValue(const char* name, const char* value) { +bool CubeCellSensors::setSettingValue(const char *name, const char *value) { if (strcmp(name, "gps") == 0) { if (strcmp(value, "0") != 0 && strcmp(value, "1") != 0) return false; cubecell_gps.apply(value[0] == '1', cubecell_gps.interval()); @@ -35,7 +59,7 @@ bool CubeCellSensors::setSettingValue(const char* name, const char* value) { if (strcmp(name, "gps_interval") == 0) { if (!*value) return false; unsigned long v = 0; - for (const char* p = value; *p; p++) { + for (const char *p = value; *p; p++) { if (*p < '0' || *p > '9') return false; v = v * 10 + (unsigned long)(*p - '0'); if (v > 86400UL) return false; @@ -43,5 +67,5 @@ bool CubeCellSensors::setSettingValue(const char* name, const char* value) { cubecell_gps.apply(cubecell_gps.enabled(), (uint32_t)v); return true; } - return false; // read-only or unknown key + return false; // read-only or unknown key } diff --git a/variants/heltec_cubecell_ab02s/CubeCellSensors.h b/variants/heltec_cubecell_ab02s/CubeCellSensors.h index 40707958ca..db83db1c47 100644 --- a/variants/heltec_cubecell_ab02s/CubeCellSensors.h +++ b/variants/heltec_cubecell_ab02s/CubeCellSensors.h @@ -1,18 +1,19 @@ #pragma once #include -// GPS settings exposed through MeshCore's custom-variable commands (CMD_GET/SET_CUSTOM_VAR); read-only keys give -// the hardware tests visibility into GPS and OLED state. +// GPS settings exposed through MeshCore's custom-variable commands (CMD_GET/SET_CUSTOM_VAR); read-only keys +// give the hardware tests visibility into GPS and OLED state. class CubeCellSensors : public SensorManager { char _val[12]; - const char* _oled; + const char *_oled; + public: CubeCellSensors() : _oled("none") { _val[0] = 0; } - void setOledStatus(const char* s) { _oled = s; } + void setOledStatus(const char *s) { _oled = s; } bool begin() override; void loop() override; int getNumSettings() const override { return 7; } - const char* getSettingName(int i) const override; - const char* getSettingValue(int i) const override; - bool setSettingValue(const char* name, const char* value) override; + const char *getSettingName(int i) const override; + const char *getSettingValue(int i) const override; + bool setSettingValue(const char *name, const char *value) override; }; diff --git a/variants/heltec_cubecell_ab02s/Oled.cpp b/variants/heltec_cubecell_ab02s/Oled.cpp index bc4f8c52bb..27a8e2f323 100644 --- a/variants/heltec_cubecell_ab02s/Oled.cpp +++ b/variants/heltec_cubecell_ab02s/Oled.cpp @@ -1,78 +1,81 @@ #include "Oled.h" + #include -#include #include +#include static const uint8_t ADDR = 0x3C; -// Direct use of the PSoC I2C master block (what Wire does underneath): Wire.cpp holds 2 x 128 B buffers per instance, -// which this firmware cannot afford in RAM. Timeout 10 ms per byte, same as the core. +// Direct use of the PSoC I2C master block (what Wire does underneath): Wire.cpp holds 2 x 128 B buffers per +// instance, which this firmware cannot afford in RAM. Timeout 10 ms per byte, same as the core. static const uint32_t I2C_TIMEOUT_MS = 10; -static bool i2c_write(const uint8_t* b, uint8_t n) { +static bool i2cWrite(const uint8_t *b, uint8_t n) { I2C_I2CMasterClearStatus(); if (I2C_I2CMasterSendStart(ADDR, I2C_I2C_WRITE_XFER_MODE, I2C_TIMEOUT_MS) != I2C_I2C_MSTR_NO_ERROR) { I2C_I2CMasterSendStop(I2C_TIMEOUT_MS); return false; } bool ok = true; - for (uint8_t i = 0; i < n && ok; i++) ok = I2C_I2CMasterWriteByte(b[i], I2C_TIMEOUT_MS) == I2C_I2C_MSTR_NO_ERROR; + for (uint8_t i = 0; i < n && ok; i++) + ok = I2C_I2CMasterWriteByte(b[i], I2C_TIMEOUT_MS) == I2C_I2C_MSTR_NO_ERROR; I2C_I2CMasterSendStop(I2C_TIMEOUT_MS); return ok; } bool OledPanel::cmd(uint8_t c) { - const uint8_t b[2] = { 0x80, c }; // control byte: single command - return i2c_write(b, 2); + const uint8_t b[2] = { 0x80, c }; // control byte: single command + return i2cWrite(b, 2); } -bool OledPanel::cmds(const uint8_t* c, uint8_t n) { - for (uint8_t i = 0; i < n; i++) if (!cmd(c[i])) return false; +bool OledPanel::cmds(const uint8_t *c, uint8_t n) { + for (uint8_t i = 0; i < n; i++) + if (!cmd(c[i])) return false; return true; } -bool OledPanel::init_panel() { +bool OledPanel::initPanel() { static const uint8_t seq[] = { - 0xAE, // display off - 0xD5, 0xF0, // clock divide / oscillator - 0xA8, 63, // multiplex 1/64 - 0xD3, 0x00, // display offset - 0x40, // start line 0 - 0x8D, 0x14, // charge pump on - 0x20, 0x00, // horizontal addressing - 0xA1, // segment re-map - 0xC8, // COM scan direction - 0xDA, 0x12, // COM pins (128x64) - 0x81, 0xCF, // contrast - 0xD9, 0xF1, // pre-charge - 0xDB, 0x40, // VCOMH - 0xA4, // resume RAM content - 0xA6, // normal (not inverted) - 0x2E // stop scroll + 0xAE, // display off + 0xD5, 0xF0, // clock divide / oscillator + 0xA8, 63, // multiplex 1/64 + 0xD3, 0x00, // display offset + 0x40, // start line 0 + 0x8D, 0x14, // charge pump on + 0x20, 0x00, // horizontal addressing + 0xA1, // segment re-map + 0xC8, // COM scan direction + 0xDA, 0x12, // COM pins (128x64) + 0x81, 0xCF, // contrast + 0xD9, 0xF1, // pre-charge + 0xDB, 0x40, // VCOMH + 0xA4, // resume RAM content + 0xA6, // normal (not inverted) + 0x2E // stop scroll }; return cmds(seq, sizeof(seq)); } bool OledPanel::begin() { pinMode(Vext, OUTPUT); - digitalWrite(Vext, LOW); // panel power on + digitalWrite(Vext, LOW); // panel power on delay(50); pinMode(GPIO10, OUTPUT); - digitalWrite(GPIO10, LOW); // reset pulse + digitalWrite(GPIO10, LOW); // reset pulse delay(20); digitalWrite(GPIO10, HIGH); delay(20); - if (!_wire) { // once only + if (!_wire) { // once only I2C_SCBCLK_DIV_REG = (uint32_t)(CYDEV_BCLK__HFCLK__HZ / 8 / 400000 / 3) << 8; I2C_Start(); _wire = true; } - if (!i2c_write(0, 0)) { // no acknowledge: panel absent or unpowered + if (!i2cWrite(0, 0)) { // no acknowledge: panel absent or unpowered _present = false; digitalWrite(Vext, HIGH); return false; } - _present = init_panel(); + _present = initPanel(); _on = false; if (!_present) digitalWrite(Vext, HIGH); return _present; @@ -88,27 +91,31 @@ void OledPanel::power(bool on) { delay(10); digitalWrite(GPIO10, HIGH); delay(10); - if (!init_panel()) { _present = false; digitalWrite(Vext, HIGH); return; } - cmd(0xAF); // display on + if (!initPanel()) { + _present = false; + digitalWrite(Vext, HIGH); + return; + } + cmd(0xAF); // display on _on = true; } else { - cmd(0xAE); // display off + cmd(0xAE); // display off cmd(0x8D); - cmd(0x10); // charge pump off + cmd(0x10); // charge pump off digitalWrite(Vext, HIGH); _on = false; } } -bool OledPanel::write_page(uint8_t page, const uint8_t data[128]) { +bool OledPanel::writePage(uint8_t page, const uint8_t data[128]) { if (!_present || !_on || page > 7) return false; - if (!cmd(0x21) || !cmd(0) || !cmd(127)) return false; // column window 0..127 - if (!cmd(0x22) || !cmd(page) || !cmd(page)) return false; // page window + if (!cmd(0x21) || !cmd(0) || !cmd(127)) return false; // column window 0..127 + if (!cmd(0x22) || !cmd(page) || !cmd(page)) return false; // page window uint8_t b[17]; - b[0] = 0x40; // control byte: data + b[0] = 0x40; // control byte: data for (int i = 0; i < 128; i += 16) { memcpy(b + 1, data + i, 16); - if (!i2c_write(b, 17)) return false; + if (!i2cWrite(b, 17)) return false; } return true; } diff --git a/variants/heltec_cubecell_ab02s/Oled.h b/variants/heltec_cubecell_ab02s/Oled.h index 0abd49b727..1f36f200b2 100644 --- a/variants/heltec_cubecell_ab02s/Oled.h +++ b/variants/heltec_cubecell_ab02s/Oled.h @@ -1,20 +1,22 @@ #pragma once #include -// Minimal SSD1306 128x64 driver over I2C (0x3C) with no frame buffer: the caller writes one 128-byte page at a time. -// Vext (P3_2, LOW = on) powers the panel; GPIO10 is its reset. If the panel does not acknowledge, begin() returns false. +// Minimal SSD1306 128x64 driver over I2C (0x3C) with no frame buffer: the caller writes one 128-byte page at +// a time. Vext (P3_2, LOW = on) powers the panel; GPIO10 is its reset. If the panel does not acknowledge, +// begin() returns false. class OledPanel { bool _present; bool _on; bool _wire; bool cmd(uint8_t c); - bool cmds(const uint8_t* c, uint8_t n); - bool init_panel(); + bool cmds(const uint8_t *c, uint8_t n); + bool initPanel(); + public: OledPanel() : _present(false), _on(false), _wire(false) {} - bool begin(); // power the panel, reset it, probe 0x3C, run the init sequence (display off) + bool begin(); // power the panel, reset it, probe 0x3C, run the init sequence (display off) bool present() const { return _present; } - bool is_on() const { return _on; } - void power(bool on); // on: Vext + init + DISPLAYON; off: DISPLAYOFF + charge pump off + Vext off - bool write_page(uint8_t page, const uint8_t data[128]); // false on I2C error + bool isOn() const { return _on; } + void power(bool on); // on: Vext + init + DISPLAYON; off: DISPLAYOFF + charge pump off + Vext off + bool writePage(uint8_t page, const uint8_t data[128]); // false on I2C error }; diff --git a/variants/heltec_cubecell_ab02s/StatusScreen.cpp b/variants/heltec_cubecell_ab02s/StatusScreen.cpp index 39f13e7b7f..0557779e16 100644 --- a/variants/heltec_cubecell_ab02s/StatusScreen.cpp +++ b/variants/heltec_cubecell_ab02s/StatusScreen.cpp @@ -1,17 +1,18 @@ #include "StatusScreen.h" -#include + #include +#include -void StatusScreen::begin(OledPanel* oled) { +void StatusScreen::begin(OledPanel *oled) { _oled = oled; - pinMode(USER_KEY, INPUT); // external 10k pull-up; pressed = LOW - wake(millis()); // visible at boot + pinMode(USER_KEY, INPUT); // external 10k pull-up; pressed = LOW + wake(millis()); // visible at boot } void StatusScreen::wake(uint32_t now_ms) { if (!_oled || !_oled->present()) return; _oled->power(true); - _on = _oled->is_on(); + _on = _oled->isOn(); _on_since = now_ms; _next_page = _on ? 0 : -1; _last_refresh = now_ms; @@ -21,7 +22,7 @@ void StatusScreen::loop(uint32_t now_ms) { if (!_oled || !_oled->present()) return; if (_button.update(digitalRead(USER_KEY) == LOW, now_ms)) wake(now_ms); - if (_on && now_ms - _on_since >= ON_MS) { // auto-off + if (_on && now_ms - _on_since >= ON_MS) { // auto-off _oled->power(false); _on = false; _next_page = -1; @@ -29,9 +30,9 @@ void StatusScreen::loop(uint32_t now_ms) { } if (!_on) return; - if (_next_page < 0 && now_ms - _last_refresh >= 1000) { // start a refresh: battery at most every 10 s + if (_next_page < 0 && now_ms - _last_refresh >= 1000) { // start a refresh: battery at most every 10 s if (now_ms - _last_batt_ms >= 10000 || _batt_mv == 0) { - _batt_mv = getBatteryVoltage(); // drives the shared USER_KEY pin LOW for a moment + _batt_mv = getBatteryVoltage(); // drives the shared USER_KEY pin LOW for a moment _last_batt_ms = now_ms; _button.holdoff(millis(), 50); } @@ -39,11 +40,17 @@ void StatusScreen::loop(uint32_t now_ms) { _next_page = 0; _last_refresh = now_ms; } - if (_next_page >= 0) { // one page per call + if (_next_page >= 0) { // one page per call char line[22]; - asr650x::status_line(_next_page, _in, line); - asr650x::render_line(line, _page); - if (!_oled->write_page((uint8_t)_next_page, _page)) { _error = true; _on = false; _oled->power(false); _next_page = -1; return; } + asr650x::statusLine(_next_page, _in, line); + asr650x::renderLine(line, _page); + if (!_oled->writePage((uint8_t)_next_page, _page)) { + _error = true; + _on = false; + _oled->power(false); + _next_page = -1; + return; + } _next_page = (_next_page == 7) ? -1 : (int8_t)(_next_page + 1); } } diff --git a/variants/heltec_cubecell_ab02s/StatusScreen.h b/variants/heltec_cubecell_ab02s/StatusScreen.h index a5a031a71a..335ff87c45 100644 --- a/variants/heltec_cubecell_ab02s/StatusScreen.h +++ b/variants/heltec_cubecell_ab02s/StatusScreen.h @@ -1,32 +1,39 @@ #pragma once -#include -#include #include "Oled.h" -#include + #include +#include +#include +#include // One status screen. Auto-off after ON_MS, USER key wakes it. Redraws once a second, ONE page per loop() call // (about 3-4 ms of I2C each) so serial frames and radio events are never starved. class StatusScreen { - OledPanel* _oled; + OledPanel *_oled; asr650x::Button _button; asr650x::StatusInput _in; uint8_t _page[128]; bool _on; uint32_t _on_since, _last_refresh; - int8_t _next_page; // -1 = idle, 0..7 = pages still to draw + int8_t _next_page; // -1 = idle, 0..7 = pages still to draw uint32_t _last_batt_ms; uint16_t _batt_mv; bool _error; + public: static const uint32_t ON_MS = 30000; - StatusScreen() : _oled(0), _on(false), _on_since(0), _last_refresh(0), _next_page(-1), _last_batt_ms(0), _batt_mv(0), _error(false) { + StatusScreen() + : _oled(0), _on(false), _on_since(0), _last_refresh(0), _next_page(-1), _last_batt_ms(0), _batt_mv(0), + _error(false) { memset(&_in, 0, sizeof(_in)); } - void begin(OledPanel* oled); - void set_input(const asr650x::StatusInput& in) { _in = in; _in.batt_mv = _batt_mv; } + void begin(OledPanel *oled); + void setInput(const asr650x::StatusInput &in) { + _in = in; + _in.batt_mv = _batt_mv; + } void wake(uint32_t now_ms); void loop(uint32_t now_ms); - bool is_on() const { return _on; } + bool isOn() const { return _on; } bool failed() const { return _error; } }; diff --git a/variants/heltec_cubecell_ab02s/shims/CayenneLPP.h b/variants/heltec_cubecell_ab02s/shims/CayenneLPP.h index 997c201daa..93a689c954 100644 --- a/variants/heltec_cubecell_ab02s/shims/CayenneLPP.h +++ b/variants/heltec_cubecell_ab02s/shims/CayenneLPP.h @@ -7,23 +7,28 @@ class CayenneLPP { uint8_t _buf[64]; uint8_t _cursor; + public: explicit CayenneLPP(uint8_t) : _cursor(0) {} void reset() { _cursor = 0; } uint8_t getSize() const { return _cursor; } - uint8_t* getBuffer() { return _buf; } + uint8_t *getBuffer() { return _buf; } uint8_t addVoltage(uint8_t channel, float volts) { if (_cursor + 4 > sizeof(_buf)) return 0; uint16_t v = (uint16_t)(volts * 100.0f + 0.5f); - _buf[_cursor++] = channel; _buf[_cursor++] = 116; - _buf[_cursor++] = v >> 8; _buf[_cursor++] = v & 0xFF; + _buf[_cursor++] = channel; + _buf[_cursor++] = 116; + _buf[_cursor++] = v >> 8; + _buf[_cursor++] = v & 0xFF; return _cursor; } uint8_t addTemperature(uint8_t channel, float celsius) { if (_cursor + 4 > sizeof(_buf)) return 0; int16_t v = (int16_t)(celsius * 10.0f); - _buf[_cursor++] = channel; _buf[_cursor++] = 103; - _buf[_cursor++] = (uint16_t)v >> 8; _buf[_cursor++] = (uint16_t)v & 0xFF; + _buf[_cursor++] = channel; + _buf[_cursor++] = 103; + _buf[_cursor++] = (uint16_t)v >> 8; + _buf[_cursor++] = (uint16_t)v & 0xFF; return _cursor; } }; diff --git a/variants/heltec_cubecell_ab02s/target.cpp b/variants/heltec_cubecell_ab02s/target.cpp index 87ec29a120..18fba75153 100644 --- a/variants/heltec_cubecell_ab02s/target.cpp +++ b/variants/heltec_cubecell_ab02s/target.cpp @@ -1,6 +1,7 @@ #include "target.h" -#include + #include +#include CubeCellBoard board; CubeCellRadio radio_driver; @@ -8,38 +9,39 @@ CubeCellSensors sensors; CubeCellRTC rtc_clock; bool radio_init() { - radio_driver.init(); // Radio.Init first: getRngSeed() (called next by main.cpp) needs the radio + radio_driver.init(); // Radio.Init first: getRngSeed() (called next by main.cpp) needs the radio return true; } namespace { class SeedRNG : public mesh::RNG { - const uint8_t* _p; + const uint8_t *_p; + public: - explicit SeedRNG(const uint8_t* p) : _p(p) {} - void random(uint8_t* dest, size_t sz) override { memcpy(dest, _p, sz); } + explicit SeedRNG(const uint8_t *p) : _p(p) {} + void random(uint8_t *dest, size_t sz) override { memcpy(dest, _p, sz); } }; // Deterministic, device-unique, NOT secret (derived from the public chip ID): it only exists so the // firmware can answer the app before a key is imported. It is never persisted and never transmits // (TxGate), so the app must import a private key before the node can be used. -void placeholder_seed(uint8_t seed[32]) { +void placeholderSeed(uint8_t seed[32]) { uint64_t id = getID(); uint8_t in[8 + 21]; memcpy(in, &id, 8); memcpy(in + 8, "asr650x-unprovisioned", 21); mesh::Utils::sha256(seed, 32, in, sizeof(in)); } -} +} // namespace mesh::LocalIdentity radio_new_identity() { uint8_t seed[32]; - placeholder_seed(seed); + placeholderSeed(seed); SeedRNG rng(seed); return mesh::LocalIdentity(&rng); } -bool asr650x_identity_is_placeholder(const mesh::Identity& id) { +bool asr650xIdentityIsPlaceholder(const mesh::Identity &id) { mesh::LocalIdentity p = radio_new_identity(); return id.matches(p); } diff --git a/variants/heltec_cubecell_ab02s/target.h b/variants/heltec_cubecell_ab02s/target.h index 6fcaf49291..b92c667641 100644 --- a/variants/heltec_cubecell_ab02s/target.h +++ b/variants/heltec_cubecell_ab02s/target.h @@ -1,18 +1,19 @@ #pragma once -// Heltec CubeCell GPS-6502 (HTCC-AB02S): legacy ASR6502 board (ARM Cortex-M0+, 128 KB flash, 16 KB RAM, fixed 2 KB -// stack), SX1262, Air530Z GPS, 0.96" SSD1306 OLED. Companion over USB/UART only (no BLE, no WiFi). +// Heltec CubeCell GPS-6502 (HTCC-AB02S): legacy ASR6502 board (ARM Cortex-M0+, 128 KB flash, 16 KB RAM, fixed +// 2 KB stack), SX1262, Air530Z GPS, 0.96" SSD1306 OLED. Companion over USB/UART only (no BLE, no WiFi). + +#include "CubeCellBoard.h" +#include "CubeCellSensors.h" #include #include -#include #include +#include #include -#include "CubeCellBoard.h" -#include "CubeCellSensors.h" #ifndef min - #define min(a, b) ((a) < (b) ? (a) : (b)) // the CubeCell core does not provide min() +#define min(a, b) ((a) < (b) ? (a) : (b)) // the CubeCell core does not provide min() #endif extern CubeCellBoard board; @@ -21,6 +22,6 @@ extern CubeCellSensors sensors; extern CubeCellRTC rtc_clock; bool radio_init(); -mesh::LocalIdentity radio_new_identity(); // placeholder derived from the chip ID, NOT random (see target.cpp) -bool asr650x_identity_is_placeholder(const mesh::Identity& id); -extern uint8_t asr650x_boot_pool[4]; // fold of the boot-time Radio.Random() outputs (CubeCellRadio.cpp) +mesh::LocalIdentity radio_new_identity(); // placeholder derived from the chip ID, NOT random (see target.cpp) +bool asr650xIdentityIsPlaceholder(const mesh::Identity &id); +extern uint8_t asr650x_boot_pool[4]; // fold of the boot-time Radio.Random() outputs (CubeCellRadio.cpp) From afb277443de47f057a4b1ab831fd6a467df09aab Mon Sep 17 00:00:00 2001 From: HAN Date: Wed, 7 Oct 2026 07:36:34 +0700 Subject: [PATCH 15/16] build.sh and CI: ASR650x firmware artifacts, new native test envs, AB02S in the PR build check - build.sh copies the .cyacd (CubeCellFlash / bootloader image) and .hex of ASR650X_PLATFORM envs to out/ - run-unit-tests runs native_noop_config and native_ed25519_small as well - pr-build-check builds Heltec_CubeCell_AB02S_companion_radio_usb (128 KB part: catches size regressions) --- .github/workflows/pr-build-check.yml | 2 ++ .github/workflows/run-unit-tests.yml | 2 +- build.sh | 8 +++++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-build-check.yml b/.github/workflows/pr-build-check.yml index cebf0cfe5c..eed78a7d65 100644 --- a/.github/workflows/pr-build-check.yml +++ b/.github/workflows/pr-build-check.yml @@ -46,6 +46,8 @@ jobs: - wio_wm1110_repeater # SX1276 (ESP32) - Tbeam_SX1276_repeater + # ASR650x (CubeCell, 128 KB flash: catches size regressions) + - Heltec_CubeCell_AB02S_companion_radio_usb steps: - name: Clone Repo diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index 5d48f4c67b..88bebf9ee3 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -20,7 +20,7 @@ jobs: uses: ./.github/actions/setup-build-environment - name: Run Unit Tests - run: pio test -e native -e native_kiss_modem -vv + run: pio test -e native -e native_kiss_modem -e native_noop_config -e native_ed25519_small -vv - name: Upload Test Results # Upload test results even if the test step failed. diff --git a/build.sh b/build.sh index 80ff4cdf2d..126d34497c 100755 --- a/build.sh +++ b/build.sh @@ -107,7 +107,7 @@ for section, options in data: for key, value in options: if key == 'build_flags': for flag in value: - match = re.search(r'(ESP32_PLATFORM|NRF52_PLATFORM|STM32_PLATFORM|RP2040_PLATFORM)', flag) + match = re.search(r'(ESP32_PLATFORM|NRF52_PLATFORM|STM32_PLATFORM|RP2040_PLATFORM|ASR650X_PLATFORM)', flag) if match: print(match.group(1)) sys.exit(0) @@ -181,6 +181,12 @@ build_firmware() { cp .pio/build/$1/firmware.uf2 out/${FIRMWARE_FILENAME}.uf2 2>/dev/null || true fi + # for asr650x (CubeCell), copy .cyacd (CubeCellFlash / bootloader image) and .hex to out folder + if [ "$ENV_PLATFORM" == "ASR650X_PLATFORM" ]; then + cp .pio/build/$1/firmware.cyacd out/${FIRMWARE_FILENAME}.cyacd 2>/dev/null || true + cp .pio/build/$1/firmware.hex out/${FIRMWARE_FILENAME}.hex 2>/dev/null || true + fi + } # firmwares containing $1 will be built From a3142ace15b6d89efa0fadf01a4a960bd93e0489 Mon Sep 17 00:00:00 2001 From: HAN Date: Wed, 7 Oct 2026 08:00:05 +0700 Subject: [PATCH 16/16] AB02S: pinned contacts reloaded with lastmod=1 so GET_CONTACTS lists them GET_CONTACTS only sends contacts with lastmod > since (0 for a full listing); pinned contacts were restored with lastmod 0, so they were in the table but never reported to the app. --- examples/companion_radio/FlashDataStore.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/companion_radio/FlashDataStore.cpp b/examples/companion_radio/FlashDataStore.cpp index c67619ab36..f89f3eb4ed 100644 --- a/examples/companion_radio/FlashDataStore.cpp +++ b/examples/companion_radio/FlashDataStore.cpp @@ -63,6 +63,7 @@ void runPins(void *p) { ci.type = s.e[i].type; ci.flags = s.e[i].flags; ci.out_path_len = OUT_PATH_UNKNOWN; // the route is learnt again (flood first) + ci.lastmod = 1; // non-zero: GET_CONTACTS only lists contacts with lastmod > since (0) a->host->onContactLoaded(ci); } return;