mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-05-01 19:12:57 +02:00
Modify radio contact fill logic to use sent OR received messages as recency queue for loadin selection after favorites
This commit is contained in:
@@ -1295,7 +1295,13 @@ async def stop_background_contact_reconciliation() -> None:
|
||||
|
||||
|
||||
async def get_contacts_selected_for_radio_sync() -> list[Contact]:
|
||||
"""Return the contacts that would be loaded onto the radio right now."""
|
||||
"""Return the contacts that would be loaded onto the radio right now.
|
||||
|
||||
Fill order:
|
||||
1. Favorites (up to full capacity)
|
||||
2. Most recently DM-active non-repeaters (sent or received, up to 80% refill target)
|
||||
3. Most recently advertised non-repeaters (up to 80% refill target)
|
||||
"""
|
||||
app_settings = await AppSettingsRepository.get()
|
||||
max_contacts = _effective_radio_capacity(app_settings.max_radio_contacts)
|
||||
refill_target, _full_sync_trigger = _compute_radio_contact_limits(max_contacts)
|
||||
@@ -1315,7 +1321,7 @@ async def get_contacts_selected_for_radio_sync() -> list[Contact]:
|
||||
break
|
||||
|
||||
if len(selected_contacts) < refill_target:
|
||||
for contact in await ContactRepository.get_recently_contacted_non_repeaters(
|
||||
for contact in await ContactRepository.get_recently_dm_active_non_repeaters(
|
||||
limit=max_contacts
|
||||
):
|
||||
key = contact.public_key.lower()
|
||||
@@ -1354,8 +1360,8 @@ async def _sync_contacts_to_radio_inner(mc: MeshCore) -> dict:
|
||||
|
||||
Fill order is:
|
||||
1. Favorite contacts
|
||||
2. Most recently interacted-with non-repeaters
|
||||
3. Most recently advert-heard non-repeaters without interaction history
|
||||
2. Most recently DM-active non-repeaters (sent or received)
|
||||
3. Most recently advert-heard non-repeaters
|
||||
|
||||
Favorite contacts are always reloaded first, up to the configured capacity.
|
||||
Additional non-favorite fill stops at the refill target (80% of capacity).
|
||||
@@ -1489,8 +1495,8 @@ async def sync_recent_contacts_to_radio(force: bool = False, mc: MeshCore | None
|
||||
"""
|
||||
Load contacts to the radio for DM ACK support.
|
||||
|
||||
Fill order is favorites, then recently contacted non-repeaters,
|
||||
then recently advert-heard non-repeaters. Favorites are always reloaded
|
||||
Fill order is favorites, then recently DM-active non-repeaters (sent or
|
||||
received), then recently advert-heard non-repeaters. Favorites are always reloaded
|
||||
up to the configured capacity; additional non-favorite fill stops at the
|
||||
80% refill target.
|
||||
Only runs at most once every CONTACT_SYNC_THROTTLE_SECONDS unless forced.
|
||||
|
||||
@@ -294,6 +294,28 @@ class ContactRepository:
|
||||
rows = await cursor.fetchall()
|
||||
return [ContactRepository._row_to_contact(row) for row in rows]
|
||||
|
||||
@staticmethod
|
||||
async def get_recently_dm_active_non_repeaters(limit: int = 200) -> list[Contact]:
|
||||
"""Get non-repeater contacts with the most recent DM activity (sent or received)."""
|
||||
cursor = await db.conn.execute(
|
||||
"""
|
||||
SELECT c.*
|
||||
FROM contacts c
|
||||
INNER JOIN (
|
||||
SELECT conversation_key, MAX(received_at) AS last_dm
|
||||
FROM messages
|
||||
WHERE type = 'PRIV'
|
||||
GROUP BY conversation_key
|
||||
) m ON c.public_key = m.conversation_key
|
||||
WHERE c.type != 2 AND length(c.public_key) = 64
|
||||
ORDER BY m.last_dm DESC
|
||||
LIMIT ?
|
||||
""",
|
||||
(limit,),
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
return [ContactRepository._row_to_contact(row) for row in rows]
|
||||
|
||||
@staticmethod
|
||||
async def get_recently_advertised_non_repeaters(limit: int = 200) -> list[Contact]:
|
||||
"""Get recently advert-heard non-repeater contacts."""
|
||||
|
||||
@@ -377,14 +377,22 @@ class TestSyncRecentContactsToRadio:
|
||||
assert result["loaded"] == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fills_remaining_slots_with_recently_contacted_then_advertised(self, test_db):
|
||||
"""Fill order is favorites, then recent contacts, then recent adverts."""
|
||||
await _insert_contact(KEY_A, "Alice", last_contacted=100)
|
||||
await _insert_contact(KEY_B, "Bob", last_contacted=2000)
|
||||
await _insert_contact("cc" * 32, "Carol", last_contacted=1000)
|
||||
async def test_fills_remaining_slots_with_dm_active_then_advertised(self, test_db):
|
||||
"""Fill order is favorites, then DM-active contacts, then recent adverts."""
|
||||
await _insert_contact(KEY_A, "Alice")
|
||||
await _insert_contact(KEY_B, "Bob")
|
||||
await _insert_contact("cc" * 32, "Carol")
|
||||
await _insert_contact("dd" * 32, "Dave", last_advert=3000)
|
||||
await _insert_contact("ee" * 32, "Eve", last_advert=2500)
|
||||
|
||||
# Create DM activity for Alice (oldest), Bob (most recent), Carol (middle)
|
||||
for key, ts in [(KEY_A, 100), (KEY_B, 2000), ("cc" * 32, 1000)]:
|
||||
await test_db.conn.execute(
|
||||
"INSERT INTO messages (type, conversation_key, text, received_at) VALUES ('PRIV', ?, 'hi', ?)",
|
||||
(key, ts),
|
||||
)
|
||||
await test_db.conn.commit()
|
||||
|
||||
await AppSettingsRepository.update(max_radio_contacts=5)
|
||||
await ContactRepository.set_favorite(KEY_A, True)
|
||||
|
||||
@@ -401,6 +409,7 @@ class TestSyncRecentContactsToRadio:
|
||||
loaded_keys = [
|
||||
call.args[0]["public_key"] for call in mock_mc.commands.add_contact.call_args_list
|
||||
]
|
||||
# Alice (favorite), then Bob & Carol (DM-active, most recent first), then Dave (advert)
|
||||
assert loaded_keys == [KEY_A, KEY_B, "cc" * 32, "dd" * 32]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -509,8 +518,15 @@ class TestSyncAndOffloadAll:
|
||||
@pytest.mark.asyncio
|
||||
async def test_duplicate_favorite_not_loaded_twice(self, test_db):
|
||||
"""Duplicate favorite entries still load the contact only once."""
|
||||
await _insert_contact(KEY_A, "Alice", last_contacted=2000)
|
||||
await _insert_contact(KEY_B, "Bob", last_contacted=1000)
|
||||
await _insert_contact(KEY_A, "Alice")
|
||||
await _insert_contact(KEY_B, "Bob")
|
||||
|
||||
# Bob has DM activity so he appears in tier 2
|
||||
await test_db.conn.execute(
|
||||
"INSERT INTO messages (type, conversation_key, text, received_at) VALUES ('PRIV', ?, 'hi', 1000)",
|
||||
(KEY_B,),
|
||||
)
|
||||
await test_db.conn.commit()
|
||||
|
||||
await AppSettingsRepository.update(max_radio_contacts=2)
|
||||
await ContactRepository.set_favorite(KEY_A, True)
|
||||
@@ -1862,3 +1878,102 @@ class TestCollectRepeaterTelemetryLpp:
|
||||
await _collect_repeater_telemetry(mc, contact)
|
||||
|
||||
assert "lpp_sensors" not in recorded_data
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_contacts_selected_for_radio_sync — DM-active prioritization
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestContactSelectionDmActive:
|
||||
"""Verify that tier 2 prioritizes contacts with recent DM activity."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_incoming_dm_contact_selected_over_advert_only(self, test_db):
|
||||
"""A contact who sent us a DM should be prioritized over one who only advertised."""
|
||||
from app.radio_sync import get_contacts_selected_for_radio_sync
|
||||
|
||||
# Create two non-repeater contacts
|
||||
dm_sender_key = "aa" * 32
|
||||
advert_only_key = "bb" * 32
|
||||
|
||||
await test_db.conn.execute(
|
||||
"INSERT INTO contacts (public_key, name, type, last_seen, last_advert) VALUES (?, ?, 1, 100, 100)",
|
||||
(dm_sender_key, "DM Sender"),
|
||||
)
|
||||
await test_db.conn.execute(
|
||||
"INSERT INTO contacts (public_key, name, type, last_seen, last_advert) VALUES (?, ?, 1, 200, 200)",
|
||||
(advert_only_key, "Advert Only"),
|
||||
)
|
||||
|
||||
# DM Sender sent us a message (incoming DM)
|
||||
await test_db.conn.execute(
|
||||
"INSERT INTO messages (type, conversation_key, text, received_at) VALUES ('PRIV', ?, 'hello', 300)",
|
||||
(dm_sender_key,),
|
||||
)
|
||||
await test_db.conn.commit()
|
||||
|
||||
with patch(
|
||||
"app.radio_sync.AppSettingsRepository.get",
|
||||
new_callable=AsyncMock,
|
||||
return_value=MagicMock(max_radio_contacts=200, tracked_telemetry_repeaters=[]),
|
||||
):
|
||||
selected = await get_contacts_selected_for_radio_sync()
|
||||
|
||||
keys = [c.public_key for c in selected]
|
||||
assert dm_sender_key in keys
|
||||
assert advert_only_key in keys
|
||||
# DM Sender should come before Advert Only (tier 2 before tier 3)
|
||||
assert keys.index(dm_sender_key) < keys.index(advert_only_key)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_outgoing_dm_contact_also_selected(self, test_db):
|
||||
"""A contact we sent a DM to should also appear via DM-active tier."""
|
||||
from app.radio_sync import get_contacts_selected_for_radio_sync
|
||||
|
||||
contact_key = "cc" * 32
|
||||
await test_db.conn.execute(
|
||||
"INSERT INTO contacts (public_key, name, type) VALUES (?, ?, 1)",
|
||||
(contact_key, "Outgoing Target"),
|
||||
)
|
||||
await test_db.conn.execute(
|
||||
"INSERT INTO messages (type, conversation_key, text, received_at, outgoing) VALUES ('PRIV', ?, 'hey', 300, 1)",
|
||||
(contact_key,),
|
||||
)
|
||||
await test_db.conn.commit()
|
||||
|
||||
with patch(
|
||||
"app.radio_sync.AppSettingsRepository.get",
|
||||
new_callable=AsyncMock,
|
||||
return_value=MagicMock(max_radio_contacts=200, tracked_telemetry_repeaters=[]),
|
||||
):
|
||||
selected = await get_contacts_selected_for_radio_sync()
|
||||
|
||||
keys = [c.public_key for c in selected]
|
||||
assert contact_key in keys
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_repeaters_excluded_from_dm_active_tier(self, test_db):
|
||||
"""Repeater contacts should not appear in tier 2 even with DM activity."""
|
||||
from app.radio_sync import get_contacts_selected_for_radio_sync
|
||||
|
||||
repeater_key = "dd" * 32
|
||||
await test_db.conn.execute(
|
||||
"INSERT INTO contacts (public_key, name, type) VALUES (?, ?, 2)",
|
||||
(repeater_key, "Repeater"),
|
||||
)
|
||||
await test_db.conn.execute(
|
||||
"INSERT INTO messages (type, conversation_key, text, received_at) VALUES ('PRIV', ?, 'cmd', 300)",
|
||||
(repeater_key,),
|
||||
)
|
||||
await test_db.conn.commit()
|
||||
|
||||
with patch(
|
||||
"app.radio_sync.AppSettingsRepository.get",
|
||||
new_callable=AsyncMock,
|
||||
return_value=MagicMock(max_radio_contacts=200, tracked_telemetry_repeaters=[]),
|
||||
):
|
||||
selected = await get_contacts_selected_for_radio_sync()
|
||||
|
||||
keys = [c.public_key for c in selected]
|
||||
assert repeater_key not in keys
|
||||
|
||||
Reference in New Issue
Block a user