mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-10 18:53:03 +02:00
Force auto-advert to respect set intervals
This commit is contained in:
@@ -107,6 +107,13 @@ async def run_migrations(conn: aiosqlite.Connection) -> int:
|
|||||||
await set_version(conn, 10)
|
await set_version(conn, 10)
|
||||||
applied += 1
|
applied += 1
|
||||||
|
|
||||||
|
# Migration 11: Add last_advert_time column to app_settings
|
||||||
|
if version < 11:
|
||||||
|
logger.info("Applying migration 11: add last_advert_time column")
|
||||||
|
await _migrate_011_add_last_advert_time(conn)
|
||||||
|
await set_version(conn, 11)
|
||||||
|
applied += 1
|
||||||
|
|
||||||
if applied > 0:
|
if applied > 0:
|
||||||
logger.info(
|
logger.info(
|
||||||
"Applied %d migration(s), schema now at version %d", applied, await get_version(conn)
|
"Applied %d migration(s), schema now at version %d", applied, await get_version(conn)
|
||||||
@@ -654,3 +661,22 @@ async def _migrate_010_add_advert_interval(conn: aiosqlite.Connection) -> None:
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
await conn.commit()
|
await conn.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def _migrate_011_add_last_advert_time(conn: aiosqlite.Connection) -> None:
|
||||||
|
"""
|
||||||
|
Add last_advert_time column to app_settings table.
|
||||||
|
|
||||||
|
This tracks when the last advertisement was sent, ensuring we never
|
||||||
|
advertise faster than the configured advert_interval.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
await conn.execute("ALTER TABLE app_settings ADD COLUMN last_advert_time INTEGER DEFAULT 0")
|
||||||
|
logger.debug("Added last_advert_time column to app_settings")
|
||||||
|
except aiosqlite.OperationalError as e:
|
||||||
|
if "duplicate column" in str(e).lower():
|
||||||
|
logger.debug("last_advert_time column already exists, skipping")
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
await conn.commit()
|
||||||
|
|||||||
@@ -259,3 +259,7 @@ class AppSettings(BaseModel):
|
|||||||
default=0,
|
default=0,
|
||||||
description="Periodic advertisement interval in seconds (0 = disabled)",
|
description="Periodic advertisement interval in seconds (0 = disabled)",
|
||||||
)
|
)
|
||||||
|
last_advert_time: int = Field(
|
||||||
|
default=0,
|
||||||
|
description="Unix timestamp of last advertisement sent (0 = never)",
|
||||||
|
)
|
||||||
|
|||||||
+48
-36
@@ -336,19 +336,54 @@ async def stop_message_polling():
|
|||||||
logger.info("Stopped periodic message polling")
|
logger.info("Stopped periodic message polling")
|
||||||
|
|
||||||
|
|
||||||
async def send_advertisement() -> bool:
|
async def send_advertisement(force: bool = False) -> bool:
|
||||||
"""Send an advertisement to announce presence on the mesh.
|
"""Send an advertisement to announce presence on the mesh.
|
||||||
|
|
||||||
Returns True if successful, False otherwise.
|
Respects the configured advert_interval - won't send if not enough time
|
||||||
|
has elapsed since the last advertisement, unless force=True.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
force: If True, send immediately regardless of interval.
|
||||||
|
|
||||||
|
Returns True if successful, False otherwise (including if throttled).
|
||||||
"""
|
"""
|
||||||
|
from app.repository import AppSettingsRepository
|
||||||
|
|
||||||
if not radio_manager.is_connected or radio_manager.meshcore is None:
|
if not radio_manager.is_connected or radio_manager.meshcore is None:
|
||||||
logger.debug("Cannot send advertisement: radio not connected")
|
logger.debug("Cannot send advertisement: radio not connected")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Check if enough time has elapsed (unless forced)
|
||||||
|
if not force:
|
||||||
|
settings = await AppSettingsRepository.get()
|
||||||
|
interval = settings.advert_interval
|
||||||
|
last_time = settings.last_advert_time
|
||||||
|
now = int(time.time())
|
||||||
|
|
||||||
|
# If interval is 0, advertising is disabled
|
||||||
|
if interval <= 0:
|
||||||
|
logger.debug("Advertisement skipped: periodic advertising is disabled")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if enough time has passed
|
||||||
|
elapsed = now - last_time
|
||||||
|
if elapsed < interval:
|
||||||
|
remaining = interval - elapsed
|
||||||
|
logger.debug(
|
||||||
|
"Advertisement throttled: %d seconds remaining (interval=%d, elapsed=%d)",
|
||||||
|
remaining,
|
||||||
|
interval,
|
||||||
|
elapsed,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = await radio_manager.meshcore.commands.send_advert(flood=True)
|
result = await radio_manager.meshcore.commands.send_advert(flood=True)
|
||||||
if result.type == EventType.OK:
|
if result.type == EventType.OK:
|
||||||
logger.info("Periodic advertisement sent successfully")
|
# Update last_advert_time in database
|
||||||
|
now = int(time.time())
|
||||||
|
await AppSettingsRepository.update(last_advert_time=now)
|
||||||
|
logger.info("Advertisement sent successfully")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
logger.warning("Failed to send advertisement: %s", result.payload)
|
logger.warning("Failed to send advertisement: %s", result.payload)
|
||||||
@@ -359,50 +394,27 @@ async def send_advertisement() -> bool:
|
|||||||
|
|
||||||
|
|
||||||
async def _periodic_advert_loop():
|
async def _periodic_advert_loop():
|
||||||
"""Background task that periodically sends advertisements.
|
"""Background task that periodically checks if an advertisement should be sent.
|
||||||
|
|
||||||
Reads the interval from app_settings on each iteration, allowing
|
The actual throttling logic is in send_advertisement(), which checks
|
||||||
dynamic configuration changes without restarting the task.
|
last_advert_time from the database. This loop just triggers the check
|
||||||
If interval is 0, advertising is disabled but the task continues
|
periodically and sleeps between attempts.
|
||||||
running to detect when it's re-enabled.
|
|
||||||
"""
|
"""
|
||||||
from app.repository import AppSettingsRepository
|
|
||||||
|
|
||||||
last_advert_time = 0.0
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
# Get current interval setting
|
# Try to send - send_advertisement() handles all checks
|
||||||
settings = await AppSettingsRepository.get()
|
# (disabled, throttled, not connected)
|
||||||
interval = settings.advert_interval
|
if radio_manager.is_connected:
|
||||||
|
await send_advertisement()
|
||||||
|
|
||||||
if interval <= 0:
|
# Sleep before next check
|
||||||
# Advertising disabled - check again later
|
await asyncio.sleep(ADVERT_CHECK_INTERVAL)
|
||||||
await asyncio.sleep(ADVERT_CHECK_INTERVAL)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Check if enough time has passed since last advertisement
|
|
||||||
now = asyncio.get_running_loop().time()
|
|
||||||
time_since_last = now - last_advert_time
|
|
||||||
|
|
||||||
if time_since_last >= interval and radio_manager.is_connected:
|
|
||||||
if await send_advertisement():
|
|
||||||
last_advert_time = now
|
|
||||||
elif time_since_last < interval:
|
|
||||||
# Sleep until next advertisement is due
|
|
||||||
sleep_time = min(interval - time_since_last, ADVERT_CHECK_INTERVAL)
|
|
||||||
await asyncio.sleep(sleep_time)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Sleep for the configured interval
|
|
||||||
await asyncio.sleep(interval)
|
|
||||||
|
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
logger.info("Periodic advertisement task cancelled")
|
logger.info("Periodic advertisement task cancelled")
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("Error in periodic advertisement loop: %s", e)
|
logger.error("Error in periodic advertisement loop: %s", e)
|
||||||
# Sleep a bit before retrying on error
|
|
||||||
await asyncio.sleep(ADVERT_CHECK_INTERVAL)
|
await asyncio.sleep(ADVERT_CHECK_INTERVAL)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -682,7 +682,7 @@ class AppSettingsRepository:
|
|||||||
"""
|
"""
|
||||||
SELECT max_radio_contacts, favorites, auto_decrypt_dm_on_advert,
|
SELECT max_radio_contacts, favorites, auto_decrypt_dm_on_advert,
|
||||||
sidebar_sort_order, last_message_times, preferences_migrated,
|
sidebar_sort_order, last_message_times, preferences_migrated,
|
||||||
advert_interval
|
advert_interval, last_advert_time
|
||||||
FROM app_settings WHERE id = 1
|
FROM app_settings WHERE id = 1
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
@@ -731,6 +731,7 @@ class AppSettingsRepository:
|
|||||||
last_message_times=last_message_times,
|
last_message_times=last_message_times,
|
||||||
preferences_migrated=bool(row["preferences_migrated"]),
|
preferences_migrated=bool(row["preferences_migrated"]),
|
||||||
advert_interval=row["advert_interval"] or 0,
|
advert_interval=row["advert_interval"] or 0,
|
||||||
|
last_advert_time=row["last_advert_time"] or 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -742,6 +743,7 @@ class AppSettingsRepository:
|
|||||||
last_message_times: dict[str, int] | None = None,
|
last_message_times: dict[str, int] | None = None,
|
||||||
preferences_migrated: bool | None = None,
|
preferences_migrated: bool | None = None,
|
||||||
advert_interval: int | None = None,
|
advert_interval: int | None = None,
|
||||||
|
last_advert_time: int | None = None,
|
||||||
) -> AppSettings:
|
) -> AppSettings:
|
||||||
"""Update app settings. Only provided fields are updated."""
|
"""Update app settings. Only provided fields are updated."""
|
||||||
updates = []
|
updates = []
|
||||||
@@ -776,6 +778,10 @@ class AppSettingsRepository:
|
|||||||
updates.append("advert_interval = ?")
|
updates.append("advert_interval = ?")
|
||||||
params.append(advert_interval)
|
params.append(advert_interval)
|
||||||
|
|
||||||
|
if last_advert_time is not None:
|
||||||
|
updates.append("last_advert_time = ?")
|
||||||
|
params.append(last_advert_time)
|
||||||
|
|
||||||
if updates:
|
if updates:
|
||||||
query = f"UPDATE app_settings SET {', '.join(updates)} WHERE id = 1"
|
query = f"UPDATE app_settings SET {', '.join(updates)} WHERE id = 1"
|
||||||
await db.conn.execute(query, params)
|
await db.conn.execute(query, params)
|
||||||
|
|||||||
+18
-7
@@ -5,6 +5,7 @@ from meshcore import EventType
|
|||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from app.dependencies import require_connected
|
from app.dependencies import require_connected
|
||||||
|
from app.radio_sync import send_advertisement as do_send_advertisement
|
||||||
from app.radio_sync import sync_radio_time
|
from app.radio_sync import sync_radio_time
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -129,16 +130,26 @@ async def set_private_key(update: PrivateKeyUpdate) -> dict:
|
|||||||
|
|
||||||
@router.post("/advertise")
|
@router.post("/advertise")
|
||||||
async def send_advertisement(flood: bool = True) -> dict:
|
async def send_advertisement(flood: bool = True) -> dict:
|
||||||
"""Send a radio advertisement to announce presence on the mesh."""
|
"""Send a radio advertisement to announce presence on the mesh.
|
||||||
mc = require_connected()
|
|
||||||
|
|
||||||
|
Manual advertisement requests always send immediately, updating the
|
||||||
|
last_advert_time which affects when the next periodic/startup advert
|
||||||
|
can occur.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
flood: Whether to flood the advertisement (default True).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
status: "ok" if sent successfully
|
||||||
|
"""
|
||||||
|
require_connected()
|
||||||
|
|
||||||
|
# Manual requests always send (force=True), but still update last_advert_time
|
||||||
logger.info("Sending advertisement (flood=%s)", flood)
|
logger.info("Sending advertisement (flood=%s)", flood)
|
||||||
result = await mc.commands.send_advert(flood=flood)
|
success = await do_send_advertisement(force=True)
|
||||||
|
|
||||||
if result.type == EventType.ERROR:
|
if not success:
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=500, detail="Failed to send advertisement")
|
||||||
status_code=500, detail=f"Failed to send advertisement: {result.payload}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return {"status": "ok", "flood": flood}
|
return {"status": "ok", "flood": flood}
|
||||||
|
|
||||||
|
|||||||
@@ -100,8 +100,8 @@ class TestMigration001:
|
|||||||
# Run migrations
|
# Run migrations
|
||||||
applied = await run_migrations(conn)
|
applied = await run_migrations(conn)
|
||||||
|
|
||||||
assert applied == 10 # All 10 migrations run
|
assert applied == 11 # All 11 migrations run
|
||||||
assert await get_version(conn) == 10
|
assert await get_version(conn) == 11
|
||||||
|
|
||||||
# Verify columns exist by inserting and selecting
|
# Verify columns exist by inserting and selecting
|
||||||
await conn.execute(
|
await conn.execute(
|
||||||
@@ -183,9 +183,9 @@ class TestMigration001:
|
|||||||
applied1 = await run_migrations(conn)
|
applied1 = await run_migrations(conn)
|
||||||
applied2 = await run_migrations(conn)
|
applied2 = await run_migrations(conn)
|
||||||
|
|
||||||
assert applied1 == 10 # All 10 migrations run
|
assert applied1 == 11 # All 11 migrations run
|
||||||
assert applied2 == 0 # No migrations on second run
|
assert applied2 == 0 # No migrations on second run
|
||||||
assert await get_version(conn) == 10
|
assert await get_version(conn) == 11
|
||||||
finally:
|
finally:
|
||||||
await conn.close()
|
await conn.close()
|
||||||
|
|
||||||
@@ -245,9 +245,9 @@ class TestMigration001:
|
|||||||
# Run migrations - should not fail
|
# Run migrations - should not fail
|
||||||
applied = await run_migrations(conn)
|
applied = await run_migrations(conn)
|
||||||
|
|
||||||
# All 10 migrations applied (version incremented) but no error
|
# All 11 migrations applied (version incremented) but no error
|
||||||
assert applied == 10
|
assert applied == 11
|
||||||
assert await get_version(conn) == 10
|
assert await get_version(conn) == 11
|
||||||
finally:
|
finally:
|
||||||
await conn.close()
|
await conn.close()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user