diff --git a/src/mesh/mesh_wrapper.cpp b/src/mesh/mesh_wrapper.cpp index 0c1b9e2..d063a75 100644 --- a/src/mesh/mesh_wrapper.cpp +++ b/src/mesh/mesh_wrapper.cpp @@ -386,6 +386,9 @@ int exportContactsFull(ContactInfo* out, int max) { strncpy(out[n].name, c->name, 31); out[n].name[31] = '\0'; out[n].type = c->type; + out[n].has_location = c->has_location; + out[n].latitude = c->latitude; + out[n].longitude = c->longitude; out[n].rssi = c->last_rssi; out[n].last_seen = c->last_seen; n++; diff --git a/src/mesh/mesh_wrapper.h b/src/mesh/mesh_wrapper.h index 66214ef..dc52462 100644 --- a/src/mesh/mesh_wrapper.h +++ b/src/mesh/mesh_wrapper.h @@ -28,6 +28,9 @@ struct MeshMessage { struct ContactInfo { char name[32]; uint8_t type; // ADV_TYPE_* (ADV_TYPE_CHAT=companion, ADV_TYPE_REPEATER, ADV_TYPE_ROOM, etc.) + bool has_location; + float latitude; + float longitude; int rssi; uint32_t last_seen; }; diff --git a/src/mesh/slop_mesh.h b/src/mesh/slop_mesh.h index 36149f7..178c5f0 100644 --- a/src/mesh/slop_mesh.h +++ b/src/mesh/slop_mesh.h @@ -28,6 +28,9 @@ struct SlopContact { uint8_t secret[PUB_KEY_SIZE]; char name[32]; uint8_t type; // ADV_TYPE_* from the advert (CHAT, REPEATER, ROOM, etc.) + bool has_location; // true when the advert included GPS coordinates + float latitude; + float longitude; uint32_t last_seen; // RTC timestamp of last advert int last_rssi; // RSSI of last received packet (dBm) uint8_t out_path_len; // OUT_PATH_UNKNOWN if no known direct path @@ -137,12 +140,24 @@ class SlopMesh : public ::mesh::Mesh { name = fallback; } + auto update_location = [](SlopContact& c, const AdvertDataParser& p) { + c.has_location = p.hasLatLon(); + if (c.has_location) { + c.latitude = (float)p.getLat(); + c.longitude = (float)p.getLon(); + } else { + c.latitude = 0.0f; + c.longitude = 0.0f; + } + }; + // Deduplicate for (int i = 0; i < _nContacts; i++) if (_contacts[i].id.matches(id)) { _contacts[i].last_seen = timestamp; _contacts[i].last_rssi = (int)_radio->getLastRSSI(); _contacts[i].type = parser.getType(); + update_location(_contacts[i], parser); strncpy(_contacts[i].name, name, sizeof(_contacts[i].name) - 1); _contacts[i].name[sizeof(_contacts[i].name) - 1] = '\0'; pushPacketLog(name, _contacts[i].last_rssi, pkt->getSNR(), "ADVERT"); @@ -161,6 +176,7 @@ class SlopMesh : public ::mesh::Mesh { c.last_seen = timestamp; c.last_rssi = (int)_radio->getLastRSSI(); c.type = parser.getType(); + update_location(c, parser); c.out_path_len = OUT_PATH_UNKNOWN; self_id.calcSharedSecret(c.secret, id); strncpy(c.name, name, sizeof(c.name) - 1); @@ -174,6 +190,7 @@ class SlopMesh : public ::mesh::Mesh { c.last_seen = timestamp; c.last_rssi = (int)_radio->getLastRSSI(); c.type = parser.getType(); + update_location(c, parser); self_id.calcSharedSecret(c.secret, id); strncpy(c.name, name, sizeof(c.name) - 1); diff --git a/test/test_mesh_messaging/test_mesh_messaging.cpp b/test/test_mesh_messaging/test_mesh_messaging.cpp index 212604b..574254c 100644 --- a/test/test_mesh_messaging/test_mesh_messaging.cpp +++ b/test/test_mesh_messaging/test_mesh_messaging.cpp @@ -38,6 +38,9 @@ struct TestContact { char name[32]; uint32_t last_seen; int last_rssi; + bool has_location; + float latitude; + float longitude; bool valid; }; @@ -51,16 +54,27 @@ struct ContactList { contacts[i].valid = false; contacts[i].last_seen = 0; contacts[i].name[0] = '\0'; + contacts[i].has_location = false; + contacts[i].latitude = 0.0f; + contacts[i].longitude = 0.0f; } } // Returns true if contact was added/updated, false if dropped bool on_advert(const char* name, uint32_t timestamp, int rssi) { + return on_advert(name, timestamp, rssi, false, 0.0f, 0.0f); + } + + bool on_advert(const char* name, uint32_t timestamp, int rssi, + bool has_location, float lat, float lon) { // Deduplicate for (int i = 0; i < n_contacts; i++) { if (strcmp(contacts[i].name, name) == 0) { contacts[i].last_seen = timestamp; contacts[i].last_rssi = rssi; + contacts[i].has_location = has_location; + contacts[i].latitude = has_location ? lat : 0.0f; + contacts[i].longitude = has_location ? lon : 0.0f; return true; } } @@ -76,6 +90,9 @@ struct ContactList { c.valid = true; c.last_seen = timestamp; c.last_rssi = rssi; + c.has_location = has_location; + c.latitude = has_location ? lat : 0.0f; + c.longitude = has_location ? lon : 0.0f; strncpy(c.name, name, sizeof(c.name) - 1); c.name[sizeof(c.name) - 1] = '\0'; return true; @@ -85,6 +102,9 @@ struct ContactList { c.valid = true; c.last_seen = timestamp; c.last_rssi = rssi; + c.has_location = has_location; + c.latitude = has_location ? lat : 0.0f; + c.longitude = has_location ? lon : 0.0f; strncpy(c.name, name, sizeof(c.name) - 1); c.name[sizeof(c.name) - 1] = '\0'; return true; @@ -466,6 +486,31 @@ TEST_F(ContactEvictionTest, DedupUpdatesLastSeen) { EXPECT_EQ(cl.get(idx)->last_rssi, -80); } +TEST_F(ContactEvictionTest, StoresAdvertLocationWhenPresent) { + EXPECT_TRUE(cl.on_advert("Toronto", 1000, -82, true, 43.6532f, -79.3832f)); + + int idx = cl.find("Toronto"); + ASSERT_GE(idx, 0); + const TestContact* c = cl.get(idx); + ASSERT_NE(c, nullptr); + EXPECT_TRUE(c->has_location); + EXPECT_NEAR(c->latitude, 43.6532f, 0.0001f); + EXPECT_NEAR(c->longitude, -79.3832f, 0.0001f); +} + +TEST_F(ContactEvictionTest, ClearsAdvertLocationWhenMissingOnUpdate) { + cl.on_advert("Mobile", 1000, -82, true, 43.6532f, -79.3832f); + cl.on_advert("Mobile", 2000, -78); + + int idx = cl.find("Mobile"); + ASSERT_GE(idx, 0); + const TestContact* c = cl.get(idx); + ASSERT_NE(c, nullptr); + EXPECT_FALSE(c->has_location); + EXPECT_FLOAT_EQ(c->latitude, 0.0f); + EXPECT_FLOAT_EQ(c->longitude, 0.0f); +} + TEST_F(ContactEvictionTest, FillsToMax) { for (int i = 0; i < MAX_CONTACTS; i++) { char name[16]; diff --git a/test/test_mesh_wrapper/test_mesh_wrapper.cpp b/test/test_mesh_wrapper/test_mesh_wrapper.cpp index 1b0ee8c..eb49120 100644 --- a/test/test_mesh_wrapper/test_mesh_wrapper.cpp +++ b/test/test_mesh_wrapper/test_mesh_wrapper.cpp @@ -140,6 +140,17 @@ TEST_F(MeshWrapperTest, ContactInfoHasTypeField) { EXPECT_EQ(sizeof(ci.type), sizeof(uint8_t)); } +TEST_F(MeshWrapperTest, ContactInfoHasLocationFields) { + slopos::mesh::ContactInfo ci{}; + ci.has_location = true; + ci.latitude = 43.6532f; + ci.longitude = -79.3832f; + + EXPECT_TRUE(ci.has_location); + EXPECT_NEAR(ci.latitude, 43.6532f, 0.0001f); + EXPECT_NEAR(ci.longitude, -79.3832f, 0.0001f); +} + // ── ADV_TYPE constants have expected values ──────────── TEST_F(MeshWrapperTest, AdvTypeConstants) { EXPECT_EQ(ADV_TYPE_NONE, 0);