2.8: NodeDB shrink, decoupling, and restructuring (#10413)

* 2.8: NodeDB refactor to decouple satellite entries and decrease size

* Regen

* Refactor node mute handling to use dedicated functions for clarity and consistency

* Develop ref

* Fix NodeDB review follow-ups

Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/6b1d6cf6-ed6b-43b6-95cb-8e141757664e

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* Address review validation nits

Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/6b1d6cf6-ed6b-43b6-95cb-8e141757664e

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* Trunk

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Extract legacy NodeDatabase migration

* Fix remaining NodeDB review issues

Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/c76b9a5a-7244-4fbc-9ef0-98091d8caaea

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* Fixes

* Trunk

* Fix latest review compile follow-ups

Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/5198da01-ec4c-4c16-8a09-68b8e6d5d410

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* Fix cppcheck style warnings

Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/e60287ba-4ece-46e0-83d8-a6d89664c0bb

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* Change pointer type for mesh node in set_favorite function

* Change pointer types for mesh node references to const in multiple applets

* Add NodeDB layout v25 documentation and migration guidelines

* Remove tests for uninitialized PacketHistory state due to undefined behavior

* Fix code block formatting in copilot instructions

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Ben Meadors
2026-05-09 15:12:10 -05:00
committed by GitHub
co-authored by GitHub copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Copilot Autofix powered by AI
parent 1bcabb893b
commit 94bb21ecc7
58 changed files with 2605 additions and 643 deletions
+1 -1
View File
@@ -113,7 +113,7 @@ void test_DH25519(void)
void test_PKC(void)
{
uint8_t private_key[32];
meshtastic_UserLite_public_key_t public_key;
meshtastic_NodeInfoLite_public_key_t public_key;
uint8_t expected_shared[32];
uint8_t expected_decrypted[32];
uint8_t radioBytes[128] __attribute__((__aligned__));
+6 -26
View File
@@ -641,30 +641,12 @@ void test_sender_zero_substituted(void)
TEST_ASSERT_TRUE(ph->wasSeenRecently(&p2, false));
}
void test_uninitialized_wasSeenRecently(void)
{
// Simulate uninitialized state — create a PacketHistory that looks uninitialized
// We can't easily make allocation fail, but we can test the initOk guard with a destructed one
PacketHistory h(4);
TEST_ASSERT_TRUE(h.initOk()); // sanity check
h.~PacketHistory();
auto p = makePacket(0x1111, 100);
TEST_ASSERT_FALSE(h.wasSeenRecently(&p));
// Reconstruct in place to allow proper destruction
new (&h) PacketHistory(4);
}
void test_uninitialized_wasRelayer(void)
{
PacketHistory h(4);
h.~PacketHistory();
TEST_ASSERT_FALSE(h.wasRelayer(0xAA, 100, 0x1111));
new (&h) PacketHistory(4);
}
// NOTE: Tests for the !initOk() short-circuit in wasSeenRecently / wasRelayer
// were removed: the only way to drive that branch from a test is to destruct
// the object and then call methods on it, which is UB and trips ASAN under
// `pio test -e coverage`. In practice the guard can only fire if `new[]` throws
// (in which case the constructor doesn't return), so the lost coverage is
// purely defensive.
void test_multiple_instances_independent(void)
{
@@ -819,8 +801,6 @@ void setup()
// Group 10 — Edge Cases
RUN_TEST(test_packet_id_zero_not_stored);
RUN_TEST(test_sender_zero_substituted);
RUN_TEST(test_uninitialized_wasSeenRecently);
RUN_TEST(test_uninitialized_wasRelayer);
RUN_TEST(test_multiple_instances_independent);
// Group 11 — Hash Table Stress
+4 -4
View File
@@ -50,7 +50,7 @@ class MockNodeDB : public NodeDB
hasCachedNode = true;
cachedNodeNum = n;
cachedNode.num = n;
cachedNode.has_user = true;
cachedNode.bitfield |= NODEINFO_BITFIELD_HAS_USER_MASK;
}
private:
@@ -494,9 +494,9 @@ static void test_tm_nodeinfo_directResponse_learnsRequestorNodeInfo(void)
TEST_ASSERT_EQUAL_INT(static_cast<int>(ProcessMessage::STOP), static_cast<int>(result));
TEST_ASSERT_NOT_NULL(requestor);
TEST_ASSERT_TRUE(requestor->has_user);
TEST_ASSERT_EQUAL_STRING("requester-long", requestor->user.long_name);
TEST_ASSERT_EQUAL_STRING("rq", requestor->user.short_name);
TEST_ASSERT_TRUE((requestor->bitfield & NODEINFO_BITFIELD_HAS_USER_MASK) != 0);
TEST_ASSERT_EQUAL_STRING("requester-long", requestor->long_name);
TEST_ASSERT_EQUAL_STRING("rq", requestor->short_name);
TEST_ASSERT_EQUAL_UINT8(request.channel, requestor->channel);
}
+406
View File
@@ -0,0 +1,406 @@
// Tests for src/mesh/TypeConversions.cpp covering:
// - bitfield bit collapse on store + extraction round-trip
// - long_name / short_name truncation at the new max_size:25 / 5 boundaries
// - public_key / hw_model / role pass-through
// - thin vs bundled NodeInfo emission
//
// All exercised via the explicit-args overload of ConvertToNodeInfo so we don't
// touch the global nodeDB pointer (which isn't initialized in this test env).
#include "NodeDB.h"
#include "TestUtil.h"
#include "TypeConversions.h"
#include <cstdio>
#include <cstring>
#include <unity.h>
void setUp(void) {}
void tearDown(void) {}
// ---------- helpers -----------------------------------------------------------
static meshtastic_User makeUser(const char *longName, const char *shortName)
{
meshtastic_User u = meshtastic_User_init_zero;
if (longName)
strncpy(u.long_name, longName, sizeof(u.long_name) - 1);
if (shortName)
strncpy(u.short_name, shortName, sizeof(u.short_name) - 1);
u.hw_model = meshtastic_HardwareModel_TBEAM;
u.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
return u;
}
// ---------- has_user / id / macaddr ------------------------------------------
void test_copy_user_sets_has_user_bit(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
meshtastic_User u = makeUser("Kevin Hester", "kh");
TEST_ASSERT_FALSE((lite.bitfield & NODEINFO_BITFIELD_HAS_USER_MASK) != 0);
TypeConversions::CopyUserToNodeInfoLite(&lite, u);
TEST_ASSERT_TRUE((lite.bitfield & NODEINFO_BITFIELD_HAS_USER_MASK) != 0);
}
void test_convert_to_user_id_derived_from_nodenum(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
lite.num = 0x12345678;
meshtastic_User u = TypeConversions::ConvertToUser(&lite);
TEST_ASSERT_EQUAL_STRING("!12345678", u.id);
}
void test_convert_to_user_zero_fills_macaddr(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
lite.num = 0xCAFEBABE;
meshtastic_User u = TypeConversions::ConvertToUser(&lite);
uint8_t zeros[sizeof(u.macaddr)] = {0};
TEST_ASSERT_EQUAL_MEMORY(zeros, u.macaddr, sizeof(u.macaddr));
}
// ---------- long_name truncation ---------------------------------------------
void test_long_name_short_passes_through(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
meshtastic_User u = makeUser("Kevin Hester", "kh"); // 12 chars, fits
TypeConversions::CopyUserToNodeInfoLite(&lite, u);
TEST_ASSERT_EQUAL_STRING("Kevin Hester", lite.long_name);
}
void test_long_name_exact_24_fits(void)
{
// 24 chars -> stored as 24 chars + NUL inside char[25].
const char *exact24 = "abcdefghijklmnopqrstuvwx";
TEST_ASSERT_EQUAL_INT(24, (int)strlen(exact24));
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
meshtastic_User u = makeUser(exact24, "ex");
TypeConversions::CopyUserToNodeInfoLite(&lite, u);
TEST_ASSERT_EQUAL_STRING(exact24, lite.long_name);
TEST_ASSERT_EQUAL_INT(24, (int)strlen(lite.long_name));
}
void test_long_name_truncates_when_too_long(void)
{
// 33 chars in, must fit in 24 + NUL.
const char *tooLong = "North-County Search & Rescue Base";
TEST_ASSERT_EQUAL_INT(33, (int)strlen(tooLong));
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
meshtastic_User u = makeUser(tooLong, "nc");
TypeConversions::CopyUserToNodeInfoLite(&lite, u);
TEST_ASSERT_EQUAL_INT(24, (int)strlen(lite.long_name));
TEST_ASSERT_EQUAL_STRING_LEN(tooLong, lite.long_name, 24);
TEST_ASSERT_EQUAL_INT('\0', lite.long_name[24]);
}
void test_long_name_round_trip_to_wire(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
meshtastic_User in = makeUser("Mountain Repeater Site E", "mr"); // exactly 24
TypeConversions::CopyUserToNodeInfoLite(&lite, in);
meshtastic_User out = TypeConversions::ConvertToUser(&lite);
TEST_ASSERT_EQUAL_STRING(in.long_name, out.long_name);
}
void test_long_name_truncated_utf8_boundary_sanitized(void)
{
// Suffix the 24th byte with the start of a 4-byte emoji; truncation should
// leave the dangling bytes for sanitizeUtf8 to replace with '?'.
char input[40] = {0};
memset(input, 'a', 22); // 22 ASCII
input[22] = static_cast<char>(0xF0); // emoji lead byte at position 22
input[23] = static_cast<char>(0x9F); // continuation
input[24] = static_cast<char>(0xA4); // continuation - past the cap
input[25] = static_cast<char>(0x96); // continuation - past the cap
input[26] = '\0';
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
meshtastic_User u = makeUser(input, "u8");
TypeConversions::CopyUserToNodeInfoLite(&lite, u);
// 24 bytes survive plus NUL. sanitizeUtf8 should have turned the dangling
// multi-byte head into '?' since its continuation bytes were chopped.
TEST_ASSERT_EQUAL_INT(24, (int)strlen(lite.long_name));
for (int i = 0; i < 22; ++i)
TEST_ASSERT_EQUAL_INT('a', lite.long_name[i]);
// The 4-byte sequence got truncated mid-codepoint; sanitizeUtf8 replaces
// any invalid lead/continuation byte with '?'.
TEST_ASSERT_EQUAL_INT('?', lite.long_name[22]);
TEST_ASSERT_EQUAL_INT('?', lite.long_name[23]);
}
// ---------- short_name truncation --------------------------------------------
void test_short_name_passes_through(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
meshtastic_User u = makeUser("Test", "abcd"); // 4 chars, fits short_name[5]
TypeConversions::CopyUserToNodeInfoLite(&lite, u);
TEST_ASSERT_EQUAL_STRING("abcd", lite.short_name);
}
void test_short_name_truncates_when_too_long(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
meshtastic_User u = makeUser("Test", "abcdefgh");
TypeConversions::CopyUserToNodeInfoLite(&lite, u);
TEST_ASSERT_EQUAL_INT(4, (int)strlen(lite.short_name));
TEST_ASSERT_EQUAL_INT('\0', lite.short_name[4]);
}
// ---------- bitfield collapse + round-trip per bool --------------------------
void test_bitfield_is_licensed_round_trip(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
meshtastic_User in = makeUser("a", "a");
in.is_licensed = true;
TypeConversions::CopyUserToNodeInfoLite(&lite, in);
TEST_ASSERT_TRUE((lite.bitfield & NODEINFO_BITFIELD_IS_LICENSED_MASK) != 0);
meshtastic_User out = TypeConversions::ConvertToUser(&lite);
TEST_ASSERT_TRUE(out.is_licensed);
in.is_licensed = false;
meshtastic_NodeInfoLite lite2 = meshtastic_NodeInfoLite_init_default;
TypeConversions::CopyUserToNodeInfoLite(&lite2, in);
TEST_ASSERT_FALSE((lite2.bitfield & NODEINFO_BITFIELD_IS_LICENSED_MASK) != 0);
meshtastic_User out2 = TypeConversions::ConvertToUser(&lite2);
TEST_ASSERT_FALSE(out2.is_licensed);
}
void test_bitfield_unmessagable_present_and_true(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
meshtastic_User in = makeUser("a", "a");
in.has_is_unmessagable = true;
in.is_unmessagable = true;
TypeConversions::CopyUserToNodeInfoLite(&lite, in);
TEST_ASSERT_TRUE((lite.bitfield & NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK) != 0);
TEST_ASSERT_TRUE((lite.bitfield & NODEINFO_BITFIELD_IS_UNMESSAGABLE_MASK) != 0);
meshtastic_User out = TypeConversions::ConvertToUser(&lite);
TEST_ASSERT_TRUE(out.has_is_unmessagable);
TEST_ASSERT_TRUE(out.is_unmessagable);
}
void test_bitfield_unmessagable_present_but_false(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
meshtastic_User in = makeUser("a", "a");
in.has_is_unmessagable = true;
in.is_unmessagable = false;
TypeConversions::CopyUserToNodeInfoLite(&lite, in);
TEST_ASSERT_TRUE((lite.bitfield & NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK) != 0);
TEST_ASSERT_FALSE((lite.bitfield & NODEINFO_BITFIELD_IS_UNMESSAGABLE_MASK) != 0);
meshtastic_User out = TypeConversions::ConvertToUser(&lite);
TEST_ASSERT_TRUE(out.has_is_unmessagable);
TEST_ASSERT_FALSE(out.is_unmessagable);
}
void test_bitfield_unmessagable_absent(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
meshtastic_User in = makeUser("a", "a");
in.has_is_unmessagable = false;
in.is_unmessagable = true; // explicitly true to make sure absence still wins
TypeConversions::CopyUserToNodeInfoLite(&lite, in);
TEST_ASSERT_FALSE((lite.bitfield & NODEINFO_BITFIELD_HAS_IS_UNMESSAGABLE_MASK) != 0);
TEST_ASSERT_FALSE((lite.bitfield & NODEINFO_BITFIELD_IS_UNMESSAGABLE_MASK) != 0);
meshtastic_User out = TypeConversions::ConvertToUser(&lite);
TEST_ASSERT_FALSE(out.has_is_unmessagable);
TEST_ASSERT_FALSE(out.is_unmessagable);
}
void test_copy_user_preserves_unrelated_bits(void)
{
// Pre-set is_muted and is_key_manually_verified - CopyUserToNodeInfoLite
// must not stomp them.
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
lite.bitfield = NODEINFO_BITFIELD_IS_MUTED_MASK | NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK |
NODEINFO_BITFIELD_VIA_MQTT_MASK | NODEINFO_BITFIELD_IS_FAVORITE_MASK | NODEINFO_BITFIELD_IS_IGNORED_MASK;
meshtastic_User in = makeUser("a", "a");
TypeConversions::CopyUserToNodeInfoLite(&lite, in);
TEST_ASSERT_TRUE((lite.bitfield & NODEINFO_BITFIELD_IS_MUTED_MASK) != 0);
TEST_ASSERT_TRUE((lite.bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) != 0);
TEST_ASSERT_TRUE((lite.bitfield & NODEINFO_BITFIELD_VIA_MQTT_MASK) != 0);
TEST_ASSERT_TRUE((lite.bitfield & NODEINFO_BITFIELD_IS_FAVORITE_MASK) != 0);
TEST_ASSERT_TRUE((lite.bitfield & NODEINFO_BITFIELD_IS_IGNORED_MASK) != 0);
}
void test_bitfield_bits_are_independent(void)
{
// Set just is_licensed, verify only that bit (and HAS_USER) is set.
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
meshtastic_User in = makeUser("a", "a");
in.is_licensed = true;
TypeConversions::CopyUserToNodeInfoLite(&lite, in);
const uint32_t expected = NODEINFO_BITFIELD_HAS_USER_MASK | NODEINFO_BITFIELD_IS_LICENSED_MASK;
TEST_ASSERT_EQUAL_HEX32(expected, lite.bitfield);
}
// ---------- public_key / hw_model / role pass-through ------------------------
void test_public_key_round_trip(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
meshtastic_User in = makeUser("a", "a");
in.public_key.size = 32;
for (int i = 0; i < 32; ++i)
in.public_key.bytes[i] = (uint8_t)(i ^ 0xA5);
TypeConversions::CopyUserToNodeInfoLite(&lite, in);
TEST_ASSERT_EQUAL_INT(32, lite.public_key.size);
TEST_ASSERT_EQUAL_MEMORY(in.public_key.bytes, lite.public_key.bytes, 32);
meshtastic_User out = TypeConversions::ConvertToUser(&lite);
TEST_ASSERT_EQUAL_INT(32, out.public_key.size);
TEST_ASSERT_EQUAL_MEMORY(in.public_key.bytes, out.public_key.bytes, 32);
}
void test_hw_model_and_role_round_trip(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
meshtastic_User in = makeUser("a", "a");
in.hw_model = meshtastic_HardwareModel_HELTEC_V3;
in.role = meshtastic_Config_DeviceConfig_Role_ROUTER;
TypeConversions::CopyUserToNodeInfoLite(&lite, in);
TEST_ASSERT_EQUAL_INT(meshtastic_HardwareModel_HELTEC_V3, lite.hw_model);
TEST_ASSERT_EQUAL_INT(meshtastic_Config_DeviceConfig_Role_ROUTER, lite.role);
meshtastic_User out = TypeConversions::ConvertToUser(&lite);
TEST_ASSERT_EQUAL_INT(meshtastic_HardwareModel_HELTEC_V3, out.hw_model);
TEST_ASSERT_EQUAL_INT(meshtastic_Config_DeviceConfig_Role_ROUTER, out.role);
}
// ---------- ConvertToNodeInfo (3-arg) ----------------------------------------
void test_convert_to_node_info_thin_omits_position_and_metrics(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
lite.num = 0xAA;
meshtastic_NodeInfo info = TypeConversions::ConvertToNodeInfoThin(&lite);
TEST_ASSERT_FALSE(info.has_position);
TEST_ASSERT_FALSE(info.has_device_metrics);
}
void test_convert_to_node_info_3arg_with_position(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
lite.num = 0xAA;
meshtastic_PositionLite pos = meshtastic_PositionLite_init_default;
pos.latitude_i = 374200000;
pos.longitude_i = -1221000000;
pos.altitude = 30;
pos.time = 12345;
meshtastic_NodeInfo info = TypeConversions::ConvertToNodeInfo(&lite, &pos, nullptr);
TEST_ASSERT_TRUE(info.has_position);
TEST_ASSERT_FALSE(info.has_device_metrics);
TEST_ASSERT_EQUAL_INT32(374200000, info.position.latitude_i);
TEST_ASSERT_EQUAL_INT32(-1221000000, info.position.longitude_i);
TEST_ASSERT_EQUAL_INT32(30, info.position.altitude);
TEST_ASSERT_EQUAL_UINT32(12345, info.position.time);
}
void test_convert_to_node_info_3arg_with_metrics(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
lite.num = 0xAA;
meshtastic_DeviceMetrics dm = meshtastic_DeviceMetrics_init_default;
dm.battery_level = 88;
dm.has_battery_level = true;
dm.voltage = 3.71f;
dm.has_voltage = true;
meshtastic_NodeInfo info = TypeConversions::ConvertToNodeInfo(&lite, nullptr, &dm);
TEST_ASSERT_FALSE(info.has_position);
TEST_ASSERT_TRUE(info.has_device_metrics);
TEST_ASSERT_EQUAL_INT(88, info.device_metrics.battery_level);
TEST_ASSERT_TRUE(info.device_metrics.has_battery_level);
TEST_ASSERT_EQUAL_FLOAT(3.71f, info.device_metrics.voltage);
}
void test_convert_to_node_info_3arg_null_inputs(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
lite.num = 0xAA;
meshtastic_NodeInfo info = TypeConversions::ConvertToNodeInfo(&lite, nullptr, nullptr);
TEST_ASSERT_FALSE(info.has_position);
TEST_ASSERT_FALSE(info.has_device_metrics);
}
void test_convert_to_node_info_extracts_bitfield_bools(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
lite.num = 0xBB;
lite.bitfield = NODEINFO_BITFIELD_VIA_MQTT_MASK | NODEINFO_BITFIELD_IS_FAVORITE_MASK | NODEINFO_BITFIELD_IS_IGNORED_MASK |
NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK | NODEINFO_BITFIELD_IS_MUTED_MASK;
meshtastic_NodeInfo info = TypeConversions::ConvertToNodeInfo(&lite, nullptr, nullptr);
TEST_ASSERT_TRUE(info.via_mqtt);
TEST_ASSERT_TRUE(info.is_favorite);
TEST_ASSERT_TRUE(info.is_ignored);
TEST_ASSERT_TRUE(info.is_key_manually_verified);
TEST_ASSERT_TRUE(info.is_muted);
}
void test_convert_to_node_info_extracts_bitfield_bools_none_set(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
lite.num = 0xBB;
lite.bitfield = 0;
meshtastic_NodeInfo info = TypeConversions::ConvertToNodeInfo(&lite, nullptr, nullptr);
TEST_ASSERT_FALSE(info.via_mqtt);
TEST_ASSERT_FALSE(info.is_favorite);
TEST_ASSERT_FALSE(info.is_ignored);
TEST_ASSERT_FALSE(info.is_key_manually_verified);
TEST_ASSERT_FALSE(info.is_muted);
}
void test_convert_to_node_info_user_only_when_has_user_bit_set(void)
{
meshtastic_NodeInfoLite lite = meshtastic_NodeInfoLite_init_default;
lite.num = 0x01;
strcpy(lite.long_name, "Alpha");
strcpy(lite.short_name, "A");
// No HAS_USER bit -> user fields ignored on emit.
meshtastic_NodeInfo info = TypeConversions::ConvertToNodeInfo(&lite, nullptr, nullptr);
TEST_ASSERT_FALSE(info.has_user);
lite.bitfield |= NODEINFO_BITFIELD_HAS_USER_MASK;
meshtastic_NodeInfo info2 = TypeConversions::ConvertToNodeInfo(&lite, nullptr, nullptr);
TEST_ASSERT_TRUE(info2.has_user);
TEST_ASSERT_EQUAL_STRING("Alpha", info2.user.long_name);
TEST_ASSERT_EQUAL_STRING("A", info2.user.short_name);
TEST_ASSERT_EQUAL_STRING("!00000001", info2.user.id);
}
// ---------- entry point -------------------------------------------------------
void setup()
{
delay(10);
delay(2000);
initializeTestEnvironment();
UNITY_BEGIN();
RUN_TEST(test_copy_user_sets_has_user_bit);
RUN_TEST(test_convert_to_user_id_derived_from_nodenum);
RUN_TEST(test_convert_to_user_zero_fills_macaddr);
RUN_TEST(test_long_name_short_passes_through);
RUN_TEST(test_long_name_exact_24_fits);
RUN_TEST(test_long_name_truncates_when_too_long);
RUN_TEST(test_long_name_round_trip_to_wire);
RUN_TEST(test_long_name_truncated_utf8_boundary_sanitized);
RUN_TEST(test_short_name_passes_through);
RUN_TEST(test_short_name_truncates_when_too_long);
RUN_TEST(test_bitfield_is_licensed_round_trip);
RUN_TEST(test_bitfield_unmessagable_present_and_true);
RUN_TEST(test_bitfield_unmessagable_present_but_false);
RUN_TEST(test_bitfield_unmessagable_absent);
RUN_TEST(test_copy_user_preserves_unrelated_bits);
RUN_TEST(test_bitfield_bits_are_independent);
RUN_TEST(test_public_key_round_trip);
RUN_TEST(test_hw_model_and_role_round_trip);
RUN_TEST(test_convert_to_node_info_thin_omits_position_and_metrics);
RUN_TEST(test_convert_to_node_info_3arg_with_position);
RUN_TEST(test_convert_to_node_info_3arg_with_metrics);
RUN_TEST(test_convert_to_node_info_3arg_null_inputs);
RUN_TEST(test_convert_to_node_info_extracts_bitfield_bools);
RUN_TEST(test_convert_to_node_info_extracts_bitfield_bools_none_set);
RUN_TEST(test_convert_to_node_info_user_only_when_has_user_bit_set);
exit(UNITY_END());
}
void loop() {}