mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-07 09:13:04 +02:00
Make advert interval manual
This commit is contained in:
@@ -100,6 +100,13 @@ async def run_migrations(conn: aiosqlite.Connection) -> int:
|
|||||||
await set_version(conn, 9)
|
await set_version(conn, 9)
|
||||||
applied += 1
|
applied += 1
|
||||||
|
|
||||||
|
# Migration 10: Add advert_interval column to app_settings
|
||||||
|
if version < 10:
|
||||||
|
logger.info("Applying migration 10: add advert_interval column")
|
||||||
|
await _migrate_010_add_advert_interval(conn)
|
||||||
|
await set_version(conn, 10)
|
||||||
|
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)
|
||||||
@@ -629,3 +636,21 @@ async def _migrate_009_create_app_settings_table(conn: aiosqlite.Connection) ->
|
|||||||
|
|
||||||
await conn.commit()
|
await conn.commit()
|
||||||
logger.debug("Created app_settings table with default values")
|
logger.debug("Created app_settings table with default values")
|
||||||
|
|
||||||
|
|
||||||
|
async def _migrate_010_add_advert_interval(conn: aiosqlite.Connection) -> None:
|
||||||
|
"""
|
||||||
|
Add advert_interval column to app_settings table.
|
||||||
|
|
||||||
|
This enables configurable periodic advertisement interval (default 0 = disabled).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
await conn.execute("ALTER TABLE app_settings ADD COLUMN advert_interval INTEGER DEFAULT 0")
|
||||||
|
logger.debug("Added advert_interval column to app_settings")
|
||||||
|
except aiosqlite.OperationalError as e:
|
||||||
|
if "duplicate column" in str(e).lower():
|
||||||
|
logger.debug("advert_interval column already exists, skipping")
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
await conn.commit()
|
||||||
|
|||||||
@@ -255,3 +255,7 @@ class AppSettings(BaseModel):
|
|||||||
default=False,
|
default=False,
|
||||||
description="Whether preferences have been migrated from localStorage",
|
description="Whether preferences have been migrated from localStorage",
|
||||||
)
|
)
|
||||||
|
advert_interval: int = Field(
|
||||||
|
default=0,
|
||||||
|
description="Periodic advertisement interval in seconds (0 = disabled)",
|
||||||
|
)
|
||||||
|
|||||||
+45
-12
@@ -32,8 +32,9 @@ MESSAGE_POLL_INTERVAL = 5
|
|||||||
# Periodic advertisement task handle
|
# Periodic advertisement task handle
|
||||||
_advert_task: asyncio.Task | None = None
|
_advert_task: asyncio.Task | None = None
|
||||||
|
|
||||||
# Advertisement interval in seconds (1 hour)
|
# Default check interval when periodic advertising is disabled (seconds)
|
||||||
ADVERT_INTERVAL = 3600
|
# We still need to periodically check if it's been enabled
|
||||||
|
ADVERT_CHECK_INTERVAL = 60
|
||||||
|
|
||||||
# Counter to pause polling during repeater operations (supports nested pauses)
|
# Counter to pause polling during repeater operations (supports nested pauses)
|
||||||
_polling_pause_count: int = 0
|
_polling_pause_count: int = 0
|
||||||
@@ -358,31 +359,63 @@ 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 sends advertisements.
|
||||||
|
|
||||||
|
Reads the interval from app_settings on each iteration, allowing
|
||||||
|
dynamic configuration changes without restarting the task.
|
||||||
|
If interval is 0, advertising is disabled but the task continues
|
||||||
|
running to detect when it's re-enabled.
|
||||||
|
"""
|
||||||
|
from app.repository import AppSettingsRepository
|
||||||
|
|
||||||
|
last_advert_time = 0.0
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
await asyncio.sleep(ADVERT_INTERVAL)
|
# Get current interval setting
|
||||||
|
settings = await AppSettingsRepository.get()
|
||||||
|
interval = settings.advert_interval
|
||||||
|
|
||||||
if radio_manager.is_connected:
|
if interval <= 0:
|
||||||
await send_advertisement()
|
# Advertising disabled - check again later
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
def start_periodic_advert():
|
def start_periodic_advert():
|
||||||
"""Start the periodic advertisement background task."""
|
"""Start the periodic advertisement background task.
|
||||||
|
|
||||||
|
The task reads interval from app_settings dynamically, so it will
|
||||||
|
adapt to configuration changes without restart.
|
||||||
|
"""
|
||||||
global _advert_task
|
global _advert_task
|
||||||
if _advert_task is None or _advert_task.done():
|
if _advert_task is None or _advert_task.done():
|
||||||
_advert_task = asyncio.create_task(_periodic_advert_loop())
|
_advert_task = asyncio.create_task(_periodic_advert_loop())
|
||||||
logger.info(
|
logger.info("Started periodic advertisement task (interval configured in settings)")
|
||||||
"Started periodic advertisement (interval: %ds / %d min)",
|
|
||||||
ADVERT_INTERVAL,
|
|
||||||
ADVERT_INTERVAL // 60,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def stop_periodic_advert():
|
async def stop_periodic_advert():
|
||||||
|
|||||||
+8
-1
@@ -717,7 +717,8 @@ class AppSettingsRepository:
|
|||||||
cursor = await db.conn.execute(
|
cursor = await db.conn.execute(
|
||||||
"""
|
"""
|
||||||
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
|
||||||
FROM app_settings WHERE id = 1
|
FROM app_settings WHERE id = 1
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
@@ -765,6 +766,7 @@ class AppSettingsRepository:
|
|||||||
sidebar_sort_order=sort_order,
|
sidebar_sort_order=sort_order,
|
||||||
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -775,6 +777,7 @@ class AppSettingsRepository:
|
|||||||
sidebar_sort_order: str | None = None,
|
sidebar_sort_order: str | None = None,
|
||||||
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,
|
||||||
) -> AppSettings:
|
) -> AppSettings:
|
||||||
"""Update app settings. Only provided fields are updated."""
|
"""Update app settings. Only provided fields are updated."""
|
||||||
updates = []
|
updates = []
|
||||||
@@ -805,6 +808,10 @@ class AppSettingsRepository:
|
|||||||
updates.append("preferences_migrated = ?")
|
updates.append("preferences_migrated = ?")
|
||||||
params.append(1 if preferences_migrated else 0)
|
params.append(1 if preferences_migrated else 0)
|
||||||
|
|
||||||
|
if advert_interval is not None:
|
||||||
|
updates.append("advert_interval = ?")
|
||||||
|
params.append(advert_interval)
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ class AppSettingsUpdate(BaseModel):
|
|||||||
default=None,
|
default=None,
|
||||||
description="Sidebar sort order: 'recent' or 'alpha'",
|
description="Sidebar sort order: 'recent' or 'alpha'",
|
||||||
)
|
)
|
||||||
|
advert_interval: int | None = Field(
|
||||||
|
default=None,
|
||||||
|
ge=0,
|
||||||
|
description="Periodic advertisement interval in seconds (0 = disabled)",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FavoriteRequest(BaseModel):
|
class FavoriteRequest(BaseModel):
|
||||||
@@ -85,6 +90,10 @@ async def update_settings(update: AppSettingsUpdate) -> AppSettings:
|
|||||||
logger.info("Updating sidebar_sort_order to %s", update.sidebar_sort_order)
|
logger.info("Updating sidebar_sort_order to %s", update.sidebar_sort_order)
|
||||||
kwargs["sidebar_sort_order"] = update.sidebar_sort_order
|
kwargs["sidebar_sort_order"] = update.sidebar_sort_order
|
||||||
|
|
||||||
|
if update.advert_interval is not None:
|
||||||
|
logger.info("Updating advert_interval to %d", update.advert_interval)
|
||||||
|
kwargs["advert_interval"] = update.advert_interval
|
||||||
|
|
||||||
if kwargs:
|
if kwargs:
|
||||||
return await AppSettingsRepository.update(**kwargs)
|
return await AppSettingsRepository.update(**kwargs)
|
||||||
|
|
||||||
|
|||||||
-1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+24
-24
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -13,8 +13,8 @@
|
|||||||
<link rel="shortcut icon" href="/favicon.ico" />
|
<link rel="shortcut icon" href="/favicon.ico" />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
<link rel="manifest" href="/site.webmanifest" />
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
<script type="module" crossorigin src="/assets/index-CJZtvuFn.js"></script>
|
<script type="module" crossorigin src="/assets/index-lvsK6fUB.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BH0I1D-N.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-BsZWFlnF.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -100,6 +100,9 @@ export function SettingsModal({
|
|||||||
const [cleaning, setCleaning] = useState(false);
|
const [cleaning, setCleaning] = useState(false);
|
||||||
const [autoDecryptOnAdvert, setAutoDecryptOnAdvert] = useState(false);
|
const [autoDecryptOnAdvert, setAutoDecryptOnAdvert] = useState(false);
|
||||||
|
|
||||||
|
// Advertisement interval state
|
||||||
|
const [advertInterval, setAdvertInterval] = useState('0');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (config) {
|
if (config) {
|
||||||
setName(config.name);
|
setName(config.name);
|
||||||
@@ -117,6 +120,7 @@ export function SettingsModal({
|
|||||||
if (appSettings) {
|
if (appSettings) {
|
||||||
setMaxRadioContacts(String(appSettings.max_radio_contacts));
|
setMaxRadioContacts(String(appSettings.max_radio_contacts));
|
||||||
setAutoDecryptOnAdvert(appSettings.auto_decrypt_dm_on_advert);
|
setAutoDecryptOnAdvert(appSettings.auto_decrypt_dm_on_advert);
|
||||||
|
setAdvertInterval(String(appSettings.advert_interval));
|
||||||
}
|
}
|
||||||
}, [appSettings]);
|
}, [appSettings]);
|
||||||
|
|
||||||
@@ -220,9 +224,17 @@ export function SettingsModal({
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Save radio name
|
||||||
const update: RadioConfigUpdate = { name };
|
const update: RadioConfigUpdate = { name };
|
||||||
await onSave(update);
|
await onSave(update);
|
||||||
toast.success('Identity saved');
|
|
||||||
|
// Save advert interval to app settings
|
||||||
|
const newAdvertInterval = parseInt(advertInterval, 10);
|
||||||
|
if (!isNaN(newAdvertInterval) && newAdvertInterval !== appSettings?.advert_interval) {
|
||||||
|
await onSaveAppSettings({ advert_interval: newAdvertInterval });
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success('Identity settings saved');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Failed to save');
|
setError(err instanceof Error ? err.message : 'Failed to save');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -349,7 +361,8 @@ export function SettingsModal({
|
|||||||
<DialogTitle>Radio & Settings</DialogTitle>
|
<DialogTitle>Radio & Settings</DialogTitle>
|
||||||
<DialogDescription className="sr-only">
|
<DialogDescription className="sr-only">
|
||||||
{activeTab === 'radio' && 'Configure radio frequency, power, and location settings'}
|
{activeTab === 'radio' && 'Configure radio frequency, power, and location settings'}
|
||||||
{activeTab === 'identity' && 'Manage radio name, public key, and private key'}
|
{activeTab === 'identity' &&
|
||||||
|
'Manage radio name, public key, private key, and advertising settings'}
|
||||||
{activeTab === 'serial' && 'View serial port connection and configure contact sync'}
|
{activeTab === 'serial' && 'View serial port connection and configure contact sync'}
|
||||||
{activeTab === 'database' && 'View database statistics and clean up old packets'}
|
{activeTab === 'database' && 'View database statistics and clean up old packets'}
|
||||||
{activeTab === 'advertise' && 'Send a flood advertisement to announce your presence'}
|
{activeTab === 'advertise' && 'Send a flood advertisement to announce your presence'}
|
||||||
@@ -526,8 +539,27 @@ export function SettingsModal({
|
|||||||
<Input id="name" value={name} onChange={(e) => setName(e.target.value)} />
|
<Input id="name" value={name} onChange={(e) => setName(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="advert-interval">Periodic Advertising Interval</Label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
id="advert-interval"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
value={advertInterval}
|
||||||
|
onChange={(e) => setAdvertInterval(e.target.value)}
|
||||||
|
className="w-28"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-muted-foreground">seconds (0 = off)</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
How often to automatically advertise presence. Set to 0 to disable. Recommended:
|
||||||
|
3600 (1 hour) or higher.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Button onClick={handleSaveIdentity} disabled={loading} className="w-full">
|
<Button onClick={handleSaveIdentity} disabled={loading} className="w-full">
|
||||||
{loading ? 'Saving...' : 'Set Name'}
|
{loading ? 'Saving...' : 'Save Identity Settings'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|||||||
@@ -136,12 +136,14 @@ export interface AppSettings {
|
|||||||
sidebar_sort_order: 'recent' | 'alpha';
|
sidebar_sort_order: 'recent' | 'alpha';
|
||||||
last_message_times: Record<string, number>;
|
last_message_times: Record<string, number>;
|
||||||
preferences_migrated: boolean;
|
preferences_migrated: boolean;
|
||||||
|
advert_interval: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppSettingsUpdate {
|
export interface AppSettingsUpdate {
|
||||||
max_radio_contacts?: number;
|
max_radio_contacts?: number;
|
||||||
auto_decrypt_dm_on_advert?: boolean;
|
auto_decrypt_dm_on_advert?: boolean;
|
||||||
sidebar_sort_order?: 'recent' | 'alpha';
|
sidebar_sort_order?: 'recent' | 'alpha';
|
||||||
|
advert_interval?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MigratePreferencesRequest {
|
export interface MigratePreferencesRequest {
|
||||||
|
|||||||
@@ -100,8 +100,8 @@ class TestMigration001:
|
|||||||
# Run migrations
|
# Run migrations
|
||||||
applied = await run_migrations(conn)
|
applied = await run_migrations(conn)
|
||||||
|
|
||||||
assert applied == 9 # All 9 migrations run
|
assert applied == 10 # All 10 migrations run
|
||||||
assert await get_version(conn) == 9
|
assert await get_version(conn) == 10
|
||||||
|
|
||||||
# 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 == 9 # All 9 migrations run
|
assert applied1 == 10 # All 10 migrations run
|
||||||
assert applied2 == 0 # No migrations on second run
|
assert applied2 == 0 # No migrations on second run
|
||||||
assert await get_version(conn) == 9
|
assert await get_version(conn) == 10
|
||||||
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 9 migrations applied (version incremented) but no error
|
# All 10 migrations applied (version incremented) but no error
|
||||||
assert applied == 9
|
assert applied == 10
|
||||||
assert await get_version(conn) == 9
|
assert await get_version(conn) == 10
|
||||||
finally:
|
finally:
|
||||||
await conn.close()
|
await conn.close()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user