Skip to content

Commit 1efceae

Browse files
committed
Added speedtest_scheduled_asset_id
1 parent cdac916 commit 1efceae

12 files changed

Lines changed: 10019 additions & 23 deletions

File tree

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"enabled": true,
2+
"enabled": false,
33
"name": "Auto Deploy on Code Change",
44
"description": "Automatically deploys the SDK app to the router after Kiro finishes making code changes. Uses .kiro/steering/deploy.md.",
55
"version": "1",
@@ -9,7 +9,5 @@
99
"then": {
1010
"type": "askAgent",
1111
"prompt": "If you already ran a deploy this session, do nothing — the app is already deployed. Otherwise, if you created or modified any SDK app Python files during this session, read .kiro/steering/deploy.md and follow its instructions. Read sdk_settings.ini to get the app_name. If no app code was changed, do nothing."
12-
},
13-
"workspaceFolderName": "sdk-samples",
14-
"shortName": "auto-deploy-app"
12+
}
1513
}
Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
{
2+
"enabled": true,
23
"name": "Setup Dev Environment",
3-
"version": "1.0.0",
4-
"description": "Automatically creates a Python virtual environment and installs SDK dependencies if .venv is missing",
4+
"description": "Creates a Python virtual environment and installs SDK dependencies. Run manually via the play button in the Agent Hooks panel.",
5+
"version": "1",
56
"when": {
6-
"type": "promptSubmit"
7+
"type": "userTriggered"
78
},
89
"then": {
910
"type": "runCommand",
10-
"command": "test -d .venv || (python3 make.py setup || python make.py setup)"
11+
"command": "python3 make.py setup || python make.py setup"
1112
}
1213
}

.kiro/steering/coding-standards.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ Applications run on Cradlepoint routers using Python 3.8.
8282
- **NO memory limits in docker-compose** - Cradlepoint's container runtime does not support `mem_limit`, `deploy.resources.limits.memory`, or any memory constraint options. Omit them entirely or the compose validation will fail
8383
- **Use Compose version "2.4"** - Cradlepoint's container runtime uses Compose v2.4, not v3. Always set `version: "2.4"` in docker-compose files
8484
- **Use `restart: unless-stopped`** - Cradlepoint does not allow `restart: always`. Use `unless-stopped` instead
85+
- **NO `network_mode: host`** - Cradlepoint's container runtime only supports bridged networking. Use `ports:` to publish ports instead of `network_mode: host`
8586
- **Set shared memory with `shm_size`** - some services (e.g. databases, browsers) need more than the default 64MB `/dev/shm`. Set `shm_size: '1gb'` (or appropriate size) at the service level in docker-compose
8687

8788
Example deploy via curl:
@@ -164,6 +165,7 @@ if nmea_sentences:
164165
## Web Development
165166

166167
- **ALWAYS use Python's built-in `http.server` module** - never use third-party web frameworks (Flask, Bottle, CherryPy, etc.). The native `http.server.HTTPServer` is available on cppython and has zero dependencies
168+
- **LAN client access to router ports requires firewall zone forwarding** - For a client device on the LAN to reach a port on the router (SDK app web UI, SNMP agent, container-published port, etc.), the firewall must have a forwarding rule from the Primary LAN Zone to the Router Zone. If an app is running but LAN clients get connection timeouts, the zone forwarding is the first thing to check. This is configured at `config/firewall/zone_fwd` or via the NCOS UI under Security > Zone Firewall.
167169
- **Default port: 8000** - use port 8000 for web applications unless there's a conflict
168170
- **ALWAYS set SO_REUSEADDR** before binding: `server.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)`
169171
- **Port conflicts on redeployment** - SO_REUSEADDR doesn't prevent "Address in use" errors when redeploying without router reboot. If port 8000 is in use, either reboot router or use a different port (8001, 8002, etc.)

AutoInstall_Web/AutoInstall_Web.py

Lines changed: 129 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,13 @@ def install_cancelled_check():
539539
# Rule IDs created by ensure_sims_have_distinct_rules (for cleanup/deletion)
540540
created_rule_ids = set()
541541

542+
# SIM device uid (mdm-xxx) most recently set to alwayson by switch_to_sim.
543+
# Used by cleanup_wan_profile_changes to wait for this SIM to come up before
544+
# re-enabling any previously-disabled rules — prevents the router from falling
545+
# back to a working SIM that was just disabled while the target SIM is still
546+
# mid-connection (which would misleadingly look like "SIM1 was not disabled").
547+
current_active_sim = None
548+
542549

