mirror of
https://github.com/meshtastic/firmware.git
synced 2026-09-16 00:10:11 -04:00
* Implement GeofenceModule for waypoint crossing notifications and integrate with existing modules * Waypoint Applet Initial Support on InkHUD * undo tile change * Update screen when Waypoint shows or dissapears * Merge branch 'develop' into waypoint-geofence * Geofence on InkHUD * Update MapTile.h * Update WaypointStore.cpp * Notifications * remove GF from waypoint screen * Prevent Focus from closing the notifiaction banner * Trunk fix * cleanup * undo merge conflix mistake * Waypoint screen on BaseUI * Focus preserve fix * UI bugs * Allow Inkhud to remove waypoint * Respect Locked Waypoints * Trunk fix * Update WaypointStore.cpp * Use 8-digit hex formatting for waypoint IDs. 0x%x was inconsistent with the repo's own convention (0x%08x for 32-bit IDs, used elsewhere in this file). Fixed here and in two other spots I found with the same issue (WaypointModule.cpp, GeofenceModule.cpp). * Update ExternalNotificationModule.cpp * Reject invalid surrogate codepoints in waypoint icon rendering * Update WaypointModule.cpp * Update WaypointStore.cpp * Update WaypointStore.cpp * Update WaypointStore.cpp * trunk fix * fix warnings * power.h rename to Power.h * Update Power.h * Fix executable bit on bin/lint-ifdef-complexity.sh Lost during a prior merge from develop (Windows checkout doesn't preserve file mode), causing "execve failed: Permission denied" in the Trunk Check Runner CI job. develop has this file at 100755; restoring that here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Update README.md * Clean up waypoint and geofence integration * Minimize waypoint and geofence implementation * removed unnecessary gating * Geofence alert * trunk fix * Update test_main.cpp * Update WaypointStore.cpp --------- Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
277 lines
9.6 KiB
C++
277 lines
9.6 KiB
C++
#include "TestUtil.h"
|
|
#include "modules/GeofenceModule.h"
|
|
#include <unity.h>
|
|
|
|
// These cover pure geofence helpers without device globals or a fake clock.
|
|
// Store and notification plumbing are not covered here.
|
|
|
|
using Crossing = GeofenceModule::Crossing;
|
|
|
|
// 0.001 deg of longitude at the equator is ~111 m; 0.001 deg = 10000 in degrees x 1e-7 units.
|
|
static const int32_t kLon111m = 10000;
|
|
|
|
static void test_insideRadius_centreIsInside()
|
|
{
|
|
TEST_ASSERT_TRUE(GeofenceModule::insideRadius(123456, 654321, 123456, 654321, 10));
|
|
}
|
|
|
|
static void test_insideRadius_withinRadius()
|
|
{
|
|
// ~111 m east of the centre, radius 200 m -> inside.
|
|
TEST_ASSERT_TRUE(GeofenceModule::insideRadius(0, kLon111m, 0, 0, 200));
|
|
}
|
|
|
|
static void test_insideRadius_outsideRadius()
|
|
{
|
|
// ~111 m east of the centre, radius 50 m -> outside.
|
|
TEST_ASSERT_FALSE(GeofenceModule::insideRadius(0, kLon111m, 0, 0, 50));
|
|
}
|
|
|
|
static void test_insideRadius_zeroRadiusNeverInside()
|
|
{
|
|
// radius 0 means "no circle" -> never inside, even exactly at the centre.
|
|
TEST_ASSERT_FALSE(GeofenceModule::insideRadius(123456, 654321, 123456, 654321, 0));
|
|
}
|
|
|
|
static meshtastic_BoundingBox makeBox()
|
|
{
|
|
meshtastic_BoundingBox box = meshtastic_BoundingBox_init_zero;
|
|
box.longitude_west_i = -1000;
|
|
box.latitude_south_i = -2000;
|
|
box.longitude_east_i = 1000;
|
|
box.latitude_north_i = 2000;
|
|
return box;
|
|
}
|
|
|
|
static void test_insideBox_centreInside()
|
|
{
|
|
TEST_ASSERT_TRUE(GeofenceModule::insideBox(0, 0, makeBox()));
|
|
}
|
|
|
|
static void test_insideBox_edgesInclusive()
|
|
{
|
|
meshtastic_BoundingBox box = makeBox();
|
|
TEST_ASSERT_TRUE(GeofenceModule::insideBox(box.latitude_north_i, box.longitude_east_i, box));
|
|
TEST_ASSERT_TRUE(GeofenceModule::insideBox(box.latitude_south_i, box.longitude_west_i, box));
|
|
}
|
|
|
|
static void test_insideBox_outsideLat()
|
|
{
|
|
TEST_ASSERT_FALSE(GeofenceModule::insideBox(2001, 0, makeBox()));
|
|
}
|
|
|
|
static void test_insideBox_outsideLon()
|
|
{
|
|
TEST_ASSERT_FALSE(GeofenceModule::insideBox(0, 1001, makeBox()));
|
|
}
|
|
|
|
static void test_inside_circleOnly()
|
|
{
|
|
meshtastic_Waypoint wp = meshtastic_Waypoint_init_zero;
|
|
wp.latitude_i = 0;
|
|
wp.longitude_i = 0;
|
|
wp.geofence_radius = 200;
|
|
wp.has_bounding_box = false;
|
|
TEST_ASSERT_TRUE(GeofenceModule::inside(wp, 0, kLon111m));
|
|
TEST_ASSERT_FALSE(GeofenceModule::inside(wp, 0, kLon111m * 4)); // ~444 m east
|
|
}
|
|
|
|
static void test_inside_boxOnly()
|
|
{
|
|
meshtastic_Waypoint wp = meshtastic_Waypoint_init_zero;
|
|
wp.geofence_radius = 0;
|
|
wp.has_bounding_box = true;
|
|
wp.bounding_box = makeBox();
|
|
TEST_ASSERT_TRUE(GeofenceModule::inside(wp, 0, 0));
|
|
TEST_ASSERT_FALSE(GeofenceModule::inside(wp, 5000, 0));
|
|
}
|
|
|
|
static void test_inside_eitherShapeCounts()
|
|
{
|
|
// Circle is tiny (point far from centre is outside it) but the box still contains the point.
|
|
meshtastic_Waypoint wp = meshtastic_Waypoint_init_zero;
|
|
wp.geofence_radius = 1; // 1 m circle
|
|
wp.has_bounding_box = true;
|
|
wp.bounding_box = makeBox();
|
|
TEST_ASSERT_TRUE(GeofenceModule::inside(wp, 1500, 0)); // outside circle, inside box
|
|
}
|
|
|
|
static void test_inside_noGeofenceNeverInside()
|
|
{
|
|
meshtastic_Waypoint wp = meshtastic_Waypoint_init_zero;
|
|
wp.geofence_radius = 0;
|
|
wp.has_bounding_box = false;
|
|
TEST_ASSERT_FALSE(GeofenceModule::inside(wp, 0, 0));
|
|
TEST_ASSERT_FALSE(GeofenceModule::hasGeofence(wp));
|
|
}
|
|
|
|
static meshtastic_Waypoint makeNotifyingWaypoint()
|
|
{
|
|
meshtastic_Waypoint wp = meshtastic_Waypoint_init_zero;
|
|
return wp;
|
|
}
|
|
|
|
static void test_shouldTrack_circleWithCentre()
|
|
{
|
|
meshtastic_Waypoint wp = makeNotifyingWaypoint();
|
|
wp.geofence_radius = 100;
|
|
wp.has_latitude_i = true;
|
|
wp.has_longitude_i = true;
|
|
TEST_ASSERT_TRUE(GeofenceModule::shouldTrack(wp, WAYPOINT_NOTIFY_ENTER, 0));
|
|
}
|
|
|
|
static void test_shouldTrack_circleWithoutCentreRejected()
|
|
{
|
|
meshtastic_Waypoint wp = makeNotifyingWaypoint();
|
|
wp.geofence_radius = 100; // circle needs a centre
|
|
wp.has_latitude_i = false;
|
|
wp.has_longitude_i = false;
|
|
TEST_ASSERT_FALSE(GeofenceModule::shouldTrack(wp, WAYPOINT_NOTIFY_ENTER, 0));
|
|
}
|
|
|
|
static void test_shouldTrack_boxOnlyWithoutCentreAccepted()
|
|
{
|
|
// A box-only geofence carries absolute corners, so it needs no waypoint centre.
|
|
meshtastic_Waypoint wp = makeNotifyingWaypoint();
|
|
wp.geofence_radius = 0;
|
|
wp.has_bounding_box = true;
|
|
wp.bounding_box = makeBox();
|
|
wp.has_latitude_i = false;
|
|
wp.has_longitude_i = false;
|
|
TEST_ASSERT_TRUE(GeofenceModule::shouldTrack(wp, WAYPOINT_NOTIFY_ENTER, 0));
|
|
}
|
|
|
|
static void test_shouldTrack_noGeofenceRejected()
|
|
{
|
|
meshtastic_Waypoint wp = makeNotifyingWaypoint(); // notify set, but no geofence shape
|
|
TEST_ASSERT_FALSE(GeofenceModule::shouldTrack(wp, WAYPOINT_NOTIFY_ENTER, 0));
|
|
}
|
|
|
|
static void test_shouldTrack_noLocalPreferencesRejected()
|
|
{
|
|
meshtastic_Waypoint wp = meshtastic_Waypoint_init_zero;
|
|
wp.geofence_radius = 100;
|
|
wp.has_latitude_i = true;
|
|
wp.has_longitude_i = true;
|
|
wp.notify_on_enter = true;
|
|
wp.notify_on_exit = true;
|
|
TEST_ASSERT_FALSE(GeofenceModule::shouldTrack(wp, 0, 0));
|
|
}
|
|
|
|
static void test_shouldTrack_expiredRejectedButLiveAccepted()
|
|
{
|
|
meshtastic_Waypoint wp = makeNotifyingWaypoint();
|
|
wp.geofence_radius = 100;
|
|
wp.has_latitude_i = true;
|
|
wp.has_longitude_i = true;
|
|
wp.expire = 1000;
|
|
TEST_ASSERT_FALSE(GeofenceModule::shouldTrack(wp, WAYPOINT_NOTIFY_ENTER, 2000)); // expire <= now -> expired
|
|
TEST_ASSERT_TRUE(GeofenceModule::shouldTrack(wp, WAYPOINT_NOTIFY_ENTER, 500)); // expire > now -> live
|
|
TEST_ASSERT_TRUE(GeofenceModule::shouldTrack(wp, WAYPOINT_NOTIFY_ENTER, 0)); // no clock -> treat as live
|
|
}
|
|
|
|
static meshtastic_Waypoint makeWireNotificationWaypoint()
|
|
{
|
|
meshtastic_Waypoint wp = meshtastic_Waypoint_init_zero;
|
|
wp.notify_on_enter = true;
|
|
wp.notify_on_exit = true;
|
|
wp.notify_favorites_only = true;
|
|
return wp;
|
|
}
|
|
|
|
static void test_localWaypointInitializesPreferencesFromWireFields()
|
|
{
|
|
const meshtastic_Waypoint wp = makeWireNotificationWaypoint();
|
|
const uint8_t preferences = WaypointStore::mergeNotificationPreferences(true, false, 0, wp);
|
|
TEST_ASSERT_EQUAL_UINT8(WAYPOINT_NOTIFY_ENTER | WAYPOINT_NOTIFY_EXIT | WAYPOINT_NOTIFY_FAVORITES_ONLY, preferences);
|
|
}
|
|
|
|
static void test_newRemoteWaypointIgnoresWirePreferences()
|
|
{
|
|
const meshtastic_Waypoint wp = makeWireNotificationWaypoint();
|
|
TEST_ASSERT_EQUAL_UINT8(0, WaypointStore::mergeNotificationPreferences(false, false, 0, wp));
|
|
}
|
|
|
|
static void test_remoteWaypointUpdatePreservesLocalPreferences()
|
|
{
|
|
meshtastic_Waypoint wp = makeWireNotificationWaypoint();
|
|
wp.notify_on_enter = false;
|
|
const uint8_t existing = WAYPOINT_NOTIFY_ENTER | WAYPOINT_NOTIFY_FAVORITES_ONLY;
|
|
TEST_ASSERT_EQUAL_UINT8(existing, WaypointStore::mergeNotificationPreferences(false, true, existing, wp));
|
|
}
|
|
|
|
static void test_storedWaypoint_clearsWirePreferences()
|
|
{
|
|
meshtastic_Waypoint wp = makeWireNotificationWaypoint();
|
|
WaypointStore::clearWireNotificationPreferences(wp);
|
|
TEST_ASSERT_FALSE(wp.notify_on_enter);
|
|
TEST_ASSERT_FALSE(wp.notify_on_exit);
|
|
TEST_ASSERT_FALSE(wp.notify_favorites_only);
|
|
}
|
|
|
|
static void test_first_sighting_no_notification()
|
|
{
|
|
// First sighting only baselines, regardless of inside state or notify flags.
|
|
TEST_ASSERT_TRUE(GeofenceModule::classify(true, false, true, true, true) == Crossing::None);
|
|
TEST_ASSERT_TRUE(GeofenceModule::classify(true, false, false, true, true) == Crossing::None);
|
|
}
|
|
|
|
static void test_classify_noTransitionNeverNotifies()
|
|
{
|
|
TEST_ASSERT_TRUE(GeofenceModule::classify(false, true, true, true, true) == Crossing::None);
|
|
TEST_ASSERT_TRUE(GeofenceModule::classify(false, false, false, true, true) == Crossing::None);
|
|
}
|
|
|
|
static void test_classify_enterFiresOnlyWhenEnabled()
|
|
{
|
|
TEST_ASSERT_TRUE(GeofenceModule::classify(false, false, true, true, false) == Crossing::Enter);
|
|
TEST_ASSERT_TRUE(GeofenceModule::classify(false, false, true, false, false) == Crossing::None);
|
|
}
|
|
|
|
static void test_classify_exitFiresOnlyWhenEnabled()
|
|
{
|
|
TEST_ASSERT_TRUE(GeofenceModule::classify(false, true, false, false, true) == Crossing::Exit);
|
|
TEST_ASSERT_TRUE(GeofenceModule::classify(false, true, false, false, false) == Crossing::None);
|
|
}
|
|
|
|
void setUp(void) {}
|
|
|
|
void tearDown(void) {}
|
|
|
|
extern "C" {
|
|
void setup()
|
|
{
|
|
initializeTestEnvironment();
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_insideRadius_centreIsInside);
|
|
RUN_TEST(test_insideRadius_withinRadius);
|
|
RUN_TEST(test_insideRadius_outsideRadius);
|
|
RUN_TEST(test_insideRadius_zeroRadiusNeverInside);
|
|
RUN_TEST(test_insideBox_centreInside);
|
|
RUN_TEST(test_insideBox_edgesInclusive);
|
|
RUN_TEST(test_insideBox_outsideLat);
|
|
RUN_TEST(test_insideBox_outsideLon);
|
|
RUN_TEST(test_inside_circleOnly);
|
|
RUN_TEST(test_inside_boxOnly);
|
|
RUN_TEST(test_inside_eitherShapeCounts);
|
|
RUN_TEST(test_inside_noGeofenceNeverInside);
|
|
RUN_TEST(test_shouldTrack_circleWithCentre);
|
|
RUN_TEST(test_shouldTrack_circleWithoutCentreRejected);
|
|
RUN_TEST(test_shouldTrack_boxOnlyWithoutCentreAccepted);
|
|
RUN_TEST(test_shouldTrack_noGeofenceRejected);
|
|
RUN_TEST(test_shouldTrack_noLocalPreferencesRejected);
|
|
RUN_TEST(test_shouldTrack_expiredRejectedButLiveAccepted);
|
|
RUN_TEST(test_localWaypointInitializesPreferencesFromWireFields);
|
|
RUN_TEST(test_newRemoteWaypointIgnoresWirePreferences);
|
|
RUN_TEST(test_remoteWaypointUpdatePreservesLocalPreferences);
|
|
RUN_TEST(test_storedWaypoint_clearsWirePreferences);
|
|
RUN_TEST(test_first_sighting_no_notification);
|
|
RUN_TEST(test_classify_noTransitionNeverNotifies);
|
|
RUN_TEST(test_classify_enterFiresOnlyWhenEnabled);
|
|
RUN_TEST(test_classify_exitFiresOnlyWhenEnabled);
|
|
exit(UNITY_END());
|
|
}
|
|
|
|
void loop() {}
|
|
}
|