543550
def write_results_appdata(sims):
544551
"""Write a one-line parseable results string to appdata 'results'.
@@ -1385,22 +1392,35 @@ def switch_to_sim(active_sim, all_sims, rule_states):
13851392

13861393
# Step 1: Disable ALL other SIM rules first
13871394
other_rule_ids = set()
1395+
other_sims = []
13881396
for sim_device in all_sims:
13891397
if sim_device == active_sim:
13901398
continue
13911399
rule_id = get_sim_rule_id(sim_device)
13921400
if rule_id and rule_id != active_rule_id:
13931401
other_rule_ids.add(rule_id)
1402+
other_sims.append(sim_device)
1403+
# Revert def_conn_state on the rules we're about to disable so we never
1404+
# leave a rule in the contradictory state "disabled=True AND alwayson"
1405+
# (which can happen across iterations if this rule was previously the
1406+
# active rule and had def_conn_state set to 'alwayson').
1407+
if other_rule_ids:
1408+
_restore_def_conn_state_from_originals(other_rule_ids)
13941409
for rule_id in other_rule_ids:
13951410
_capture_rule_state(rule_states, rule_id)
13961411
cp.put(f'config/wan/rules2/{rule_id}/disabled', True)
13971412
cp.log(f'Disabled rule {get_rule_display_name(rule_id)}')
13981413
write_log(f'Disabled rule {get_rule_display_name(rule_id)}')
1399-
time.sleep(2)
1414+
# Wait briefly for other SIMs to actually drop their connections so the
1415+
# router doesn't keep using a "disabled" SIM while we try to bring up
1416+
# the new one.
1417+
_wait_for_sims_disconnected(other_sims, timeout_sec=30)
14001418

14011419
# Step 2: Enable active SIM rule with alwayson
14021420
cp.put(f'config/wan/rules2/{active_rule_id}/disabled', False)
14031421
cp.put(f'config/wan/rules2/{active_rule_id}/def_conn_state', 'alwayson')
1422+
global current_active_sim
1423+
current_active_sim = active_sim
14041424
cp.log(f'Set {port_sim} rule {get_rule_display_name(active_rule_id)} to alwayson')
14051425
write_log(f'Set {port_sim} rule {get_rule_display_name(active_rule_id)} to alwayson')
14061426

@@ -1426,33 +1446,114 @@ def switch_to_sim(active_sim, all_sims, rule_states):
14261446
time.sleep(poll_interval)
14271447
elapsed += poll_interval
14281448

1429-
# Step 4: Re-enable other rules only after connected
1430-
if connected:
1431-
for rule_id in other_rule_ids:
1432-
try:
1433-
cp.put(f'config/wan/rules2/{rule_id}/disabled', False)
1434-
cp.log(f'Re-enabled rule {get_rule_display_name(rule_id)}')
1435-
except Exception as e:
1436-
cp.log(f'Could not re-enable rule {get_rule_display_name(rule_id)}: {e}')
1437-
else:
1449+
# NOTE: We intentionally do NOT re-enable the other SIM rules here.
1450+
# Re-enabling them while this SIM is alwayson creates a window where a
1451+
# higher-priority rule on a different modem can take over and disrupt
1452+
# testing. The other rules stay disabled until the next explicit action:
1453+
# - Next switch_to_sim call (handles state for the new active SIM)
1454+
# - cleanup_wan_profile_changes on cancel/fail/completion
1455+
if not connected:
14381456
cp.log(f'{port_sim} did not connect within {timeout_sec}s')
14391457
write_log(f'{port_sim} did not connect within {timeout_sec}s')
14401458
return True
1459+
except InstallCancelledException:
1460+
# Let the top-level cancel handler run its proper cleanup path.
1461+
raise
14411462
except Exception as e:
14421463
port_sim = (get_display_port(active_sim) or '') + ' ' + (get_sim_slot(active_sim) or '')
14431464
port_sim = port_sim.strip() or 'SIM'
14441465
cp.log(f'Error switching to {port_sim}: {e}')
14451466
write_log(f'Error switching to {port_sim}: {e}')
14461467
return False
14471468

1448-
def cleanup_wan_profile_changes(rule_states):
1469+
def _wait_for_sims_disconnected(sim_devices, timeout_sec=30, poll_interval=2):
1470+
"""Poll the given SIMs' connection_state until all report not-connected or timeout.
1471+
Does NOT check install_cancelled so switching state can't be left half-applied.
1472+
Returns True if all disconnected, False on timeout."""
1473+
if not sim_devices:
1474+
return True
1475+
try:
1476+
pending = list(sim_devices)
1477+
elapsed = 0
1478+
while elapsed < timeout_sec:
1479+
still_connected = []
1480+
for sim in pending:
1481+
try:
1482+
state = cp.get(f'status/wan/devices/{sim}/status/connection_state')
1483+
if state == 'connected':
1484+
still_connected.append(sim)
1485+
except Exception:
1486+
pass
1487+
if not still_connected:
1488+
return True
1489+
pending = still_connected
1490+
time.sleep(poll_interval)
1491+
elapsed += poll_interval
1492+
labels = []
1493+
for sim in pending:
1494+
port_display = get_display_port(sim) or get_sim_port(sim) or ''
1495+
sim_slot_str = get_sim_slot(sim) or ''
1496+
labels.append((port_display + ' ' + sim_slot_str).strip() or sim)
1497+
cp.log(f'Timed out waiting for SIMs to disconnect after {timeout_sec}s: {", ".join(labels)}')
1498+
return False
1499+
except Exception as e:
1500+
cp.log(f'Error waiting for SIMs to disconnect: {e}')
1501+
return False
1502+
1503+
1504+
def _wait_for_active_sim_connected(active_sim, timeout_sec=60, poll_interval=3):
1505+
"""Poll the given SIM's connection_state until it is 'connected' or timeout.
1506+
Does NOT check install_cancelled so cleanup can finish after a cancel.
1507+
Returns True if connected, False on timeout or error."""
1508+
if not active_sim:
1509+
return True
1510+
try:
1511+
port_display = get_display_port(active_sim) or get_sim_port(active_sim) or ''
1512+
sim_slot_str = get_sim_slot(active_sim) or ''
1513+
port_sim = (port_display + ' ' + sim_slot_str).strip() or 'SIM'
1514+
conn_path = f'status/wan/devices/{active_sim}/status/connection_state'
1515+
try:
1516+
if cp.get(conn_path) == 'connected':
1517+
return True
1518+
except Exception:
1519+
pass
1520+
cp.log(f'Waiting up to {timeout_sec}s for {port_sim} to connect before restoring other SIM rules')
1521+
write_log(f'Waiting up to {timeout_sec}s for {port_sim} to connect before restoring other SIM rules')
1522+
elapsed = 0
1523+
while elapsed < timeout_sec:
1524+
try:
1525+
if cp.get(conn_path) == 'connected':
1526+
cp.log(f'{port_sim} connected after {elapsed}s; proceeding with restore')
1527+
return True
1528+
except Exception:
1529+
pass
1530+
time.sleep(poll_interval)
1531+
elapsed += poll_interval
1532+
cp.log(f'{port_sim} still not connected after {timeout_sec}s; restoring other SIM rules anyway so router has WAN')
1533+
write_log(f'{port_sim} still not connected after {timeout_sec}s; restoring other SIM rules anyway so router has WAN')
1534+
return False
1535+
except Exception as e:
1536+
cp.log(f'Error waiting for active SIM to connect: {e}')
1537+
return False
1538+
1539+
1540+
def cleanup_wan_profile_changes(rule_states, active_sim=None, wait_timeout=60):
14491541
"""Restore def_conn_state and disabled to defaults for all modified rules.
14501542
Delete def_conn_state (default ondemand) and delete disabled (default enabled)
14511543
so profiles return to a clean state regardless of prior captures.
1452-
Also deletes any per-SIM rules created by ensure_sims_have_distinct_rules."""
1453-
global created_rule_ids
1544+
Also deletes any per-SIM rules created by ensure_sims_have_distinct_rules.
1545+
1546+
If active_sim is provided (or tracked globally), wait up to wait_timeout seconds
1547+
for that SIM to be connected before re-enabling other rules. This avoids the
1548+
router falling back to a previously-disabled SIM while the new one is still
1549+
mid-connection. Pass wait_timeout=0 to skip the wait."""
1550+
global created_rule_ids, current_active_sim
14541551
if not rule_states and not created_rule_ids:
14551552
return
1553+
# Wait for the active SIM (if any) to connect before re-enabling other rules
1554+
target_sim = active_sim if active_sim is not None else current_active_sim
1555+
if target_sim and wait_timeout > 0:
1556+
_wait_for_active_sim_connected(target_sim, timeout_sec=wait_timeout)
14561557
# First restore state on all tracked rules (excluding ones we'll delete)
14571558
for rule_id in rule_states:
14581559
if rule_id in created_rule_ids:
@@ -1479,6 +1580,7 @@ def cleanup_wan_profile_changes(rule_states):
14791580
except Exception as e:
14801581
cp.log(f'Error deleting created rule {rule_id}: {e}')
14811582
created_rule_ids.clear()
1583+
current_active_sim = None
14821584

14831585
def cleanup_all_speedtest_routes_for_device(sim_device):
14841586
"""Clean up any leftover speedtest routes for a specific device."""
@@ -1726,6 +1828,9 @@ def test_sim(device, sims):
17261828
write_log(error_msg)
17271829
sims[device]['download'] = sims[device]['upload'] = 0.0
17281830
return False
1831+
except InstallCancelledException:
1832+
# Let the top-level cancel handler run its proper cleanup path.
1833+
raise
17291834
except Exception as e:
17301835
port_display = get_display_port(device) or get_sim_port(device) or ''
17311836
sim_slot = get_sim_slot(device) or ''
@@ -1890,8 +1995,10 @@ def run_auto_install():
18901995
"""Main auto-install process."""
18911996
global log_filename, install_cancelled
18921997
install_cancelled = False
1893-
global created_rule_ids
1998+
global created_rule_ids, current_active_sim, original_rule_states
18941999
created_rule_ids = set()
2000+
current_active_sim = None
2001+
original_rule_states = {}
18952002
rule_states = {}
18962003
sims = {}
18972004
try:
@@ -2040,6 +2147,8 @@ def run_auto_install():
20402147
update_status('running', 'Normalizing Config...', prog_switch)
20412148
time.sleep(0.5)
20422149
cleanup_wan_profile_changes(rule_states)
2150+
for sim_device in sims:
2151+
cleanup_all_speedtest_routes_for_device(sim_device)
20432152
update_status('error', f'Failed to switch to {port_sim}{carrier_suffix}', 0)
20442153
time.sleep(0.5)
20452154
return
@@ -2051,6 +2160,8 @@ def run_auto_install():
20512160
update_status('running', 'Normalizing Config...', prog_switch)
20522161
time.sleep(0.5)
20532162
cleanup_wan_profile_changes(rule_states)
2163+
for sim_device in sims:
2164+
cleanup_all_speedtest_routes_for_device(sim_device)
20542165
update_status('error', error_msg, 0)
20552166
time.sleep(0.5)
20562167
return
@@ -2066,13 +2177,17 @@ def run_auto_install():
20662177
update_status('running', 'Normalizing Config...', prog_testing)
20672178
time.sleep(0.5)
20682179
cleanup_wan_profile_changes(rule_states)
2180+
for sim_device in sims:
2181+
cleanup_all_speedtest_routes_for_device(sim_device)
20692182
update_status('error', error_msg, 0)
20702183
time.sleep(0.5)
20712184
return
20722185
tested_count += 1
20732186
update_status('running', 'Normalizing Config...', 80)
20742187
time.sleep(0.5)
20752188
cleanup_wan_profile_changes(rule_states)
2189+
for sim_device in sims:
2190+
cleanup_all_speedtest_routes_for_device(sim_device)
20762191
time.sleep(2)
20772192
update_status('running', 'Waiting for WAN Connection...', 82)
20782193
time.sleep(0.5)
@@ -2456,7 +2571,6 @@ def run_auto_install():
24562571
update_status('running', 'Normalizing Config...', 86)
24572572
time.sleep(0.5)
24582573
all_rules_to_restore = dict(rule_states) if rule_states else {}
2459-
global original_rule_states
24602574
for rid in original_rule_states:
24612575
if rid not in all_rules_to_restore:
24622576
all_rules_to_restore[rid] = {}

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,8 @@ Ready-to-use applications you can install from the [releases page](https://githu
210210
- **speedtest_scheduled_custom1**
211211
- Run Ookla speedtests on a cron schedule from appdata. Results are written to NCM custom1 field via the ncm PyPI library.
212212
- **Download:** [speedtest_scheduled_custom1 v1.0.1.tar.gz](https://github.com/cradlepoint/sdk-samples/releases/download/built_apps/speedtest_scheduled_custom1.v1.0.1.tar.gz)
213+
- **speedtest_scheduled_asset_id**
214+
- Run Ookla speedtests on a configurable cron schedule and write results to the asset_id field. Includes modem diagnostics (DBM, SINR) when the primary WAN is a modem.
213215
- **splunk_conntrack**
214216
- This app monitors the conntrack table and sends new connections to Splunk.
215217
- **Download:** [splunk_conntrack v1.1.1.tar.gz](https://github.com/cradlepoint/sdk-samples/releases/download/built_apps/splunk_conntrack.v1.1.1.tar.gz)

0 commit comments

Comments
 (0)